diff --git a/app/assets/images/feedback-space-created.png b/app/assets/images/feedback-space-created.png new file mode 100644 index 00000000..c794da34 Binary files /dev/null and b/app/assets/images/feedback-space-created.png differ 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 @@