1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-06 01:08:21 +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. * Then, periodically check for new notifications.
*/ */
var getNotifications = function () { const getNotifications = function () {
$rootScope.toCheckNotifications = true; $rootScope.toCheckNotifications = true;
if (!$rootScope.checkNotificationsIsInit && !!$rootScope.currentUser) { if (!$rootScope.checkNotificationsIsInit && !!$rootScope.currentUser) {
setTimeout(function () { setTimeout(function () {
@ -373,7 +373,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
/** /**
* Open the modal window allowing the user to log in. * 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 %> <% active_provider = AuthProvider.active %>
<% if active_provider.providable_type != DatabaseProvider.name %> <% if active_provider.providable_type != DatabaseProvider.name %>
$window.location.href = '/sso-redirect'; $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 * 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 * 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'; let hidden = 'hidden';
const onchange = function (evt) { const onchange = function (evt) {

View File

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

View File

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

View File

@ -72,7 +72,7 @@
<!-- Admin entries --> <!-- Admin entries -->
<div class="line-s bg-red-dark dk " ng-if="isAuthorized('admin')"></div> <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')"> <ul class="nav" ng-if="isAuthorized('admin')">
<li class="" ng-repeat="navLink in adminNavLinks"> <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"> <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"> <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.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-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> </div>
<button type="button" class="btn btn-sm btn-default" ng-click="tour.end()" translate>{{ 'app.public.tour.end' }}</button> <button type="button" class="btn btn-sm btn-default" ng-click="tour.end()" translate>{{ 'app.public.tour.end' }}</button>
</div> </div>

View File

@ -3,7 +3,7 @@
# API Controller for resources of type User with role 'member' # API Controller for resources of type User with role 'member'
class API::MembersController < API::ApiController class API::MembersController < API::ApiController
before_action :authenticate_user!, except: [:last_subscribed] 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 respond_to :json
def index def index
@ -189,6 +189,15 @@ class API::MembersController < API::ApiController
@members = User.includes(:profile) @members = User.includes(:profile)
end 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 private
def set_member def set_member

View File

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

View File

@ -21,6 +21,7 @@ json.profile do
json.website member.profile.website json.website member.profile.website
json.job member.profile.job 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.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 end
json.invoicing_profile do json.invoicing_profile do

View File

@ -89,7 +89,14 @@
<![endif]--> <![endif]-->
</head> </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> <div growl></div>
<%= flash_messages %> <%= flash_messages %>

View File

@ -381,7 +381,6 @@ fr:
tour: tour:
previous: "Précédent" previous: "Précédent"
next: "Suivant" next: "Suivant"
pause: "Pause"
end: "Terminer la visite" end: "Terminer la visite"
welcome: welcome:
title: "Bienvenue dans Fab-Manager" 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 !" content: "Une soirée porte ouverte ou un stage pour fabriquer sa lampe de bureau ? C'est par ici !"
calendar: calendar:
title: "Le calendrier publique" 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: projects:
title: "Les projets" 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: plans:
title: "Les abonnements" 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: about:
title: "À propos" 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: notifications:
title: "Centre de 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: profile:
title: "Menu utilisateur" 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 post 'list', action: 'list', on: :collection
get 'search/:query', action: 'search', on: :collection get 'search/:query', action: 'search', on: :collection
get 'mapping', action: 'mapping', on: :collection get 'mapping', action: 'mapping', on: :collection
patch ':id/complete_tour', action: 'complete_tour', on: :collection
end end
resources :reservations, only: %i[show create index update] resources :reservations, only: %i[show create index update]
resources :notifications, only: %i[index show update] do 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 class AddNameToStylesheet < ActiveRecord::Migration
def change def change
add_column :stylesheets, :name, :string 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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 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| create_table "price_categories", force: :cascade do |t|
t.string "name" t.string "name"
t.text "conditions" t.text "conditions"
@ -511,6 +519,7 @@ ActiveRecord::Schema.define(version: 20200127111404) do
t.string "lastfm" t.string "lastfm"
t.string "flickr" t.string "flickr"
t.string "job" t.string "job"
t.string "tours"
end end
add_index "profiles", ["user_id"], name: "index_profiles_on_user_id", using: :btree add_index "profiles", ["user_id"], name: "index_profiles_on_user_id", using: :btree