1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-17 06:52:27 +01:00

welcome tour + save completed tours in database

This commit is contained in:
Sylvain 2020-02-18 17:36:45 +01:00
parent 11a2dde776
commit 5b46edd748
14 changed files with 156 additions and 51 deletions

View File

@ -329,10 +329,10 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
};
/**
* Retreive once the notifications from the server and display a message popup for each new one.
* Retrieve once the notifications from the server and display a message popup for each new one.
* Then, periodically check for new notifications.
*/
var getNotifications = function () {
const getNotifications = function () {
$rootScope.toCheckNotifications = true;
if (!$rootScope.checkNotificationsIsInit && !!$rootScope.currentUser) {
setTimeout(function () {
@ -373,7 +373,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
/**
* Open the modal window allowing the user to log in.
*/
var openLoginModal = function (toState, toParams, callback) {
const openLoginModal = function (toState, toParams, callback) {
<% active_provider = AuthProvider.active %>
<% if active_provider.providable_type != DatabaseProvider.name %>
$window.location.href = '/sso-redirect';
@ -480,7 +480,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
* When the status changes, the callback is triggered with the new status as parameter
* Inspired by http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active#answer-1060034
*/
var onPageVisible = function (callback) {
const onPageVisible = function (callback) {
let hidden = 'hidden';
const onchange = function (evt) {

View File

@ -1,7 +1,7 @@
'use strict';
Application.Controllers.controller('HomeController', ['$scope', '$stateParams', 'homeContentPromise', 'uiTourService', '_t',
function ($scope, $stateParams, homeContentPromise, uiTourService, _t) {
Application.Controllers.controller('HomeController', ['$scope', '$stateParams', 'homeContentPromise', 'Member', 'uiTourService', '_t',
function ($scope, $stateParams, homeContentPromise, Member, uiTourService, _t) {
/* PUBLIC SCOPE */
// Home page HTML content
@ -22,8 +22,48 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams',
// We set the home page content, with the directives replacing the placeholders
$scope.homeContent = insertDirectives(homeContentPromise.setting.value);
// setup the tour
// setup the tour for admins
if ($scope.currentUser && $scope.currentUser.role === 'admin') {
setupWelcomeTour();
}
};
const insertDirectives = function (html) {
const node = document.createElement('div');
node.innerHTML = html.trim();
node.querySelectorAll('div#news').forEach((newsNode) => {
const news = document.createElement('news');
newsNode.parentNode.replaceChild(news, newsNode);
});
node.querySelectorAll('div#projects').forEach((projectsNode) => {
const projects = document.createElement('projects');
projectsNode.parentNode.replaceChild(projects, projectsNode);
});
node.querySelectorAll('div#twitter').forEach((twitterNode) => {
const twitter = document.createElement('twitter');
twitterNode.parentNode.replaceChild(twitter, twitterNode);
});
node.querySelectorAll('div#members').forEach((membersNode) => {
const members = document.createElement('members');
membersNode.parentNode.replaceChild(members, membersNode);
});
node.querySelectorAll('div#events').forEach((eventsNode) => {
const events = document.createElement('events');
eventsNode.parentNode.replaceChild(events, eventsNode);
});
return node.outerHTML;
};
const setupWelcomeTour = function () {
// get the tour defined by the ui-tour directive
const uitour = uiTourService.getTour();
// add the steps
uitour.createStep({
selector: 'body',
stepId: 'welcome',
@ -97,10 +137,18 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams',
content: _t('app.public.tour.plans.content'),
placement: 'right'
});
uitour.createStep({
selector: '.nav-primary .admin-section',
stepId: 'admin',
order: 9,
title: _t('app.public.tour.admin.title'),
content: _t('app.public.tour.admin.content'),
placement: 'right'
});
uitour.createStep({
selector: '.navbar.header li.about-page-link',
stepId: 'about',
order: 9,
order: 10,
title: _t('app.public.tour.about.title'),
content: _t('app.public.tour.about.content'),
placement: 'bottom'
@ -108,7 +156,7 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams',
uitour.createStep({
selector: '.navbar.header li.notification-center-link',
stepId: 'notifications',
order: 10,
order: 11,
title: _t('app.public.tour.notifications.title'),
content: _t('app.public.tour.notifications.content'),
placement: 'bottom'
@ -116,44 +164,47 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams',
uitour.createStep({
selector: '.navbar.header li.user-menu-dropdown',
stepId: 'profile',
order: 11,
order: 12,
title: _t('app.public.tour.profile.title'),
content: _t('app.public.tour.profile.content'),
placement: 'bottom'
});
uitour.start();
};
const insertDirectives = function (html) {
const node = document.createElement('div');
node.innerHTML = html.trim();
node.querySelectorAll('div#news').forEach((newsNode) => {
const news = document.createElement('news');
newsNode.parentNode.replaceChild(news, newsNode);
uitour.createStep({
selector: '.app-generator .app-version',
stepId: 'version',
order: 13,
title: _t('app.public.tour.version.title'),
content: _t('app.public.tour.version.content'),
placement: 'top'
});
node.querySelectorAll('div#projects').forEach((projectsNode) => {
const projects = document.createElement('projects');
projectsNode.parentNode.replaceChild(projects, projectsNode);
uitour.createStep({
selector: 'body',
stepId: 'conclusion',
order: 14,
title: _t('app.public.tour.conclusion.title'),
content: _t('app.public.tour.conclusion.content'),
placement: 'bottom',
orphan: true
});
node.querySelectorAll('div#twitter').forEach((twitterNode) => {
const twitter = document.createElement('twitter');
twitterNode.parentNode.replaceChild(twitter, twitterNode);
// on tour end, save the status in database
uitour.on('ended', function () {
if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('welcome') < 0) {
Member.completeTour({ id: $scope.currentUser.id }, { tour: 'welcome' }, function (res) {
$scope.currentUser.profile.tours = res.tours;
});
}
});
node.querySelectorAll('div#members').forEach((membersNode) => {
const members = document.createElement('members');
membersNode.parentNode.replaceChild(members, membersNode);
// if the user has never seen the tour, show him now
if ($scope.currentUser.profile.tours.indexOf('welcome') < 0) {
uitour.start();
}
// start this tour when an user press F1 - this is contextual help
window.addEventListener('keydown', function (e) {
if (e.key === 'F1') {
e.preventDefault();
uitour.start();
}
});
node.querySelectorAll('div#events').forEach((eventsNode) => {
const events = document.createElement('events');
eventsNode.parentNode.replaceChild(events, eventsNode);
});
return node.outerHTML;
};
// !!! MUST BE CALLED AT THE END of the controller

View File

@ -30,6 +30,11 @@ Application.Services.factory('Member', ['$resource', function ($resource) {
mapping: {
method: 'GET',
url: '/api/members/mapping'
},
completeTour: {
method: 'PATCH',
url: '/api/members/:id/complete_tour',
params: { id: '@id' }
}
}
);

View File

@ -72,7 +72,7 @@
<!-- Admin entries -->
<div class="line-s bg-red-dark dk " ng-if="isAuthorized('admin')"></div>
<div class="text-xs font-bold text-bordeau hidden-nav-xs padder m-t-lg m-b-sm" ng-if="isAuthorized('admin')" translate>{{ 'app.public.common.admin' }}</div>
<div class="text-xs font-bold text-bordeau hidden-nav-xs padder m-t-lg m-b-sm admin-section" ng-if="isAuthorized('admin')" translate>{{ 'app.public.common.admin' }}</div>
<ul class="nav" ng-if="isAuthorized('admin')">
<li class="" ng-repeat="navLink in adminNavLinks">
<a ng-click="toggleNavSize($event)" ga ui-sref="{{navLink.state}}" ui-sref-active="active" class="auto" data-toggle="class:nav-off-screen" data-target="#nav">

View File

@ -4,7 +4,6 @@
<div class="btn-group">
<button type="button" class="btn btn-sm btn-default" ng-if="tourStep.isPrev()" ng-click="tour.prev()" translate>{{ 'app.public.tour.previous' }}</button>
<button type="button" class="btn btn-sm btn-default" ng-if="tourStep.isNext()" ng-click="tour.next()" translate>{{ 'app.public.tour.next' }}</button>
<button type="button" class="btn btn-sm btn-default" ng-click="tour.pause()" translate>{{ 'app.public.tour.pause' }}</button>
</div>
<button type="button" class="btn btn-sm btn-default" ng-click="tour.end()" translate>{{ 'app.public.tour.end' }}</button>
</div>

View File

@ -3,7 +3,7 @@
# API Controller for resources of type User with role 'member'
class API::MembersController < API::ApiController
before_action :authenticate_user!, except: [:last_subscribed]
before_action :set_member, only: %i[update destroy merge]
before_action :set_member, only: %i[update destroy merge complete_tour]
respond_to :json
def index
@ -189,6 +189,15 @@ class API::MembersController < API::ApiController
@members = User.includes(:profile)
end
def complete_tour
authorize @member
tours = "#{@member.profile.tours} #{params[:tour]}"
@member.profile.update_attributes(tours: tours.strip)
render json: { tours: @member.profile.tours }
end
private
def set_member

View File

@ -31,6 +31,10 @@ class UserPolicy < ApplicationPolicy
user.id == record.id
end
def complete_tour?
user.id == record.id
end
%w[list create mapping].each do |action|
define_method "#{action}?" do
user.admin?

View File

@ -21,6 +21,7 @@ json.profile do
json.website member.profile.website
json.job member.profile.job
json.extract! member.profile, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr
json.tours member.profile.tours&.split || []
end
json.invoicing_profile do

View File

@ -89,7 +89,14 @@
<![endif]-->
</head>
<body ng-controller="ApplicationController" ng-init="setCurrentUser(<%= current_user ? current_user.to_json : 'null' %>)" ng-cloak ui-tour ui-tour-backdrop="true" ui-tour-template-url="'<%= asset_path "shared/tour-step-template.html" %>'" ui-tour-scroll-offset="0">
<body ng-controller="ApplicationController"
ng-init="setCurrentUser(<%= current_user ? current_user.to_json : 'null' %>)"
ng-cloak
ui-tour
ui-tour-backdrop="true"
ui-tour-template-url="'<%= asset_path "shared/tour-step-template.html" %>'"
ui-tour-use-hotkeys="true"
>
<div growl></div>
<%= flash_messages %>

View File

@ -381,7 +381,6 @@ fr:
tour:
previous: "Précédent"
next: "Suivant"
pause: "Pause"
end: "Terminer la visite"
welcome:
title: "Bienvenue dans Fab-Manager"
@ -403,20 +402,29 @@ fr:
content: "Une soirée porte ouverte ou un stage pour fabriquer sa lampe de bureau ? C'est par ici !"
calendar:
title: "Le calendrier publique"
content: "Visualisez en un clin d'oeil tout ce qui est prévu au programme des prochaines semaines"
content: "Visualisez en un clin d'oeil tout ce qui est prévu au programme des prochaines semaines."
projects:
title: "Les projets"
content: "Documentez et partagez avec la communauté toutes vos réalisations"
content: "Documentez et partagez avec la communauté toutes vos réalisations."
plans:
title: "Les abonnements"
content: "Les abonnements offrent un moyen de segmenter vos tarifs et d'accorder des avantages aux utilisateurs réguliers"
content: "Les abonnements offrent un moyen de segmenter vos tarifs et d'accorder des avantages aux utilisateurs réguliers."
admin:
title: "Section administrateur"
content: "L'ensemble des éléments ci-dessous ne sont accessibles qu'aux administrateurs. Ils permettent de gérer et de configurer Fab-Manager. À la fin de cette visite, cliquez sur l'un d'entre eux pour en savoir plus."
about:
title: "À propos"
content: "Une page que vous pouvez entièrement personnaliser, pour y présenter votre activité"
content: "Une page que vous pouvez entièrement personnaliser, pour y présenter votre activité."
notifications:
title: "Centre de notifications"
content: "Chaque fois qu'il se passe quelque chose d'important, vous serez notifié ici"
content: "Chaque fois qu'il se passe quelque chose d'important, vous serez notifié ici."
profile:
title: "Menu utilisateur"
content: "Retrouvez ici vos informations personnelles ainsi que toute votre activité sur Fab-Manager"
content: "Retrouvez ici vos informations personnelles ainsi que toute votre activité sur Fab-Manager."
version:
title: "Version de l'application"
content: "Passez votre curseur sur cette icône pour connaître la version de Fab-Manager. Si vous n'êtes pas à jour, cela vous sera signalé ici et vous pourrez alors obtenir des détails en cliquant dessus."
conclusion:
title: "Merci de votre attention"
content: "Affichez de l'aide en appuyant sur <strong>F1</strong> à n'importe quel moment.<br> Bonne continuation avec Fab-Manager."

View File

@ -58,6 +58,7 @@ Rails.application.routes.draw do
post 'list', action: 'list', on: :collection
get 'search/:query', action: 'search', on: :collection
get 'mapping', action: 'mapping', on: :collection
patch ':id/complete_tour', action: 'complete_tour', on: :collection
end
resources :reservations, only: %i[show create index update]
resources :notifications, only: %i[index show update] do

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# From this migration, we distinct multiple stylesheets by their name (previously there was one only one for the main theme override)
class AddNameToStylesheet < ActiveRecord::Migration
def change
add_column :stylesheets, :name, :string

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
# From this migration, we save in database the "feature tours" viewed by each users to prevent displaying them many times
class AddToursToProfile < ActiveRecord::Migration
def change
add_column :profiles, :tours, :string
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20200127111404) do
ActiveRecord::Schema.define(version: 20200218092221) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -465,6 +465,14 @@ ActiveRecord::Schema.define(version: 20200127111404) do
add_index "plans", ["group_id"], name: "index_plans_on_group_id", using: :btree
create_table "plans_availabilities", force: :cascade do |t|
t.integer "plan_id"
t.integer "availability_id"
end
add_index "plans_availabilities", ["availability_id"], name: "index_plans_availabilities_on_availability_id", using: :btree
add_index "plans_availabilities", ["plan_id"], name: "index_plans_availabilities_on_plan_id", using: :btree
create_table "price_categories", force: :cascade do |t|
t.string "name"
t.text "conditions"
@ -511,6 +519,7 @@ ActiveRecord::Schema.define(version: 20200127111404) do
t.string "lastfm"
t.string "flickr"
t.string "job"
t.string "tours"
end
add_index "profiles", ["user_id"], name: "index_profiles_on_user_id", using: :btree