From f0346a73ec6fb987b8f1be105f5f5876756b2796 Mon Sep 17 00:00:00 2001 From: Riccardo Graziosi <31478034+riggraz@users.noreply.github.com> Date: Tue, 21 May 2024 19:10:18 +0200 Subject: [PATCH] Add tour and other improvements (#348) * Add tour * Add instructions to set password for OAuth users * Tenant signup improvement * Fix bug on user soft delete * Slighlty darken background color * Add a stronger confirmation for board deletion --- app/assets/images/feedback-space-created.png | Bin 0 -> 3934 bytes app/assets/stylesheets/common/_header.scss | 5 + app/assets/stylesheets/common/_index.scss | 8 + app/assets/stylesheets/components/Board.scss | 4 + .../stylesheets/components/TenantSignUp.scss | 2 +- app/assets/stylesheets/constants/_colors.scss | 2 +- app/controllers/application_controller.rb | 9 ++ app/controllers/o_auths_controller.rb | 3 +- app/controllers/registrations_controller.rb | 3 +- app/javascript/components/Board/BoardP.tsx | 50 +++--- .../SiteSettings/Boards/BoardEditable.tsx | 2 +- ...nUpPage.tsx => ConfirmEmailSignUpPage.tsx} | 4 +- .../TenantSignUp/ConfirmOAuthSignUpPage.tsx | 36 +++++ .../TenantSignUp/TenantSignUpForm.tsx | 2 + .../components/TenantSignUp/TenantSignUpP.tsx | 42 ++++-- .../TenantSignUp/UserSignUpForm.tsx | 13 +- .../components/TenantSignUp/index.tsx | 3 + app/javascript/components/Tour/Tour.tsx | 142 ++++++++++++++++++ app/models/user.rb | 3 +- app/views/layouts/_header.html.erb | 17 ++- app/views/layouts/_promo_banner.html.erb | 12 ++ app/views/layouts/application.html.erb | 8 + app/views/tenants/new.html.erb | 1 + app/workflows/create_stripe_customer.rb | 4 + .../create_welcome_entities_workflow.rb | 6 +- config/locales/en.yml | 7 + ...8_add_devise_trackable_columns_to_users.rb | 9 ++ db/schema.rb | 7 +- package.json | 1 + spec/factories/users.rb | 6 + .../site_settings_boards_spec.rb | 2 +- yarn.lock | 115 +++++++++++++- 32 files changed, 473 insertions(+), 55 deletions(-) create mode 100644 app/assets/images/feedback-space-created.png rename app/javascript/components/TenantSignUp/{ConfirmSignUpPage.tsx => ConfirmEmailSignUpPage.tsx} (89%) create mode 100644 app/javascript/components/TenantSignUp/ConfirmOAuthSignUpPage.tsx create mode 100644 app/javascript/components/Tour/Tour.tsx create mode 100644 app/views/layouts/_promo_banner.html.erb create mode 100644 db/migrate/20240521124018_add_devise_trackable_columns_to_users.rb diff --git a/app/assets/images/feedback-space-created.png b/app/assets/images/feedback-space-created.png new file mode 100644 index 0000000000000000000000000000000000000000..c794da342a8ecdc7a57dc38f0e84bab322b53b7d GIT binary patch literal 3934 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 G7V{tKMH(mo literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/common/_header.scss b/app/assets/stylesheets/common/_header.scss index 6cb94a75..ccd36b1b 100644 --- a/app/assets/stylesheets/common/_header.scss +++ b/app/assets/stylesheets/common/_header.scss @@ -96,4 +96,9 @@ color: white !important; background-color: var(--primary-color); } + + .dropdown-header { + text-transform: uppercase; + color: var(--astuto-grey); + } } \ No newline at end of file diff --git a/app/assets/stylesheets/common/_index.scss b/app/assets/stylesheets/common/_index.scss index 01e26c25..b1f0fbd4 100644 --- a/app/assets/stylesheets/common/_index.scss +++ b/app/assets/stylesheets/common/_index.scss @@ -315,4 +315,12 @@ body { .alert-warning, .text-center, .m-0; +} + +.promoBanner { + @extend + .alert, + .alert-secondary, + .text-center, + .m-0; } \ No newline at end of file diff --git a/app/assets/stylesheets/components/Board.scss b/app/assets/stylesheets/components/Board.scss index 70f28188..aa6b3f71 100644 --- a/app/assets/stylesheets/components/Board.scss +++ b/app/assets/stylesheets/components/Board.scss @@ -33,6 +33,10 @@ } } } + + .sidebarFilters { + @extend .m-0, .p-0; + } .postStatusListItemContainer { @extend diff --git a/app/assets/stylesheets/components/TenantSignUp.scss b/app/assets/stylesheets/components/TenantSignUp.scss index f77644c0..1a6efd5a 100644 --- a/app/assets/stylesheets/components/TenantSignUp.scss +++ b/app/assets/stylesheets/components/TenantSignUp.scss @@ -37,7 +37,7 @@ .editUser { display: block; width: fit-content; - margin-top: 4px; + margin-top: 8px; margin-left: auto; margin-right: auto; } diff --git a/app/assets/stylesheets/constants/_colors.scss b/app/assets/stylesheets/constants/_colors.scss index f7f1602f..5b95bfad 100644 --- a/app/assets/stylesheets/constants/_colors.scss +++ b/app/assets/stylesheets/constants/_colors.scss @@ -1,7 +1,7 @@ :root { // Theme palette (supposed to be customized) --primary-color: rgb(51, 51, 51); - --background-color: rgb(255, 255, 255); + --background-color: rgb(251, 251, 251); // Theme palette shades (supposed to be computed from theme palette) --primary-color-light: color-mix(in srgb,var(--primary-color), #fff 85%); diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4ae46b93..09123f0e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,6 +9,15 @@ class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? prepend_before_action :load_tenant_data + # Override Devise after sign in path + def after_sign_in_path_for(resource) + if resource.admin? && resource.sign_in_count == 1 + root_path(tour: true) + else + super + end + end + protected def configure_permitted_parameters diff --git a/app/controllers/o_auths_controller.rb b/app/controllers/o_auths_controller.rb index f7d3e7fa..041b0d40 100644 --- a/app/controllers/o_auths_controller.rb +++ b/app/controllers/o_auths_controller.rb @@ -89,7 +89,7 @@ class OAuthsController < ApplicationController elsif reason == 'tenantsignup' - @o_auths = @o_auths = OAuth.unscoped.where(tenant_id: nil, is_enabled: true) + @o_auths = OAuth.unscoped.where(tenant_id: nil, is_enabled: true) @user_email = query_path_from_object(user_profile, @o_auth.json_user_email_path) if not @o_auth.json_user_name_path.blank? @@ -106,6 +106,7 @@ class OAuthsController < ApplicationController session[:o_auth_sign_up] = "#{@user_email},#{@user_name}" + @page_title = "Create your feedback space" render 'tenants/new' else diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 11e59088..e3738349 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -8,8 +8,9 @@ class RegistrationsController < Devise::RegistrationsController # Override destroy to soft delete def destroy resource.status = "deleted" - resource.email = '' + resource.email = "#{SecureRandom.alphanumeric(16)}@deleted.com" resource.full_name = t('defaults.deleted_user_full_name') + resource.skip_confirmation resource.save Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name) set_flash_message :notice, :destroyed diff --git a/app/javascript/components/Board/BoardP.tsx b/app/javascript/components/Board/BoardP.tsx index 86bfdf31..be3160ae 100644 --- a/app/javascript/components/Board/BoardP.tsx +++ b/app/javascript/components/Board/BoardP.tsx @@ -112,33 +112,35 @@ class BoardP extends React.Component { isLoggedIn={isLoggedIn} authenticityToken={authenticityToken} /> - - { - isPowerUser && - <> - handleSortByFilterChange(sortBy)} +
+ + { + isPowerUser && + <> + handleSortByFilterChange(sortBy)} + /> - + + } + - - } - +
{ tenantSetting.show_powered_by && } diff --git a/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx b/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx index b2856306..3f590d11 100644 --- a/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx +++ b/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx @@ -97,7 +97,7 @@ class BoardsEditable extends React.Component { confirm(I18n.t('common.confirmation')) && handleDelete(id)} + onClick={() => confirm(I18n.t('common.confirmation_board_delete', { board: name }) + " " + I18n.t('common.confirmation')) && handleDelete(id)} icon={} customClass="deleteAction" > diff --git a/app/javascript/components/TenantSignUp/ConfirmSignUpPage.tsx b/app/javascript/components/TenantSignUp/ConfirmEmailSignUpPage.tsx similarity index 89% rename from app/javascript/components/TenantSignUp/ConfirmSignUpPage.tsx rename to app/javascript/components/TenantSignUp/ConfirmEmailSignUpPage.tsx index 132686f0..9b0a1795 100644 --- a/app/javascript/components/TenantSignUp/ConfirmSignUpPage.tsx +++ b/app/javascript/components/TenantSignUp/ConfirmEmailSignUpPage.tsx @@ -7,7 +7,7 @@ interface Props { pendingTenantImage: string; } -const ConfirmSignUpPage = ({ +const ConfirmEmailSignUpPage = ({ subdomain, userEmail, pendingTenantImage, @@ -23,4 +23,4 @@ const ConfirmSignUpPage = ({ ); -export default ConfirmSignUpPage; \ No newline at end of file +export default ConfirmEmailSignUpPage; \ No newline at end of file diff --git a/app/javascript/components/TenantSignUp/ConfirmOAuthSignUpPage.tsx b/app/javascript/components/TenantSignUp/ConfirmOAuthSignUpPage.tsx new file mode 100644 index 00000000..eb8342c6 --- /dev/null +++ b/app/javascript/components/TenantSignUp/ConfirmOAuthSignUpPage.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import Box from '../common/Box'; + +interface Props { + baseUrl: string; + subdomain: string; + feedbackSpaceCreatedImage: string; +} + +const ConfirmOAuthSignUpPage = ({ + baseUrl, + subdomain, + feedbackSpaceCreatedImage, +}: Props) => { + let redirectUrl = new URL(baseUrl); + redirectUrl.hostname = `${subdomain}.${redirectUrl.hostname}`; + redirectUrl.pathname = '/users/sign_in'; + + return ( + +

You're all set!

+ + + +

+ You'll be redirected to your feedback space in a few seconds. +

+ +

+ If you are not redirected, please click here. +

+
+ ); +}; + +export default ConfirmOAuthSignUpPage; \ No newline at end of file diff --git a/app/javascript/components/TenantSignUp/TenantSignUpForm.tsx b/app/javascript/components/TenantSignUp/TenantSignUpForm.tsx index 37b9e757..b5cf638e 100644 --- a/app/javascript/components/TenantSignUp/TenantSignUpForm.tsx +++ b/app/javascript/components/TenantSignUp/TenantSignUpForm.tsx @@ -15,6 +15,7 @@ interface Props { handleSignUpSubmit(siteName: string, subdomain: string): void; trialPeriodDays: number; currentStep: number; + setCurrentStep(step: number): void; } const TenantSignUpForm = ({ @@ -23,6 +24,7 @@ const TenantSignUpForm = ({ handleSignUpSubmit, trialPeriodDays, currentStep, + setCurrentStep, }: Props) => { const { register, handleSubmit, formState: { errors } } = useForm(); const onSubmit: SubmitHandler = data => { diff --git a/app/javascript/components/TenantSignUp/TenantSignUpP.tsx b/app/javascript/components/TenantSignUp/TenantSignUpP.tsx index a2f19c26..8dd84087 100644 --- a/app/javascript/components/TenantSignUp/TenantSignUpP.tsx +++ b/app/javascript/components/TenantSignUp/TenantSignUpP.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { useState } from 'react'; -import HttpStatus from '../../constants/http_status'; -import ConfirmSignUpPage from './ConfirmSignUpPage'; import TenantSignUpForm from './TenantSignUpForm'; import UserSignUpForm from './UserSignUpForm'; +import ConfirmEmailSignUpPage from './ConfirmEmailSignUpPage'; +import ConfirmOAuthSignUpPage from './ConfirmOAuthSignUpPage'; import { IOAuth } from '../../interfaces/IOAuth'; +import HttpStatus from '../../constants/http_status'; interface Props { oAuthLoginCompleted: boolean; @@ -27,6 +28,7 @@ interface Props { ): Promise; astutoLogoImage: string; + feedbackSpaceCreatedImage: string; pendingTenantImage: string; baseUrl: string; @@ -58,6 +60,7 @@ const TenantSignUpP = ({ error, handleSubmit, astutoLogoImage, + feedbackSpaceCreatedImage, pendingTenantImage, baseUrl, trialPeriodDays, @@ -66,6 +69,9 @@ const TenantSignUpP = ({ // authMethod is either 'none', 'email' or 'oauth' const [authMethod, setAuthMethod] = useState(oAuthLoginCompleted ? 'oauth' : 'none'); + // goneBack is set to true if the user goes back from the tenant form to the user form + const [goneBack, setGoneBack] = useState(false); + const [userData, setUserData] = useState({ fullName: oAuthLoginCompleted ? oauthUserName : '', email: oAuthLoginCompleted ? oauthUserEmail : '', @@ -91,15 +97,22 @@ const TenantSignUpP = ({ authenticityToken, ).then(res => { if (res?.status !== HttpStatus.Created) return; + + setTenantData({ siteName, subdomain }); + setCurrentStep(currentStep + 1); + if (authMethod == 'oauth') { let redirectUrl = new URL(baseUrl); redirectUrl.hostname = `${subdomain}.${redirectUrl.hostname}`; - window.location.href = redirectUrl.toString(); + redirectUrl.pathname = '/users/sign_in'; + + // redirect after 3 seconds + setTimeout(() => { + window.location.href = redirectUrl.toString(); + }, 3000); + return; } - - setTenantData({ siteName, subdomain }); - setCurrentStep(currentStep + 1); }); } @@ -118,23 +131,34 @@ const TenantSignUpP = ({ oAuths={oAuths} userData={userData} setUserData={setUserData} + setGoneBack={setGoneBack} /> } { - (currentStep === 1 || currentStep === 2) && + (goneBack || currentStep === 2) && } { - currentStep === 3 && - + } + + { + currentStep === 3 && authMethod === 'email' && + ; userData: ITenantSignUpUserForm; setUserData({}: ITenantSignUpUserForm): void; + setGoneBack(goneBack: boolean): void; } const UserSignUpForm = ({ @@ -31,6 +32,7 @@ const UserSignUpForm = ({ oAuths, userData, setUserData, + setGoneBack, }: Props) => { const { register, @@ -164,7 +166,16 @@ const UserSignUpForm = ({ currentStep === 2 &&

{userData.fullName} ({userData.email}) - setCurrentStep(currentStep-1)} icon={} customClass="editUser">Edit + { + setGoneBack(true); + setCurrentStep(currentStep-1); + }} + icon={} + customClass="editUser" + > + Edit +

} diff --git a/app/javascript/components/TenantSignUp/index.tsx b/app/javascript/components/TenantSignUp/index.tsx index 4db57a4d..0d8ee6ce 100644 --- a/app/javascript/components/TenantSignUp/index.tsx +++ b/app/javascript/components/TenantSignUp/index.tsx @@ -14,6 +14,7 @@ interface Props { oauthUserName?: string; baseUrl: string; astutoLogoImage: string; + feedbackSpaceCreatedImage: string; pendingTenantImage: string; trialPeriodDays: number; authenticityToken: string; @@ -35,6 +36,7 @@ class TenantSignUpRoot extends React.Component { oauthUserEmail, oauthUserName, astutoLogoImage, + feedbackSpaceCreatedImage, pendingTenantImage, baseUrl, trialPeriodDays, @@ -49,6 +51,7 @@ class TenantSignUpRoot extends React.Component { oauthUserName={oauthUserName} oAuths={oAuths.map(oAuth => oAuthJSON2JS(oAuth))} astutoLogoImage={astutoLogoImage} + feedbackSpaceCreatedImage={feedbackSpaceCreatedImage} pendingTenantImage={pendingTenantImage} baseUrl={baseUrl} trialPeriodDays={trialPeriodDays} diff --git a/app/javascript/components/Tour/Tour.tsx b/app/javascript/components/Tour/Tour.tsx new file mode 100644 index 00000000..2ea6e111 --- /dev/null +++ b/app/javascript/components/Tour/Tour.tsx @@ -0,0 +1,142 @@ +import * as React from 'react'; + +import Joyride from 'react-joyride'; + +interface Props { + userFullName: string; +} + +const BOOTSTRAP_BREAKPOINT_SM = 768; + +const Tour = ({ userFullName }: Props) => { + const boardsToggler = document.querySelector('button.navbarToggler') as HTMLElement; + const profileToggler = document.getElementById('navbarDropdown'); + const userFirstName = userFullName ? userFullName.split(' ')[0].trim() : ''; + + const steps = [ + { + target: 'body', + placement: 'center', + title: (userFirstName ? `${userFirstName}, w` : 'W') + 'elcome to your new feedback space!', + content: 'Learn how to use and customize your feedback space with a 30-second tour.', + disableBeacon: true, + }, + { + target: '.boardsNav', + title: 'Boards', + content: 'From the top navigation bar, you can access your roadmap and boards.', + disableBeacon: true, + }, + { + target: '.postListItem', + title: 'Feedback', + content: 'Each board contains feedback posted by your customers.', + disableBeacon: true, + }, + { + target: '.sidebarFilters', + placement: 'right-start', + title: 'Filters', + content: 'On the left sidebar, filters help you sort out and make sense of all received feedback.', + disableBeacon: true, + }, + { + target: '.siteSettingsDropdown', + title: 'Site settings', + content: 'Click "Site settings" to customize your feedback space. You can add custom boards and statuses, manage users, personalize appearance, and more.', + disableBeacon: true, + }, + { + target: '.tourDropdown', + title: 'That\'s all!', + content: 'We hope Astuto will help you understand your customers and make better decisions! You can always replay this tour from here.', + disableBeacon: true, + }, + ]; + + const openBoardsNav = () => { + if (boardsToggler.getAttribute('aria-expanded') === 'false') { + boardsToggler.click(); + } + }; + + const closeBoardsNav = () => { + if (boardsToggler.getAttribute('aria-expanded') === 'true') { + boardsToggler.click(); + } + }; + + const openProfileNav = () => { + if (profileToggler.getAttribute('aria-expanded') === 'false') { + profileToggler.click(); + } + }; + + const closeProfileNav = () => { + if (profileToggler.getAttribute('aria-expanded') === 'true') { + profileToggler.click(); + } + } + + return ( + { + const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) + + // Open boards navbar (only on mobile) + if ( + vw < BOOTSTRAP_BREAKPOINT_SM && + state.type === 'step:after' && + (((state.action === 'next' || state.action === 'close') && state.step.target === 'body') || + (state.action === 'prev' && state.step.target === '.postListItem')) + ) { + openBoardsNav(); + } + + // Close boards navbar (only on mobile) + if ( + vw < BOOTSTRAP_BREAKPOINT_SM && + state.type === 'step:after' && + (//(state.action === 'next' && state.step.target === '.boardsNav') || // This causes positioniting problems for Joyride tour + (state.action === 'prev' && state.step.target === '.boardsNav')) + ) { + closeBoardsNav(); + } + + // Open profile navbar + if ( + state.type === 'step:after' && + (((state.action === 'next' || state.action === 'close') && (state.step.target === '.sidebarFilters' || state.step.target === '.siteSettingsDropdown')) || + (state.action === 'prev' && state.step.target === '.tourDropdown')) + ) { + if (vw < BOOTSTRAP_BREAKPOINT_SM) openBoardsNav(); + + openProfileNav(); + } + + // Close everything on reset + if (state.action === 'reset') { + closeBoardsNav(); + closeProfileNav(); + } + }} + continuous + showSkipButton + disableScrolling + hideCloseButton + spotlightClicks={false} + locale={{ + last: 'Finish', + }} + styles={{ + overlay: { height: '200%' }, + options: { + primaryColor: '#333333', + }, + }} + /> + ); +}; + +export default Tour; \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 8a0d05af..f9278da9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,7 +2,8 @@ class User < ApplicationRecord include TenantOwnable devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :confirmable + :recoverable, :rememberable, :confirmable, + :trackable validates_confirmation_of :password diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 5b06147e..b02a7c85 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -47,17 +47,18 @@