From 6018de679dbd1bbbd1297e0a0bd82b6fef97c588 Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Wed, 17 Apr 2024 15:30:05 +0200 Subject: [PATCH 01/10] Add: open gt to all admins and change text --- .../admin/src/components/AuthenticatedApp.tsx | 4 +- .../src/components/GuidedTour/Homepage.tsx | 21 +- .../admin/src/components/GuidedTour/Modal.tsx | 22 +- .../src/components/GuidedTour/Provider.tsx | 60 +++- .../src/components/GuidedTour/constants.ts | 292 +++++++++++++++++- .../core/admin/admin/src/pages/HomePage.tsx | 11 +- .../admin/admin/src/pages/ProfilePage.tsx | 63 ++-- .../pages/Settings/pages/Users/ListPage.tsx | 12 +- .../pages/Users/components/NewUserForm.tsx | 10 +- .../core/admin/admin/src/translations/en.json | 38 ++- .../helper-plugin/src/features/GuidedTour.tsx | 32 +- .../UploadAssetDialog/UploadAssetDialog.jsx | 6 + .../src/pages/App/MediaLibrary/index.jsx | 9 + 13 files changed, 504 insertions(+), 76 deletions(-) diff --git a/packages/core/admin/admin/src/components/AuthenticatedApp.tsx b/packages/core/admin/admin/src/components/AuthenticatedApp.tsx index eab33fbd1c9..7b1a403e9fa 100644 --- a/packages/core/admin/admin/src/components/AuthenticatedApp.tsx +++ b/packages/core/admin/admin/src/components/AuthenticatedApp.tsx @@ -83,7 +83,9 @@ const AuthenticatedApp = () => { const isUserSuperAdmin = userRoles.find(({ code }) => code === 'strapi-super-admin'); if (isUserSuperAdmin && appInfo?.autoReload) { - setGuidedTourVisibility(true); + setGuidedTourVisibility('super-admin'); + } else if (!isUserSuperAdmin && appInfo?.autoReload) { + setGuidedTourVisibility('admin'); } } }, [userRoles, appInfo?.autoReload, setGuidedTourVisibility]); diff --git a/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx b/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx index 79e24db6620..23aadcc6479 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx @@ -5,15 +5,27 @@ import { ArrowRight } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { NavLink } from 'react-router-dom'; -import { LAYOUT_DATA, States, STATES } from './constants'; +import { SUPER_ADMIN_LAYOUT_DATA, LAYOUT_DATA, States, STATES } from './constants'; import { Number, VerticalDivider } from './Ornaments'; -const GuidedTourHomepage = () => { +interface GuidedTourHomepageProps { + visibility: string; +} + +const GuidedTourHomepage = ({ visibility }: GuidedTourHomepageProps) => { const { guidedTourState, setSkipped } = useGuidedTour(); const { formatMessage } = useIntl(); const { trackUsage } = useTracking(); - const sections = Object.entries(LAYOUT_DATA).map(([key, val]) => ({ + const layout = visibility === 'super-admin' ? SUPER_ADMIN_LAYOUT_DATA : LAYOUT_DATA; + const layoutCopy = JSON.parse(JSON.stringify(layout)); + + // Remove the inviteUser step if we are in the development env + if (process.env.NODE_ENV === 'development') { + delete layoutCopy.inviteUser; + } + + const sections = Object.entries(layoutCopy).map(([key, val]) => ({ key: key, title: val.home.title, content: ( @@ -51,9 +63,10 @@ const GuidedTourHomepage = () => { > + {Object.keys(layoutCopy).length} {formatMessage({ id: 'app.components.GuidedTour.title', - defaultMessage: '3 steps to get started', + defaultMessage: 'steps to get started', })} diff --git a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx index 6af417796ca..ec66650379f 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx @@ -17,7 +17,7 @@ import { MessageDescriptor, useIntl } from 'react-intl'; import { NavLink } from 'react-router-dom'; import styled from 'styled-components'; -import { LAYOUT_DATA, STATES } from './constants'; +import { LAYOUT_DATA, SUPER_ADMIN_LAYOUT_DATA, STATES } from './constants'; import { Number, VerticalDivider } from './Ornaments'; /* ------------------------------------------------------------------------------------------------- @@ -30,17 +30,25 @@ const GuidedTourModal = () => { guidedTourState, setCurrentStep, setStepState, - isGuidedTourVisible, + guidedTourVisibility, setSkipped, } = useGuidedTour(); const { formatMessage } = useIntl(); const { trackUsage } = useTracking(); - if (!currentStep || !isGuidedTourVisible) { + if (!currentStep || !guidedTourVisibility) { return null; } - const stepData = get(LAYOUT_DATA, currentStep); + const layout = guidedTourVisibility === 'super-admin' ? SUPER_ADMIN_LAYOUT_DATA : LAYOUT_DATA; + const layoutCopy = JSON.parse(JSON.stringify(layout)); + + // Remove the inviteUser step if we are in the development env + if (process.env.NODE_ENV === 'development') { + delete layoutCopy.inviteUser; + } + + const stepData = get(layoutCopy, currentStep); const sectionKeys = Object.keys(guidedTourState); const [sectionName, stepName] = currentStep.split('.') as [ keyof GuidedTourContextValue['guidedTourState'], @@ -107,6 +115,7 @@ const GuidedTourModal = () => { sectionIndex={sectionIndex} stepIndex={stepIndex} hasSectionAfter={hasSectionAfter} + stepCount={Object.keys(layoutCopy).length} > {stepData && 'content' in stepData && } @@ -150,6 +159,7 @@ interface GuidedTourStepperProps { onCtaClick: () => void; sectionIndex: number; stepIndex: number; + stepCount: number; hasSectionAfter: boolean; } @@ -160,6 +170,7 @@ const GuidedTourStepper = ({ onCtaClick, sectionIndex, stepIndex, + stepCount, hasSectionAfter, }: GuidedTourStepperProps) => { const { formatMessage } = useIntl(); @@ -175,9 +186,10 @@ const GuidedTourStepper = ({ {hasSectionBefore && } + {stepCount} {formatMessage({ id: 'app.components.GuidedTour.title', - defaultMessage: '3 steps to get started', + defaultMessage: 'steps to get started', })} diff --git a/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx b/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx index 7f81ce156ae..dfc2a122628 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx @@ -21,7 +21,7 @@ interface GuidedTourProviderProps { } const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { - const [{ currentStep, guidedTourState, isGuidedTourVisible, isSkipped }, dispatch] = + const [{ currentStep, guidedTourState, guidedTourVisibility, isSkipped }, dispatch] = React.useReducer(reducer, initialState, initialiseState); const setCurrentStep = (step: SetCurrentStepAction['step']) => { @@ -50,6 +50,30 @@ const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { }; const setGuidedTourVisibility = (value: SetGuidedTourVisibilityAction['value']) => { + // Update the initial guidedTourState depending on the visibility (executed once) + if (!guidedTourVisibility) { + const guidedTourState = JSON.parse(JSON.stringify(initialState?.guidedTourState)); + + // Remove the inviteUser step if we are in the development env + if (process.env.NODE_ENV === 'development') { + delete guidedTourState.inviteUser; + } + + // Remove non-related steps to specific guided tour (super-admin or admin) + if (value === 'admin') { + delete guidedTourState.contentTypeBuilder; + delete guidedTourState.apiTokens; + } else if (value === 'super-admin') { + delete guidedTourState.mediaLibrary; + delete guidedTourState.profile; + } + + dispatch({ + type: 'SET_GUIDED_TOUR_STATE', + value: guidedTourState, + }); + } + dispatch({ type: 'SET_GUIDED_TOUR_VISIBILITY', value, @@ -71,7 +95,6 @@ const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { if (sectionSteps) { const guidedTourArray = Object.entries(guidedTourState); - // Find current section position in the guidedTourArray // Get only previous sections based on current section position const currentSectionIndex = guidedTourArray.findIndex(([key]) => key === sectionName); @@ -81,7 +104,6 @@ const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { const isSectionToShow = previousSections.every(([, sectionValue]) => Object.values(sectionValue).every(Boolean) ); - const [firstStep] = Object.keys(sectionSteps) as [GuidedTourStepKey]; const isFirstStepDone = sectionSteps[firstStep]; @@ -109,7 +131,7 @@ const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { setSkipped={setSkipped} setStepState={setStepState} startSection={startSection} - isGuidedTourVisible={isGuidedTourVisible} + guidedTourVisibility={guidedTourVisibility} isSkipped={isSkipped} > {children} @@ -119,7 +141,7 @@ const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { type State = Pick< GuidedTourContextValue, - 'guidedTourState' | 'currentStep' | 'isGuidedTourVisible' | 'isSkipped' + 'guidedTourState' | 'currentStep' | 'guidedTourVisibility' | 'isSkipped' >; const initialState = { @@ -137,12 +159,20 @@ const initialState = { create: false, success: false, }, - transferTokens: { + mediaLibrary: { + create: false, + success: false, + }, + profile: { + create: false, + success: false, + }, + inviteUser: { create: false, success: false, }, }, - isGuidedTourVisible: false, + guidedTourVisibility: null, isSkipped: false, } satisfies State; @@ -157,6 +187,11 @@ interface SetStepStateAction { value: boolean; } +interface SetGuidedTourStateAction { + type: 'SET_GUIDED_TOUR_STATE'; + value: object; +} + interface SetSkippedAction { type: 'SET_SKIPPED'; value: boolean; @@ -164,14 +199,15 @@ interface SetSkippedAction { interface SetGuidedTourVisibilityAction { type: 'SET_GUIDED_TOUR_VISIBILITY'; - value: boolean; + value: string; } type Action = | SetCurrentStepAction | SetStepStateAction | SetSkippedAction - | SetGuidedTourVisibilityAction; + | SetGuidedTourVisibilityAction + | SetGuidedTourStateAction; const reducer: React.Reducer = (state: State = initialState, action: Action) => produce(state, (draftState) => { @@ -192,8 +228,12 @@ const reducer: React.Reducer = (state: State = initialState, acti draftState.isSkipped = action.value; break; } + case 'SET_GUIDED_TOUR_STATE': { + draftState.guidedTourState = action.value; + break; + } case 'SET_GUIDED_TOUR_VISIBILITY': { - draftState.isGuidedTourVisible = action.value; + draftState.guidedTourVisibility = action.value; break; } default: { diff --git a/packages/core/admin/admin/src/components/GuidedTour/constants.ts b/packages/core/admin/admin/src/components/GuidedTour/constants.ts index bcdaef92b53..e73be6f441b 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/constants.ts +++ b/packages/core/admin/admin/src/components/GuidedTour/constants.ts @@ -1,9 +1,225 @@ const LAYOUT_DATA = { + contentManager: { + home: { + title: { + id: 'app.components.GuidedTour.home.CM.title', + defaultMessage: '⚡️ Create a first entry', + }, + cta: { + title: { + id: 'app.components.GuidedTour.create-content', + defaultMessage: 'Create content', + }, + type: 'REDIRECT', + target: '/content-manager', + }, + trackingEvent: 'didClickGuidedTourHomepageContentManager', + }, + create: { + title: { + id: 'app.components.GuidedTour.CM.create.title', + defaultMessage: '⚡️ Create content', + }, + content: { + id: 'app.components.GuidedTour.CM.create.content', + defaultMessage: + "

Create and manage all the content here in the Content Manager.

Ex: Taking the Blog website example further, one can write an Article, save and publish it as they like.

💡 Quick tip - Don't forget to hit publish on the content you create.

", + }, + cta: { + title: { + id: 'app.components.GuidedTour.create-content', + defaultMessage: 'Create content', + }, + type: 'CLOSE', + }, + trackingEvent: 'didClickGuidedTourStep2ContentManager', + }, + success: { + title: { + id: 'app.components.GuidedTour.CM.success.title', + defaultMessage: 'Step 2: Completed ✅', + }, + content: { + id: 'app.components.GuidedTour.CM.success-admin.content', + defaultMessage: "

Good job! That was easy right? Let's browse the Media Library now.

🖼️ Upload a Media", + }, + cta: { + title: { + id: 'app.components.GuidedTour.CM.success-admin.cta.title', + defaultMessage: 'Go to the Media Library', + }, + type: 'REDIRECT', + target: '/plugins/upload', + }, + trackingEvent: 'didCreateGuidedTourEntry', + }, + }, + mediaLibrary: { + home: { + title: { + id: 'app.components.GuidedTour.home.mediaLibrary.title', + defaultMessage: '🖼️ Upload your first Media', + }, + cta: { + title: { + id: 'app.components.GuidedTour.home.mediaLibrary.cta.title', + defaultMessage: 'Go to the Media Library', + }, + type: 'REDIRECT', + target: '/plugins/upload', + }, + trackingEvent: 'didClickGuidedTourHomepageMediaLibrary', + }, + create: { + title: { + id: 'app.components.GuidedTour.mediaLibrary.create.title', + defaultMessage: '🖼️ Upload Media', + }, + content: { + id: 'app.components.GuidedTour.mediaLibrary.create.content', + defaultMessage: + '

Upload your first media in the Library. It can be an image, a video, an audio or a document.

', + }, + cta: { + title: { + id: 'app.components.GuidedTour.mediaLibrary.create.cta.title', + defaultMessage: 'Manage Media', + }, + type: 'CLOSE', + }, + trackingEvent: 'didClickGuidedTourStep2MediaLibrary', + }, + success: { + title: { + id: 'app.components.GuidedTour.mediaLibrary.success.title', + defaultMessage: 'Step 2: Completed ✅', + }, + content: { + id: 'app.components.GuidedTour.mediaLibrary.success.content', + defaultMessage: '

Awesome, you are a pro! What about customizing your experience?

', + }, + cta: { + title: { + id: 'app.components.GuidedTour.mediaLibrary.success.cta.title', + defaultMessage: 'Manage your profile', + }, + type: 'REDIRECT', + target: '/me', + }, + trackingEvent: 'didCreateGuidedTourMedia', + }, + }, + profile: { + home: { + title: { + id: 'app.components.GuidedTour.home.profile.title', + defaultMessage: '⚙️ Customize your experience', + }, + cta: { + title: { + id: 'app.components.GuidedTour.home.profile.cta.title', + defaultMessage: 'Manage your profile', + }, + type: 'REDIRECT', + target: '/me', + }, + trackingEvent: 'didClickGuidedTourHomepageProfile', + }, + create: { + title: { + id: 'app.components.GuidedTour.profile.create.title', + defaultMessage: '⚙️ Customize your experience', + }, + content: { + id: 'app.components.GuidedTour.profile.create.content', + defaultMessage: + '

Customize your profile by changing the interface mode #darkModeForTheWin.

', + }, + cta: { + title: { + id: 'app.components.GuidedTour.profile.create.cta.title', + defaultMessage: 'Manage your profile', + }, + type: 'CLOSE', + }, + trackingEvent: 'didClickGuidedTourStep3Profile', + }, + success: { + title: { + id: 'app.components.GuidedTour.profile.success.title', + defaultMessage: 'Step 3: Completed ✅', + }, + content: { + id: 'app.components.GuidedTour.profile.success.content', + defaultMessage: + '

🎉 Congratulations! You can now invite more users to collaborate with you if you have the permission to add them. Feel free to go back to Light mode, we will not judge your choices...

', + }, + cta: { + title: { + id: 'app.components.GuidedTour.profile.success.cta.title', + defaultMessage: 'Go back to the homepage', + }, + type: 'REDIRECT', + target: '/', + }, + trackingEvent: 'didCreateGuidedTourProfile', + }, + }, + inviteUser: { + home: { + title: { + id: 'app.components.GuidedTour.home.users.title', + defaultMessage: '✉️ Invite a User', + }, + cta: { + title: { + id: 'app.components.GuidedTour.home.users.create.cta.title', + defaultMessage: 'Manage admin users', + }, + type: 'REDIRECT', + target: '/settings/users', + }, + trackingEvent: 'didClickGuidedTourHomepageUsers', + }, + create: { + title: { + id: 'app.components.GuidedTour.users.create.title', + defaultMessage: '✉️ Invite a User', + }, + content: { + id: 'app.components.GuidedTour.users.create.content', + defaultMessage: + '

Create another User in the admin panel to help you manage your content.

', + }, + cta: { + title: { + id: 'app.components.GuidedTour.users.create.cta.title', + defaultMessage: 'Manage admin users', + }, + type: 'CLOSE', + }, + trackingEvent: 'didClickGuidedTourStep4Users', + }, + success: { + title: { + id: 'app.components.GuidedTour.users.success.title', + defaultMessage: 'Step 4: Completed ✅', + }, + content: { + id: 'app.components.GuidedTour.users.success.content', + defaultMessage: '

Congratulations! You finished the Guided Tour.

', + }, + trackingEvent: 'didGenerateGuidedTourUsers', + }, + }, +} as const; + +const SUPER_ADMIN_LAYOUT_DATA = { contentTypeBuilder: { home: { title: { - id: 'app.components.GuidedTour.home.CTB.title', - defaultMessage: '🧠 Build the content structure', + id: 'app.components.GuidedTour.CTB.create.title', + defaultMessage: '🧠 Create a first Collection type', }, cta: { title: { @@ -58,7 +274,7 @@ const LAYOUT_DATA = { home: { title: { id: 'app.components.GuidedTour.home.CM.title', - defaultMessage: '⚡️ What would you like to share with the world?', + defaultMessage: '⚡️ Create a first entry', }, cta: { title: { @@ -95,12 +311,12 @@ const LAYOUT_DATA = { defaultMessage: 'Step 2: Completed ✅', }, content: { - id: 'app.components.GuidedTour.CM.success.content', - defaultMessage: '

Awesome, one last step to go!

🚀 See content in action', + id: 'app.components.GuidedTour.CM.success-super-admin.content', + defaultMessage: '

Awesome, you are a pro!

🚀 See content in action', }, cta: { title: { - id: 'app.components.GuidedTour.CM.success.cta.title', + id: 'app.components.GuidedTour.CM.success-super-admin.cta.title', defaultMessage: 'Test the API', }, type: 'REDIRECT', @@ -112,8 +328,8 @@ const LAYOUT_DATA = { apiTokens: { home: { title: { - id: 'app.components.GuidedTour.apiTokens.create.title', - defaultMessage: '🚀 See content in action', + id: 'app.components.GuidedTour.home.apiTokens.title', + defaultMessage: '🚀 Fetch data from the API', }, cta: { title: { @@ -154,9 +370,64 @@ const LAYOUT_DATA = { defaultMessage: "

See content in action by making an HTTP request:

  • To this URL: https://'<'YOUR_DOMAIN'>'/api/'<'YOUR_CT'>'

  • With the header: Authorization: bearer '<'YOUR_API_TOKEN'>'

For more ways to interact with content, see the documentation.

", }, + cta: { + title: { + id: 'app.components.GuidedTour.users.create.cta.title', + defaultMessage: 'Go back to the homepage', + }, + type: 'REDIRECT', + target: '/', + }, trackingEvent: 'didGenerateGuidedTourApiTokens', }, }, + inviteUser: { + home: { + title: { + id: 'app.components.GuidedTour.home.users.title', + defaultMessage: '✉️ Invite a User', + }, + cta: { + title: { + id: 'app.components.GuidedTour.home.users.create.cta.title', + defaultMessage: 'Manage admin users', + }, + type: 'REDIRECT', + target: '/settings/users', + }, + trackingEvent: 'didClickGuidedTourHomepageUsers', + }, + create: { + title: { + id: 'app.components.GuidedTour.users.create.title', + defaultMessage: '✉️ Invite a User', + }, + content: { + id: 'app.components.GuidedTour.users.create.content', + defaultMessage: + '

Create another User in the admin panel to help you manage your content.

', + }, + cta: { + title: { + id: 'app.components.GuidedTour.users.create.cta.title', + defaultMessage: 'Manage admin users', + }, + type: 'CLOSE', + }, + trackingEvent: 'didClickGuidedTourStep4Users', + }, + success: { + title: { + id: 'app.components.GuidedTour.users.success.title', + defaultMessage: 'Step 4: Completed ✅', + }, + content: { + id: 'app.components.GuidedTour.users.success.content', + defaultMessage: '

Congratulations! You finished the Guided Tour.

', + }, + trackingEvent: 'didGenerateGuidedTourUsers', + }, + }, } as const; const STATES = { @@ -166,7 +437,8 @@ const STATES = { } as const; type LayoutData = typeof LAYOUT_DATA; +type SuperAdminLayoutData = typeof SUPER_ADMIN_LAYOUT_DATA; type States = keyof typeof STATES; -export { LAYOUT_DATA, STATES }; -export type { LayoutData, States }; +export { SUPER_ADMIN_LAYOUT_DATA, LAYOUT_DATA, STATES }; +export type { SuperAdminLayoutData, LayoutData, States }; diff --git a/packages/core/admin/admin/src/pages/HomePage.tsx b/packages/core/admin/admin/src/pages/HomePage.tsx index 6c2a532ab3f..34a65509c0b 100644 --- a/packages/core/admin/admin/src/pages/HomePage.tsx +++ b/packages/core/admin/admin/src/pages/HomePage.tsx @@ -44,13 +44,14 @@ const HomePageCE = () => { const { formatMessage } = useIntl(); // Temporary until we develop the menu API const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useContentTypes(); - const { guidedTourState, isGuidedTourVisible, isSkipped } = useGuidedTour(); + const { guidedTourState, guidedTourVisibility, isSkipped } = useGuidedTour(); const showGuidedTour = !Object.values(guidedTourState).every((section) => Object.values(section).every((step) => step) ) && - isGuidedTourVisible && + guidedTourVisibility && !isSkipped; + const { push } = useHistory(); const handleClick: React.MouseEventHandler = (e) => { e.preventDefault(); @@ -136,7 +137,11 @@ const HomePageCE = () => { - {showGuidedTour ? : } + {showGuidedTour ? ( + + ) : ( + + )} diff --git a/packages/core/admin/admin/src/pages/ProfilePage.tsx b/packages/core/admin/admin/src/pages/ProfilePage.tsx index 934c54b8873..11d9031bcd7 100644 --- a/packages/core/admin/admin/src/pages/ProfilePage.tsx +++ b/packages/core/admin/admin/src/pages/ProfilePage.tsx @@ -28,6 +28,7 @@ import { useOverlayBlocker, useTracking, useAPIErrorHandler, + useGuidedTour, } from '@strapi/helper-plugin'; import { Check, Eye, EyeStriked } from '@strapi/icons'; import { Formik, FormikHelpers } from 'formik'; @@ -74,6 +75,8 @@ const ProfilePage = () => { const { notifyStatus } = useNotifyAT(); const currentTheme = useTypedSelector((state) => state.admin_app.theme.currentTheme); const dispatch = useTypedDispatch(); + const { startSection, setCurrentStep } = useGuidedTour(); + const { _unstableFormatValidationErrors: formatValidationErrors, _unstableFormatAPIError: formatApiError, @@ -83,6 +86,10 @@ const ProfilePage = () => { const user = useAuth('ProfilePage', (state) => state.user); + React.useEffect(() => { + startSection('profile'); + }, [startSection]); + React.useEffect(() => { if (user) { notifyStatus( @@ -151,6 +158,8 @@ const ProfilePage = () => { trackUsage('didChangeMode', { newMode: currentTheme }); + setCurrentStep('profile.success'); + toggleNotification({ type: 'success', message: { id: 'notification.success.saved', defaultMessage: 'Saved' }, @@ -342,9 +351,9 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => error={ errors.currentPassword ? formatMessage({ - id: errors.currentPassword, - defaultMessage: errors.currentPassword, - }) + id: errors.currentPassword, + defaultMessage: errors.currentPassword, + }) : '' } onChange={onChange} @@ -364,13 +373,13 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => label={formatMessage( currentPasswordShown ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } + id: 'Auth.form.password.show-password', + defaultMessage: 'Show password', + } : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } + id: 'Auth.form.password.hide-password', + defaultMessage: 'Hide password', + } )} > {currentPasswordShown ? : } @@ -385,9 +394,9 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => error={ errors.password ? formatMessage({ - id: errors.password, - defaultMessage: errors.password, - }) + id: errors.password, + defaultMessage: errors.password, + }) : '' } onChange={onChange} @@ -408,13 +417,13 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => label={formatMessage( passwordShown ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } + id: 'Auth.form.password.show-password', + defaultMessage: 'Show password', + } : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } + id: 'Auth.form.password.hide-password', + defaultMessage: 'Hide password', + } )} > {passwordShown ? : } @@ -427,9 +436,9 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => error={ errors.confirmPassword ? formatMessage({ - id: errors.confirmPassword, - defaultMessage: errors.confirmPassword, - }) + id: errors.confirmPassword, + defaultMessage: errors.confirmPassword, + }) : '' } onChange={onChange} @@ -450,13 +459,13 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => label={formatMessage( passwordConfirmShown ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } + id: 'Auth.form.password.show-password', + defaultMessage: 'Show password', + } : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } + id: 'Auth.form.password.hide-password', + defaultMessage: 'Hide password', + } )} > {passwordConfirmShown ? : } diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Users/ListPage.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Users/ListPage.tsx index 6264a315d3d..0a9b4d4ffd8 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Users/ListPage.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Users/ListPage.tsx @@ -23,6 +23,7 @@ import { PaginationURLQuery, CheckPagePermissions, TableHeader, + useGuidedTour, } from '@strapi/helper-plugin'; import * as qs from 'qs'; import { IntlShape, MessageDescriptor, useIntl } from 'react-intl'; @@ -51,6 +52,8 @@ const ListPageCE = () => { } = useRBAC(permissions.settings?.users); const toggleNotification = useNotification(); const { formatMessage } = useIntl(); + const { startSection, setCurrentStep } = useGuidedTour(); + const { search } = useLocation(); useFocusWhenNavigate(); const { data, isError, isLoading } = useAdminUsers( @@ -60,6 +63,10 @@ const ListPageCE = () => { } ); + React.useEffect(() => { + startSection('inviteUser'); + }, [startSection]); + const { pagination, users } = data ?? {}; const CreateAction = useEnterprise( @@ -85,7 +92,10 @@ const ListPageCE = () => { defaultMessage: 'Users', }); - const handleToggle = () => { + const handleToggle = (step: string) => { + if (step === 'magic-link') { + setCurrentStep('inviteUser.success'); + } setIsModalOpen((prev) => !prev); }; diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx index d26b11f1326..03bfc578f8a 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx @@ -20,6 +20,7 @@ import { useOverlayBlocker, translatedErrors, useAPIErrorHandler, + useGuidedTour, } from '@strapi/helper-plugin'; import { Entity } from '@strapi/types'; import { Formik, FormikHelpers } from 'formik'; @@ -35,13 +36,14 @@ import { MagicLinkCE } from './MagicLinkCE'; import { SelectRoles } from './SelectRoles'; interface ModalFormProps { - onToggle: () => void; + onToggle: (type: string) => void; // Now expects an optional string argument } const ModalForm = ({ onToggle }: ModalFormProps) => { const [currentStep, setStep] = React.useState('create'); const [registrationToken, setRegistrationToken] = React.useState(''); const { formatMessage } = useIntl(); + const { setCurrentStep } = useGuidedTour(); const toggleNotification = useNotification(); const { lockApp, unlockApp } = useOverlayBlocker(); const { @@ -255,7 +257,11 @@ const ModalForm = ({ onToggle }: ModalFormProps) => { {formatMessage(buttonSubmitLabel)} ) : ( - ) diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index ae6dee1dd68..56be6503c20 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -414,8 +414,10 @@ "app.components.EmptyStateLayout.content-permissions": "You don't have the permissions to access that content", "app.components.GuidedTour.CM.create.content": "

Create and manage all the content here in the Content Manager.

Ex: Taking the Blog website example further, one can write an Article, save and publish it as they like.

💡 Quick tip — Don't forget to hit publish on the content you create.

", "app.components.GuidedTour.CM.create.title": "⚡️ Create content", - "app.components.GuidedTour.CM.success.content": "

Awesome, one last step to go!

🚀 See content in action", - "app.components.GuidedTour.CM.success.cta.title": "Test the API", + "app.components.GuidedTour.CM.success-super-admin.content": "

Awesome, one last step to go!

🚀 See content in action", + "app.components.GuidedTour.CM.success-admin.content": "

Good job! That was easy right? Let's browse the Media Library now.

🖼️ Upload a Media", + "app.components.GuidedTour.CM.success-super-admin.cta.title": "Test the API", + "app.components.GuidedTour.CM.success-admin.cta.title": "Go to the Media Library", "app.components.GuidedTour.CM.success.title": "Step 2: Completed ✅", "app.components.GuidedTour.CTB.create.content": "

Collection Types help you manage several entries, Single Types are suitable to manage only one entry.

Ex: For a Blog website, Articles would be a Collection Type whereas a Homepage would be a Single Type.

", "app.components.GuidedTour.CTB.create.cta.title": "Build a Collection Type", @@ -428,13 +430,39 @@ "app.components.GuidedTour.apiTokens.success.content": "

See content in action by making an HTTP request:

  • To this URL: https://'<'YOUR_DOMAIN'>'/api/'<'YOUR_CT'>'

  • With the header: Authorization: bearer '<'YOUR_API_TOKEN'>'

For more ways to interact with content, see the documentation.

", "app.components.GuidedTour.apiTokens.success.cta.title": "Go back to homepage", "app.components.GuidedTour.apiTokens.success.title": "Step 3: Completed ✅", + "app.components.GuidedTour.users.create.content": "

Create another User in the admin panel to help you manage your content.

", + "app.components.GuidedTour.users.create.cta.title": "Manage admin users", + "app.components.GuidedTour.users.create.title": "✉️ Invite a User", + "app.components.GuidedTour.users.success.content": "

Congratulations! You finished the Guided Tour.

", + "app.components.GuidedTour.users.success.cta.title": "Go back to homepage", + "app.components.GuidedTour.users.success.title": "Step 4: Completed ✅", + "app.components.GuidedTour.mediaLibrary.create.title": "🖼️ Upload Media", + "app.components.GuidedTour.mediaLibrary.create.content": "

Upload your first media in the Library. It can be an image, a video, an audio or a document.

", + "app.components.GuidedTour.mediaLibrary.create.cta.title": "Manage Media", + "app.components.GuidedTour.mediaLibrary.success.content": "

Awesome you are a pro! What about customizing your experience?

", + "app.components.GuidedTour.mediaLibrary.success.cta.title": "Manage your profile", + "app.components.GuidedTour.mediaLibrary.success.title": "Step 2: Completed ✅", + + "app.components.GuidedTour.profile.create.title": "⚙️ Customize your experience", + "app.components.GuidedTour.profile.create.content": "

Customize your profile by changing the interface mode #darkModeForTheWin.

", + "app.components.GuidedTour.profile.create.cta.title": "Manage your profile", + "app.components.GuidedTour.profile.success.content": "

🎉 Congratulations! You can now invite more users to collaborate with you if you have the permission to add them. Feel free to go back to Light mode, we will not judge your choices...

", + "app.components.GuidedTour.profile.success.cta.title": "Go back to the homepage", + "app.components.GuidedTour.profile.success.title": "Step 3: Completed ✅", + "app.components.GuidedTour.create-content": "Create content", - "app.components.GuidedTour.home.CM.title": "⚡️ What would you like to share with the world?", + "app.components.GuidedTour.home.CM.title": "⚡️ Create a first Entry", "app.components.GuidedTour.home.CTB.cta.title": "Go to the Content type Builder", "app.components.GuidedTour.home.CTB.title": "🧠 Build the content structure", "app.components.GuidedTour.home.apiTokens.cta.title": "Test the API", + "app.components.GuidedTour.home.apiTokens.title": "🚀 Fetch data from the API", + "app.components.GuidedTour.home.users.create.cta.title": "Manage admin users", + "app.components.GuidedTour.home.mediaLibrary.title": "🖼️ Upload your first Media", + "app.components.GuidedTour.home.mediaLibrary.cta.title": "Go to the Media Library", + "app.components.GuidedTour.home.profile.title": "⚙️ Customize your experience", + "app.components.GuidedTour.home.profile.cta.title": "Manage your profile", "app.components.GuidedTour.skip": "Skip the tour", - "app.components.GuidedTour.title": "3 steps to get started", + "app.components.GuidedTour.title": " steps to get started", "app.components.HomePage.button.blog": "See more on the blog", "app.components.HomePage.community": "Join the community", "app.components.HomePage.community.content": "Discuss with team members, contributors and developers on different channels.", @@ -994,4 +1022,4 @@ "selectButtonTitle": "Select", "skipToContent": "Skip to content", "submit": "Submit" -} +} \ No newline at end of file diff --git a/packages/core/helper-plugin/src/features/GuidedTour.tsx b/packages/core/helper-plugin/src/features/GuidedTour.tsx index 4590719d3db..78c126e8536 100644 --- a/packages/core/helper-plugin/src/features/GuidedTour.tsx +++ b/packages/core/helper-plugin/src/features/GuidedTour.tsx @@ -27,15 +27,23 @@ interface GuidedTourContextValue { create: boolean; success: boolean; }; - transferTokens: { + mediaLibrary: { + create: boolean; + success: boolean; + }; + profile: { + create: boolean; + success: boolean; + }; + inviteUser: { create: boolean; success: boolean; }; }; - isGuidedTourVisible: boolean; + guidedTourVisibility: string | null; isSkipped: boolean; setCurrentStep: (step: Step | null) => void | null; - setGuidedTourVisibility: (isVisible: boolean) => void; + setGuidedTourVisibility: (isVisible: string) => void; setSkipped: (isSkipped: boolean) => void; setStepState: (step: Step, state: boolean) => void; startSection: (section: SectionKey) => void; @@ -56,12 +64,20 @@ const GuidedTourContext = React.createContext({ create: false, success: false, }, - transferTokens: { + mediaLibrary: { + create: false, + success: false, + }, + profile: { + create: false, + success: false, + }, + inviteUser: { create: false, success: false, }, }, - isGuidedTourVisible: false, + guidedTourVisibility: null, isSkipped: true, setCurrentStep: () => null, setGuidedTourVisibility: () => null, @@ -82,7 +98,7 @@ const GuidedTourProvider = ({ children, currentStep = null, guidedTourState, - isGuidedTourVisible = false, + guidedTourVisibility = null, isSkipped, setCurrentStep, setGuidedTourVisibility, @@ -94,7 +110,7 @@ const GuidedTourProvider = ({ () => ({ currentStep, guidedTourState, - isGuidedTourVisible, + guidedTourVisibility, isSkipped, setCurrentStep, setGuidedTourVisibility, @@ -105,7 +121,7 @@ const GuidedTourProvider = ({ [ currentStep, guidedTourState, - isGuidedTourVisible, + guidedTourVisibility, isSkipped, setCurrentStep, setGuidedTourVisibility, diff --git a/packages/core/upload/admin/src/components/UploadAssetDialog/UploadAssetDialog.jsx b/packages/core/upload/admin/src/components/UploadAssetDialog/UploadAssetDialog.jsx index f91f11d499b..4c77a3d5349 100644 --- a/packages/core/upload/admin/src/components/UploadAssetDialog/UploadAssetDialog.jsx +++ b/packages/core/upload/admin/src/components/UploadAssetDialog/UploadAssetDialog.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { ModalLayout } from '@strapi/design-system'; +import { useGuidedTour } from '@strapi/helper-plugin'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; @@ -28,6 +29,9 @@ export const UploadAssetDialog = ({ const [assets, setAssets] = useState(initialAssetsToAdd || []); const [assetToEdit, setAssetToEdit] = useState(undefined); + const { setCurrentStep } = useGuidedTour(); + + const handleAddToPendingAssets = (nextAssets) => { validateAssetsTypes(nextAssets, () => { setAssets((prevAssets) => prevAssets.concat(nextAssets)); @@ -56,6 +60,8 @@ export const UploadAssetDialog = ({ if (nextAssets.length === 0) { onClose(); } + + setCurrentStep("mediaLibrary.success") }; const handleAssetEditValidation = (nextAsset) => { diff --git a/packages/core/upload/admin/src/pages/App/MediaLibrary/index.jsx b/packages/core/upload/admin/src/pages/App/MediaLibrary/index.jsx index 37a23fcce65..702ca052051 100644 --- a/packages/core/upload/admin/src/pages/App/MediaLibrary/index.jsx +++ b/packages/core/upload/admin/src/pages/App/MediaLibrary/index.jsx @@ -24,6 +24,7 @@ import { useQueryParams, useSelectionState, useTracking, + useGuidedTour } from '@strapi/helper-plugin'; import { Cog, Grid, List, Pencil } from '@strapi/icons'; import { stringify } from 'qs'; @@ -93,6 +94,7 @@ export const MediaLibrary = () => { const isFiltering = Boolean(query._q || query.filters); const [view, setView] = usePersistentState(localStorageKeys.view, viewOptions.GRID); const isGridView = view === viewOptions.GRID; + const { startSection } = useGuidedTour(); const { data: assetsData, @@ -120,6 +122,10 @@ export const MediaLibrary = () => { enabled: canRead && !!query?.folder, }); + React.useEffect(() => { + startSection('mediaLibrary'); + }, [startSection]); + // Folder was not found: redirect to the media library root if (currentFolderError?.response?.status === 404) { push(pathname); @@ -162,6 +168,8 @@ export const MediaLibrary = () => { setShowEditFolderDialog((prev) => !prev); }; + + const handleBulkSelect = (event, elements) => { if (event.target.checked) { trackUsage('didSelectAllMediaLibraryElements'); @@ -213,6 +221,7 @@ export const MediaLibrary = () => { useFocusWhenNavigate(); + return (
From 9976a647575fcf3671c6f6f2d748c25a71b7c4f3 Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Wed, 17 Apr 2024 16:37:12 +0200 Subject: [PATCH 02/10] Add: add properties to Guided Tour events --- .../src/components/GuidedTour/Homepage.tsx | 8 ++-- .../admin/src/components/GuidedTour/Modal.tsx | 8 ++-- .../src/components/GuidedTour/constants.ts | 4 +- .../helper-plugin/src/features/Tracking.tsx | 42 +++++++++++++------ 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx b/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx index 23aadcc6479..573e300e6c3 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx @@ -13,13 +13,15 @@ interface GuidedTourHomepageProps { } const GuidedTourHomepage = ({ visibility }: GuidedTourHomepageProps) => { - const { guidedTourState, setSkipped } = useGuidedTour(); + const { guidedTourState, setSkipped, guidedTourVisibility } = useGuidedTour(); const { formatMessage } = useIntl(); const { trackUsage } = useTracking(); const layout = visibility === 'super-admin' ? SUPER_ADMIN_LAYOUT_DATA : LAYOUT_DATA; const layoutCopy = JSON.parse(JSON.stringify(layout)); + const triggeredBySA = guidedTourVisibility === 'super-admin' ? true : false; + // Remove the inviteUser step if we are in the development env if (process.env.NODE_ENV === 'development') { delete layoutCopy.inviteUser; @@ -30,7 +32,7 @@ const GuidedTourHomepage = ({ visibility }: GuidedTourHomepageProps) => { title: val.home.title, content: ( trackUsage(val.home.trackingEvent)} + onClick={() => trackUsage(val.home.trackingEvent, { triggeredBySA })} as={NavLink} // @ts-expect-error - types are not inferred correctly through the as prop. to={val.home.cta.target} @@ -48,7 +50,7 @@ const GuidedTourHomepage = ({ visibility }: GuidedTourHomepageProps) => { const handleSkip = () => { setSkipped(true); - trackUsage('didSkipGuidedtour'); + trackUsage('didSkipGuidedtour', { triggeredBySA }); }; return ( diff --git a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx index ec66650379f..5ec420cb1cb 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx @@ -43,6 +43,8 @@ const GuidedTourModal = () => { const layout = guidedTourVisibility === 'super-admin' ? SUPER_ADMIN_LAYOUT_DATA : LAYOUT_DATA; const layoutCopy = JSON.parse(JSON.stringify(layout)); + const triggeredBySA = guidedTourVisibility === 'super-admin' ? true : false; + // Remove the inviteUser step if we are in the development env if (process.env.NODE_ENV === 'development') { delete layoutCopy.inviteUser; @@ -63,7 +65,7 @@ const GuidedTourModal = () => { setStepState(currentStep, true); if (stepData) { - trackUsage(stepData.trackingEvent); + trackUsage(stepData.trackingEvent, { triggeredBySA }); } setCurrentStep(null); @@ -72,7 +74,7 @@ const GuidedTourModal = () => { const handleSkip = () => { setSkipped(true); setCurrentStep(null); - trackUsage('didSkipGuidedtour'); + trackUsage('didSkipGuidedtour', { triggeredBySA }); }; return ( @@ -258,7 +260,7 @@ const GuidedTourStepper = ({ * -----------------------------------------------------------------------------------------------*/ interface GuidedTourContentProps - extends Required> {} + extends Required> { } const GuidedTourContent = ({ id, defaultMessage }: GuidedTourContentProps) => { const { formatMessage } = useIntl(); diff --git a/packages/core/admin/admin/src/components/GuidedTour/constants.ts b/packages/core/admin/admin/src/components/GuidedTour/constants.ts index e73be6f441b..be1eaf0b2cf 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/constants.ts +++ b/packages/core/admin/admin/src/components/GuidedTour/constants.ts @@ -32,7 +32,7 @@ const LAYOUT_DATA = { }, type: 'CLOSE', }, - trackingEvent: 'didClickGuidedTourStep2ContentManager', + trackingEvent: 'didClickGuidedTourStep1ContentManager', }, success: { title: { @@ -162,7 +162,7 @@ const LAYOUT_DATA = { type: 'REDIRECT', target: '/', }, - trackingEvent: 'didCreateGuidedTourProfile', + trackingEvent: 'didUpdateGuidedTourProfile', }, }, inviteUser: { diff --git a/packages/core/helper-plugin/src/features/Tracking.tsx b/packages/core/helper-plugin/src/features/Tracking.tsx index 5dac250da8b..9d503c33f62 100644 --- a/packages/core/helper-plugin/src/features/Tracking.tsx +++ b/packages/core/helper-plugin/src/features/Tracking.tsx @@ -61,22 +61,15 @@ interface EventWithoutProperties { | 'didBulkDeleteEntries' | 'didChangeDisplayedFields' | 'didCheckDraftRelations' - | 'didClickGuidedTourHomepageApiTokens' - | 'didClickGuidedTourHomepageContentManager' - | 'didClickGuidedTourHomepageContentTypeBuilder' - | 'didClickGuidedTourStep1CollectionType' - | 'didClickGuidedTourStep2ContentManager' - | 'didClickGuidedTourStep3ApiTokens' | 'didClickonBlogSection' | 'didClickonCodeExampleSection' | 'didClickonReadTheDocumentationSection' | 'didClickOnTryStrapiCloudSection' | 'didClickonTutorialSection' - | 'didCreateGuidedTourCollectionType' - | 'didCreateGuidedTourEntry' | 'didCreateNewRole' | 'didCreateRole' | 'didDeleteToken' + | 'didLaunchGuidedtour' | 'didDuplicateRole' | 'didEditEditSettings' | 'didEditEmailTemplates' @@ -84,9 +77,7 @@ interface EventWithoutProperties { | 'didEditListSettings' | 'didEditMediaLibraryConfig' | 'didEditNameOfContentType' - | 'didGenerateGuidedTourApiTokens' | 'didGoToMarketplace' - | 'didLaunchGuidedtour' | 'didMissMarketplacePlugin' | 'didNotCreateFirstAdmin' | 'didNotSaveComponent' @@ -96,7 +87,6 @@ interface EventWithoutProperties { | 'didSaveComponent' | 'didSaveContentType' | 'didSearch' - | 'didSkipGuidedtour' | 'didSubmitPlugin' | 'didSubmitProvider' | 'didUpdateConditions' @@ -299,6 +289,33 @@ interface DidPublishRelease { }; } +interface GuidedTourEvents { + name: + | 'didCreateGuidedTourCollectionType' + | 'didCreateGuidedTourEntry' + | 'didGenerateGuidedTourApiTokens' + | 'didCreateGuidedTourMedia' + | 'didUpdateGuidedTourProfile' + | 'didGenerateGuidedTourUsers' + | 'didClickGuidedTourHomepageContentTypeBuilder' + | 'didClickGuidedTourHomepageContentManager' + | 'didClickGuidedTourHomepageApiTokens' + | 'didClickGuidedTourHomepageMediaLibrary' + | 'didClickGuidedTourHomepageProfile' + | 'didClickGuidedTourHomepageUsers' + | 'didClickGuidedTourStep1CollectionType' + | 'didClickGuidedTourStep2ContentManager' + | 'didClickGuidedTourStep3ApiTokens' + | 'didClickGuidedTourStep1ContentManager' + | 'didClickGuidedTourStep2MediaLibrary' + | 'didClickGuidedTourStep3Profile' + | 'didClickGuidedTourStep4Users' + | 'didSkipGuidedtour'; + properties: { + triggeredBySA?: boolean; + }; +} + type EventsWithProperties = | CreateEntryEvents | DidAccessTokenListEvent @@ -317,7 +334,8 @@ type EventsWithProperties = | UpdateEntryEvents | WillModifyTokenEvent | WillNavigateEvent - | DidPublishRelease; + | DidPublishRelease + | GuidedTourEvents; export type TrackingEvent = EventWithoutProperties | EventsWithProperties; export interface UseTrackingReturn { From 081fb591fbe8f3704a4752d79cbd1c396a743208 Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Wed, 17 Apr 2024 16:55:48 +0200 Subject: [PATCH 03/10] Fix: prettier --- .../admin/src/components/GuidedTour/Modal.tsx | 2 +- .../src/components/GuidedTour/constants.ts | 3 +- .../admin/admin/src/pages/ProfilePage.tsx | 54 +++++++++---------- .../core/admin/admin/src/translations/en.json | 2 - 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx index 5ec420cb1cb..c8853acec1c 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx @@ -260,7 +260,7 @@ const GuidedTourStepper = ({ * -----------------------------------------------------------------------------------------------*/ interface GuidedTourContentProps - extends Required> { } + extends Required> {} const GuidedTourContent = ({ id, defaultMessage }: GuidedTourContentProps) => { const { formatMessage } = useIntl(); diff --git a/packages/core/admin/admin/src/components/GuidedTour/constants.ts b/packages/core/admin/admin/src/components/GuidedTour/constants.ts index be1eaf0b2cf..46047298858 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/constants.ts +++ b/packages/core/admin/admin/src/components/GuidedTour/constants.ts @@ -41,7 +41,8 @@ const LAYOUT_DATA = { }, content: { id: 'app.components.GuidedTour.CM.success-admin.content', - defaultMessage: "

Good job! That was easy right? Let's browse the Media Library now.

🖼️ Upload a Media", + defaultMessage: + "

Good job! That was easy right? Let's browse the Media Library now.

🖼️ Upload a Media", }, cta: { title: { diff --git a/packages/core/admin/admin/src/pages/ProfilePage.tsx b/packages/core/admin/admin/src/pages/ProfilePage.tsx index 11d9031bcd7..af872eff116 100644 --- a/packages/core/admin/admin/src/pages/ProfilePage.tsx +++ b/packages/core/admin/admin/src/pages/ProfilePage.tsx @@ -351,9 +351,9 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => error={ errors.currentPassword ? formatMessage({ - id: errors.currentPassword, - defaultMessage: errors.currentPassword, - }) + id: errors.currentPassword, + defaultMessage: errors.currentPassword, + }) : '' } onChange={onChange} @@ -373,13 +373,13 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => label={formatMessage( currentPasswordShown ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } + id: 'Auth.form.password.show-password', + defaultMessage: 'Show password', + } : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } + id: 'Auth.form.password.hide-password', + defaultMessage: 'Hide password', + } )} > {currentPasswordShown ? : } @@ -394,9 +394,9 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => error={ errors.password ? formatMessage({ - id: errors.password, - defaultMessage: errors.password, - }) + id: errors.password, + defaultMessage: errors.password, + }) : '' } onChange={onChange} @@ -417,13 +417,13 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => label={formatMessage( passwordShown ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } + id: 'Auth.form.password.show-password', + defaultMessage: 'Show password', + } : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } + id: 'Auth.form.password.hide-password', + defaultMessage: 'Hide password', + } )} > {passwordShown ? : } @@ -436,9 +436,9 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => error={ errors.confirmPassword ? formatMessage({ - id: errors.confirmPassword, - defaultMessage: errors.confirmPassword, - }) + id: errors.confirmPassword, + defaultMessage: errors.confirmPassword, + }) : '' } onChange={onChange} @@ -459,13 +459,13 @@ const PasswordSection = ({ errors, onChange, values }: PasswordSectionProps) => label={formatMessage( passwordConfirmShown ? { - id: 'Auth.form.password.show-password', - defaultMessage: 'Show password', - } + id: 'Auth.form.password.show-password', + defaultMessage: 'Show password', + } : { - id: 'Auth.form.password.hide-password', - defaultMessage: 'Hide password', - } + id: 'Auth.form.password.hide-password', + defaultMessage: 'Hide password', + } )} > {passwordConfirmShown ? : } diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 56be6503c20..15e445c76d5 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -442,14 +442,12 @@ "app.components.GuidedTour.mediaLibrary.success.content": "

Awesome you are a pro! What about customizing your experience?

", "app.components.GuidedTour.mediaLibrary.success.cta.title": "Manage your profile", "app.components.GuidedTour.mediaLibrary.success.title": "Step 2: Completed ✅", - "app.components.GuidedTour.profile.create.title": "⚙️ Customize your experience", "app.components.GuidedTour.profile.create.content": "

Customize your profile by changing the interface mode #darkModeForTheWin.

", "app.components.GuidedTour.profile.create.cta.title": "Manage your profile", "app.components.GuidedTour.profile.success.content": "

🎉 Congratulations! You can now invite more users to collaborate with you if you have the permission to add them. Feel free to go back to Light mode, we will not judge your choices...

", "app.components.GuidedTour.profile.success.cta.title": "Go back to the homepage", "app.components.GuidedTour.profile.success.title": "Step 3: Completed ✅", - "app.components.GuidedTour.create-content": "Create content", "app.components.GuidedTour.home.CM.title": "⚡️ Create a first Entry", "app.components.GuidedTour.home.CTB.cta.title": "Go to the Content type Builder", From 510bc653d51dd558d99b3d9e225f90d8ea104658 Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Tue, 23 Apr 2024 17:52:35 +0200 Subject: [PATCH 04/10] Update: first review --- .../admin/src/components/AuthenticatedApp.tsx | 6 +-- .../src/components/GuidedTour/Homepage.tsx | 49 ++++++++++++------- .../admin/src/components/GuidedTour/Modal.tsx | 46 +++++++++-------- .../src/components/GuidedTour/Provider.tsx | 37 +++++++++----- .../src/components/GuidedTour/constants.ts | 20 ++++++-- .../core/admin/admin/src/pages/HomePage.tsx | 10 ++-- .../pages/Users/components/NewUserForm.tsx | 2 +- .../core/admin/admin/src/translations/en.json | 6 +-- .../helper-plugin/src/features/GuidedTour.tsx | 25 +++++++--- 9 files changed, 128 insertions(+), 73 deletions(-) diff --git a/packages/core/admin/admin/src/components/AuthenticatedApp.tsx b/packages/core/admin/admin/src/components/AuthenticatedApp.tsx index 7b1a403e9fa..ced5212d29c 100644 --- a/packages/core/admin/admin/src/components/AuthenticatedApp.tsx +++ b/packages/core/admin/admin/src/components/AuthenticatedApp.tsx @@ -82,10 +82,8 @@ const AuthenticatedApp = () => { if (userRoles) { const isUserSuperAdmin = userRoles.find(({ code }) => code === 'strapi-super-admin'); - if (isUserSuperAdmin && appInfo?.autoReload) { - setGuidedTourVisibility('super-admin'); - } else if (!isUserSuperAdmin && appInfo?.autoReload) { - setGuidedTourVisibility('admin'); + if (appInfo?.autoReload) { + setGuidedTourVisibility(true, isUserSuperAdmin ? 'super-admin' : 'admin'); } } }, [userRoles, appInfo?.autoReload, setGuidedTourVisibility]); diff --git a/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx b/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx index 573e300e6c3..31d9a8702d7 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Homepage.tsx @@ -1,33 +1,46 @@ import { Box, Button, Flex, Typography } from '@strapi/design-system'; import { LinkButton } from '@strapi/design-system/v2'; -import { GuidedTourContextValue, pxToRem, useGuidedTour, useTracking } from '@strapi/helper-plugin'; +import { + GuidedTourContextValue, + pxToRem, + useGuidedTour, + useTracking, + useAppInfo, +} from '@strapi/helper-plugin'; import { ArrowRight } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { NavLink } from 'react-router-dom'; -import { SUPER_ADMIN_LAYOUT_DATA, LAYOUT_DATA, States, STATES } from './constants'; +import { + SUPER_ADMIN_LAYOUT_DATA, + LAYOUT_DATA, + States, + STATES, + LayoutData, + SuperAdminLayoutData, +} from './constants'; import { Number, VerticalDivider } from './Ornaments'; interface GuidedTourHomepageProps { - visibility: string; + userRole: string; } -const GuidedTourHomepage = ({ visibility }: GuidedTourHomepageProps) => { - const { guidedTourState, setSkipped, guidedTourVisibility } = useGuidedTour(); +const GuidedTourHomepage = ({ userRole }: GuidedTourHomepageProps) => { + const { guidedTourState, setSkipped } = useGuidedTour(); const { formatMessage } = useIntl(); const { trackUsage } = useTracking(); + const appInfo = useAppInfo(); - const layout = visibility === 'super-admin' ? SUPER_ADMIN_LAYOUT_DATA : LAYOUT_DATA; - const layoutCopy = JSON.parse(JSON.stringify(layout)); - - const triggeredBySA = guidedTourVisibility === 'super-admin' ? true : false; + const layout: SuperAdminLayoutData | LayoutData = + userRole === 'super-admin' ? SUPER_ADMIN_LAYOUT_DATA : LAYOUT_DATA; + const triggeredBySA = userRole === 'super-admin' ? true : false; // Remove the inviteUser step if we are in the development env - if (process.env.NODE_ENV === 'development') { - delete layoutCopy.inviteUser; + if (appInfo?.currentEnvironment === 'development') { + delete layout.inviteUser; } - const sections = Object.entries(layoutCopy).map(([key, val]) => ({ + const sections = Object.entries(layout).map(([key, val]) => ({ key: key, title: val.home.title, content: ( @@ -65,11 +78,13 @@ const GuidedTourHomepage = ({ visibility }: GuidedTourHomepageProps) => { > - {Object.keys(layoutCopy).length} - {formatMessage({ - id: 'app.components.GuidedTour.title', - defaultMessage: 'steps to get started', - })} + {formatMessage( + { + id: 'app.components.GuidedTour.title', + defaultMessage: '{count} steps to get started', + }, + { count: Object.keys(layout).length } + )} {sections.map((section, index) => { diff --git a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx index c8853acec1c..d2ac7ed94b7 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx @@ -10,14 +10,26 @@ import { Typography, } from '@strapi/design-system'; import { LinkButton } from '@strapi/design-system/v2'; -import { GuidedTourContextValue, pxToRem, useGuidedTour, useTracking } from '@strapi/helper-plugin'; +import { + GuidedTourContextValue, + pxToRem, + useGuidedTour, + useTracking, + useAppInfo, +} from '@strapi/helper-plugin'; import { ArrowRight, Cross } from '@strapi/icons'; import get from 'lodash/get'; import { MessageDescriptor, useIntl } from 'react-intl'; import { NavLink } from 'react-router-dom'; import styled from 'styled-components'; -import { LAYOUT_DATA, SUPER_ADMIN_LAYOUT_DATA, STATES } from './constants'; +import { + LAYOUT_DATA, + SUPER_ADMIN_LAYOUT_DATA, + STATES, + LayoutData, + SuperAdminLayoutData, +} from './constants'; import { Number, VerticalDivider } from './Ornaments'; /* ------------------------------------------------------------------------------------------------- @@ -25,32 +37,26 @@ import { Number, VerticalDivider } from './Ornaments'; * -----------------------------------------------------------------------------------------------*/ const GuidedTourModal = () => { - const { - currentStep, - guidedTourState, - setCurrentStep, - setStepState, - guidedTourVisibility, - setSkipped, - } = useGuidedTour(); + const { currentStep, guidedTourState, setCurrentStep, setStepState, userRole, setSkipped } = + useGuidedTour(); const { formatMessage } = useIntl(); const { trackUsage } = useTracking(); + const appInfo = useAppInfo(); - if (!currentStep || !guidedTourVisibility) { + if (!currentStep || !userRole) { return null; } - const layout = guidedTourVisibility === 'super-admin' ? SUPER_ADMIN_LAYOUT_DATA : LAYOUT_DATA; - const layoutCopy = JSON.parse(JSON.stringify(layout)); - - const triggeredBySA = guidedTourVisibility === 'super-admin' ? true : false; + const layout: SuperAdminLayoutData | LayoutData = + userRole === 'super-admin' ? SUPER_ADMIN_LAYOUT_DATA : LAYOUT_DATA; + const triggeredBySA = userRole === 'super-admin' ? true : false; - // Remove the inviteUser step if we are in the development env - if (process.env.NODE_ENV === 'development') { - delete layoutCopy.inviteUser; + // Remove the inviteUser step if we are in the development env + if (appInfo?.currentEnvironment === 'development') { + delete layout.inviteUser; } - const stepData = get(layoutCopy, currentStep); + const stepData = get(layout, currentStep); const sectionKeys = Object.keys(guidedTourState); const [sectionName, stepName] = currentStep.split('.') as [ keyof GuidedTourContextValue['guidedTourState'], @@ -260,7 +266,7 @@ const GuidedTourStepper = ({ * -----------------------------------------------------------------------------------------------*/ interface GuidedTourContentProps - extends Required> {} + extends Required> { } const GuidedTourContent = ({ id, defaultMessage }: GuidedTourContentProps) => { const { formatMessage } = useIntl(); diff --git a/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx b/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx index dfc2a122628..89011a3f1a9 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx @@ -7,6 +7,7 @@ import { GuidedTourStep, GuidedTourStepKey, auth, + useAppInfo, } from '@strapi/helper-plugin'; import produce from 'immer'; import get from 'lodash/get'; @@ -21,9 +22,11 @@ interface GuidedTourProviderProps { } const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { - const [{ currentStep, guidedTourState, guidedTourVisibility, isSkipped }, dispatch] = + const [{ currentStep, guidedTourState, isGuidedTourVisible, userRole, isSkipped }, dispatch] = React.useReducer(reducer, initialState, initialiseState); + const appInfo = useAppInfo(); + const setCurrentStep = (step: SetCurrentStepAction['step']) => { // if step is null it is intentional, we need to dispatch it if (step !== null) { @@ -49,21 +52,24 @@ const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { }); }; - const setGuidedTourVisibility = (value: SetGuidedTourVisibilityAction['value']) => { + const setGuidedTourVisibility = ( + value: SetGuidedTourVisibilityAction['value'], + userRole: SetGuidedTourVisibilityAction['userRole'] + ) => { // Update the initial guidedTourState depending on the visibility (executed once) - if (!guidedTourVisibility) { + if (!isGuidedTourVisible) { const guidedTourState = JSON.parse(JSON.stringify(initialState?.guidedTourState)); // Remove the inviteUser step if we are in the development env - if (process.env.NODE_ENV === 'development') { + if (appInfo?.currentEnvironment === 'development') { delete guidedTourState.inviteUser; } // Remove non-related steps to specific guided tour (super-admin or admin) - if (value === 'admin') { + if (userRole === 'admin') { delete guidedTourState.contentTypeBuilder; delete guidedTourState.apiTokens; - } else if (value === 'super-admin') { + } else if (userRole === 'super-admin') { delete guidedTourState.mediaLibrary; delete guidedTourState.profile; } @@ -77,6 +83,7 @@ const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { dispatch({ type: 'SET_GUIDED_TOUR_VISIBILITY', value, + userRole, }); }; @@ -131,7 +138,8 @@ const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { setSkipped={setSkipped} setStepState={setStepState} startSection={startSection} - guidedTourVisibility={guidedTourVisibility} + userRole={userRole} + isGuidedTourVisible={isGuidedTourVisible} isSkipped={isSkipped} > {children} @@ -141,7 +149,7 @@ const GuidedTourProvider = ({ children }: GuidedTourProviderProps) => { type State = Pick< GuidedTourContextValue, - 'guidedTourState' | 'currentStep' | 'guidedTourVisibility' | 'isSkipped' + 'guidedTourState' | 'currentStep' | 'isGuidedTourVisible' | 'isSkipped' | 'userRole' >; const initialState = { @@ -171,8 +179,13 @@ const initialState = { create: false, success: false, }, + transferTokens: { + create: false, + success: false, + }, }, - guidedTourVisibility: null, + isGuidedTourVisible: false, + userRole: 'super-admin', isSkipped: false, } satisfies State; @@ -199,7 +212,8 @@ interface SetSkippedAction { interface SetGuidedTourVisibilityAction { type: 'SET_GUIDED_TOUR_VISIBILITY'; - value: string; + value: boolean; + userRole: string; } type Action = @@ -233,7 +247,8 @@ const reducer: React.Reducer = (state: State = initialState, acti break; } case 'SET_GUIDED_TOUR_VISIBILITY': { - draftState.guidedTourVisibility = action.value; + draftState.isGuidedTourVisible = action.value; + draftState.userRole = action.userRole; break; } default: { diff --git a/packages/core/admin/admin/src/components/GuidedTour/constants.ts b/packages/core/admin/admin/src/components/GuidedTour/constants.ts index 46047298858..78247c41353 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/constants.ts +++ b/packages/core/admin/admin/src/components/GuidedTour/constants.ts @@ -213,7 +213,7 @@ const LAYOUT_DATA = { trackingEvent: 'didGenerateGuidedTourUsers', }, }, -} as const; +}; const SUPER_ADMIN_LAYOUT_DATA = { contentTypeBuilder: { @@ -429,7 +429,7 @@ const SUPER_ADMIN_LAYOUT_DATA = { trackingEvent: 'didGenerateGuidedTourUsers', }, }, -} as const; +}; const STATES = { IS_DONE: 'IS_DONE', @@ -437,8 +437,20 @@ const STATES = { IS_NOT_DONE: 'IS_NOT_DONE', } as const; -type LayoutData = typeof LAYOUT_DATA; -type SuperAdminLayoutData = typeof SUPER_ADMIN_LAYOUT_DATA; +interface LayoutData { + contentManager: object; + mediaLibrary: object; + profile: object; + inviteUser?: object; +} + +interface SuperAdminLayoutData { + contentTypeBuilder: object; + contentManager: object; + apiTokens: object; + inviteUser?: object; +} + type States = keyof typeof STATES; export { SUPER_ADMIN_LAYOUT_DATA, LAYOUT_DATA, STATES }; diff --git a/packages/core/admin/admin/src/pages/HomePage.tsx b/packages/core/admin/admin/src/pages/HomePage.tsx index 34a65509c0b..16848b998c2 100644 --- a/packages/core/admin/admin/src/pages/HomePage.tsx +++ b/packages/core/admin/admin/src/pages/HomePage.tsx @@ -44,12 +44,12 @@ const HomePageCE = () => { const { formatMessage } = useIntl(); // Temporary until we develop the menu API const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useContentTypes(); - const { guidedTourState, guidedTourVisibility, isSkipped } = useGuidedTour(); + const { guidedTourState, isGuidedTourVisible, isSkipped, userRole } = useGuidedTour(); const showGuidedTour = !Object.values(guidedTourState).every((section) => Object.values(section).every((step) => step) ) && - guidedTourVisibility && + isGuidedTourVisible && !isSkipped; const { push } = useHistory(); @@ -137,11 +137,7 @@ const HomePageCE = () => { - {showGuidedTour ? ( - - ) : ( - - )} + {showGuidedTour ? : } diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx index 03bfc578f8a..edfc72b8cdc 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx @@ -36,7 +36,7 @@ import { MagicLinkCE } from './MagicLinkCE'; import { SelectRoles } from './SelectRoles'; interface ModalFormProps { - onToggle: (type: string) => void; // Now expects an optional string argument + onToggle: (type?: string) => void; // Now expects an optional string argument } const ModalForm = ({ onToggle }: ModalFormProps) => { diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 15e445c76d5..43594d1a251 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -457,10 +457,10 @@ "app.components.GuidedTour.home.users.create.cta.title": "Manage admin users", "app.components.GuidedTour.home.mediaLibrary.title": "🖼️ Upload your first Media", "app.components.GuidedTour.home.mediaLibrary.cta.title": "Go to the Media Library", - "app.components.GuidedTour.home.profile.title": "⚙️ Customize your experience", - "app.components.GuidedTour.home.profile.cta.title": "Manage your profile", + "app.components.GuidedTour.home.profile.title": "⚙️ Customize your experience", + "app.components.GuidedTour.home.profile.cta.title": "Manage your profile", "app.components.GuidedTour.skip": "Skip the tour", - "app.components.GuidedTour.title": " steps to get started", + "app.components.GuidedTour.title": "{count} steps to get started", "app.components.HomePage.button.blog": "See more on the blog", "app.components.HomePage.community": "Join the community", "app.components.HomePage.community.content": "Discuss with team members, contributors and developers on different channels.", diff --git a/packages/core/helper-plugin/src/features/GuidedTour.tsx b/packages/core/helper-plugin/src/features/GuidedTour.tsx index 78c126e8536..f0b6f572cd7 100644 --- a/packages/core/helper-plugin/src/features/GuidedTour.tsx +++ b/packages/core/helper-plugin/src/features/GuidedTour.tsx @@ -39,11 +39,16 @@ interface GuidedTourContextValue { create: boolean; success: boolean; }; + transferTokens: { + create: boolean; + success: boolean; + }; }; - guidedTourVisibility: string | null; + userRole: string; + isGuidedTourVisible: boolean; isSkipped: boolean; setCurrentStep: (step: Step | null) => void | null; - setGuidedTourVisibility: (isVisible: string) => void; + setGuidedTourVisibility: (isVisible: boolean, userRole: string) => void; setSkipped: (isSkipped: boolean) => void; setStepState: (step: Step, state: boolean) => void; startSection: (section: SectionKey) => void; @@ -76,8 +81,13 @@ const GuidedTourContext = React.createContext({ create: false, success: false, }, + transferTokens: { + create: false, + success: false, + }, }, - guidedTourVisibility: null, + userRole: 'super-admin', + isGuidedTourVisible: false, isSkipped: true, setCurrentStep: () => null, setGuidedTourVisibility: () => null, @@ -98,7 +108,8 @@ const GuidedTourProvider = ({ children, currentStep = null, guidedTourState, - guidedTourVisibility = null, + isGuidedTourVisible = false, + userRole = 'super-admin', isSkipped, setCurrentStep, setGuidedTourVisibility, @@ -110,7 +121,8 @@ const GuidedTourProvider = ({ () => ({ currentStep, guidedTourState, - guidedTourVisibility, + userRole, + isGuidedTourVisible, isSkipped, setCurrentStep, setGuidedTourVisibility, @@ -121,7 +133,8 @@ const GuidedTourProvider = ({ [ currentStep, guidedTourState, - guidedTourVisibility, + userRole, + isGuidedTourVisible, isSkipped, setCurrentStep, setGuidedTourVisibility, From 0e159b6a4a99c463cf09d659677e79ca2e5cd024 Mon Sep 17 00:00:00 2001 From: Maxime Castres <17828745+Mcastres@users.noreply.github.com> Date: Thu, 25 Apr 2024 13:45:48 +0200 Subject: [PATCH 05/10] Update packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx Co-authored-by: Josh <37798644+joshuaellis@users.noreply.github.com> --- .../src/pages/Settings/pages/Users/components/NewUserForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx index edfc72b8cdc..4581e4b78c1 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx @@ -36,7 +36,7 @@ import { MagicLinkCE } from './MagicLinkCE'; import { SelectRoles } from './SelectRoles'; interface ModalFormProps { - onToggle: (type?: string) => void; // Now expects an optional string argument + onToggle: (type?: string) => void; } const ModalForm = ({ onToggle }: ModalFormProps) => { From a14356d0a3d2fa41c2c37eef5c1e7fd55ef38539 Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Thu, 25 Apr 2024 15:39:08 +0200 Subject: [PATCH 06/10] Fix: prettier --- .../admin/src/components/GuidedTour/Modal.tsx | 13 +++++++--- .../src/components/GuidedTour/Provider.tsx | 7 ++++- .../pages/Settings/pages/Users/ListPage.tsx | 2 +- .../pages/Users/components/NewUserForm.tsx | 4 +-- .../core/admin/admin/src/translations/en.json | 2 +- .../helper-plugin/src/features/GuidedTour.tsx | 26 +------------------ .../UploadAssetDialog/UploadAssetDialog.jsx | 3 +-- .../src/pages/App/MediaLibrary/index.jsx | 5 +--- 8 files changed, 22 insertions(+), 40 deletions(-) diff --git a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx index d2ac7ed94b7..274fecb96c5 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx @@ -56,7 +56,7 @@ const GuidedTourModal = () => { delete layout.inviteUser; } - const stepData = get(layout, currentStep); + const stepData: StepData | undefined = get(layout, currentStep); const sectionKeys = Object.keys(guidedTourState); const [sectionName, stepName] = currentStep.split('.') as [ keyof GuidedTourContextValue['guidedTourState'], @@ -123,7 +123,7 @@ const GuidedTourModal = () => { sectionIndex={sectionIndex} stepIndex={stepIndex} hasSectionAfter={hasSectionAfter} - stepCount={Object.keys(layoutCopy).length} + stepCount={Object.keys(layout).length} > {stepData && 'content' in stepData && } @@ -153,6 +153,13 @@ const ModalWrapper = styled(Flex)` background: ${({ theme }) => `${theme.colors.neutral800}1F`}; `; +interface StepData { + trackingEvent?: string; + title?: GuidedTourStepperProps['title']; + cta?: GuidedTourStepperProps['cta']; + content?: GuidedTourContentProps; +} + /* ------------------------------------------------------------------------------------------------- * GuidedTourStepper * -----------------------------------------------------------------------------------------------*/ @@ -266,7 +273,7 @@ const GuidedTourStepper = ({ * -----------------------------------------------------------------------------------------------*/ interface GuidedTourContentProps - extends Required> { } + extends Required> {} const GuidedTourContent = ({ id, defaultMessage }: GuidedTourContentProps) => { const { formatMessage } = useIntl(); diff --git a/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx b/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx index 89011a3f1a9..a35f1f4f72b 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Provider.tsx @@ -202,7 +202,12 @@ interface SetStepStateAction { interface SetGuidedTourStateAction { type: 'SET_GUIDED_TOUR_STATE'; - value: object; + value: { + [key: string]: { + create: boolean; + success: boolean; + }; + }; } interface SetSkippedAction { diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Users/ListPage.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Users/ListPage.tsx index 0a9b4d4ffd8..e1a6c310f77 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Users/ListPage.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Users/ListPage.tsx @@ -92,7 +92,7 @@ const ListPageCE = () => { defaultMessage: 'Users', }); - const handleToggle = (step: string) => { + const handleToggle = (step: unknown) => { if (step === 'magic-link') { setCurrentStep('inviteUser.success'); } diff --git a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx index 4581e4b78c1..b6f4c735d86 100644 --- a/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx +++ b/packages/core/admin/admin/src/pages/Settings/pages/Users/components/NewUserForm.tsx @@ -20,7 +20,6 @@ import { useOverlayBlocker, translatedErrors, useAPIErrorHandler, - useGuidedTour, } from '@strapi/helper-plugin'; import { Entity } from '@strapi/types'; import { Formik, FormikHelpers } from 'formik'; @@ -36,14 +35,13 @@ import { MagicLinkCE } from './MagicLinkCE'; import { SelectRoles } from './SelectRoles'; interface ModalFormProps { - onToggle: (type?: string) => void; + onToggle: (type?: unknown) => void; } const ModalForm = ({ onToggle }: ModalFormProps) => { const [currentStep, setStep] = React.useState('create'); const [registrationToken, setRegistrationToken] = React.useState(''); const { formatMessage } = useIntl(); - const { setCurrentStep } = useGuidedTour(); const toggleNotification = useNotification(); const { lockApp, unlockApp } = useOverlayBlocker(); const { diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 43594d1a251..aab919ebf1b 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -1020,4 +1020,4 @@ "selectButtonTitle": "Select", "skipToContent": "Skip to content", "submit": "Submit" -} \ No newline at end of file +} diff --git a/packages/core/helper-plugin/src/features/GuidedTour.tsx b/packages/core/helper-plugin/src/features/GuidedTour.tsx index f0b6f572cd7..56027bc51d0 100644 --- a/packages/core/helper-plugin/src/features/GuidedTour.tsx +++ b/packages/core/helper-plugin/src/features/GuidedTour.tsx @@ -15,31 +15,7 @@ type Step = `${SectionKey}.${StepKey}`; interface GuidedTourContextValue { currentStep: Step | null; guidedTourState: { - contentTypeBuilder: { - create: boolean; - success: boolean; - }; - contentManager: { - create: boolean; - success: boolean; - }; - apiTokens: { - create: boolean; - success: boolean; - }; - mediaLibrary: { - create: boolean; - success: boolean; - }; - profile: { - create: boolean; - success: boolean; - }; - inviteUser: { - create: boolean; - success: boolean; - }; - transferTokens: { + [key: string]: { create: boolean; success: boolean; }; diff --git a/packages/core/upload/admin/src/components/UploadAssetDialog/UploadAssetDialog.jsx b/packages/core/upload/admin/src/components/UploadAssetDialog/UploadAssetDialog.jsx index 4c77a3d5349..c076e9ca8d2 100644 --- a/packages/core/upload/admin/src/components/UploadAssetDialog/UploadAssetDialog.jsx +++ b/packages/core/upload/admin/src/components/UploadAssetDialog/UploadAssetDialog.jsx @@ -31,7 +31,6 @@ export const UploadAssetDialog = ({ const { setCurrentStep } = useGuidedTour(); - const handleAddToPendingAssets = (nextAssets) => { validateAssetsTypes(nextAssets, () => { setAssets((prevAssets) => prevAssets.concat(nextAssets)); @@ -61,7 +60,7 @@ export const UploadAssetDialog = ({ onClose(); } - setCurrentStep("mediaLibrary.success") + setCurrentStep('mediaLibrary.success'); }; const handleAssetEditValidation = (nextAsset) => { diff --git a/packages/core/upload/admin/src/pages/App/MediaLibrary/index.jsx b/packages/core/upload/admin/src/pages/App/MediaLibrary/index.jsx index 702ca052051..c6d2c4fe928 100644 --- a/packages/core/upload/admin/src/pages/App/MediaLibrary/index.jsx +++ b/packages/core/upload/admin/src/pages/App/MediaLibrary/index.jsx @@ -24,7 +24,7 @@ import { useQueryParams, useSelectionState, useTracking, - useGuidedTour + useGuidedTour, } from '@strapi/helper-plugin'; import { Cog, Grid, List, Pencil } from '@strapi/icons'; import { stringify } from 'qs'; @@ -168,8 +168,6 @@ export const MediaLibrary = () => { setShowEditFolderDialog((prev) => !prev); }; - - const handleBulkSelect = (event, elements) => { if (event.target.checked) { trackUsage('didSelectAllMediaLibraryElements'); @@ -221,7 +219,6 @@ export const MediaLibrary = () => { useFocusWhenNavigate(); - return (
From 567b150dc6d2bc3ed38e126fb69f8cdbb5eb3de1 Mon Sep 17 00:00:00 2001 From: Josh <37798644+joshuaellis@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:02:33 +0100 Subject: [PATCH 07/10] chore: fix types --- .../admin/src/components/GuidedTour/Modal.tsx | 22 ++++-------- .../src/components/GuidedTour/constants.ts | 34 ++++++++++++++++++- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx index 274fecb96c5..f547d4ae8ab 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx @@ -10,13 +10,7 @@ import { Typography, } from '@strapi/design-system'; import { LinkButton } from '@strapi/design-system/v2'; -import { - GuidedTourContextValue, - pxToRem, - useGuidedTour, - useTracking, - useAppInfo, -} from '@strapi/helper-plugin'; +import { pxToRem, useGuidedTour, useTracking, useAppInfo } from '@strapi/helper-plugin'; import { ArrowRight, Cross } from '@strapi/icons'; import get from 'lodash/get'; import { MessageDescriptor, useIntl } from 'react-intl'; @@ -29,6 +23,7 @@ import { STATES, LayoutData, SuperAdminLayoutData, + TrackingEvents, } from './constants'; import { Number, VerticalDivider } from './Ornaments'; @@ -56,12 +51,9 @@ const GuidedTourModal = () => { delete layout.inviteUser; } - const stepData: StepData | undefined = get(layout, currentStep); + const stepData = get(layout, currentStep) as StepData | undefined; const sectionKeys = Object.keys(guidedTourState); - const [sectionName, stepName] = currentStep.split('.') as [ - keyof GuidedTourContextValue['guidedTourState'], - string - ]; + const [sectionName, stepName] = currentStep.split('.'); const sectionIndex = sectionKeys.indexOf(sectionName); const stepIndex = Object.keys(guidedTourState[sectionName]).indexOf(stepName); const hasSectionAfter = sectionIndex < sectionKeys.length - 1; @@ -70,7 +62,7 @@ const GuidedTourModal = () => { const handleCtaClick = () => { setStepState(currentStep, true); - if (stepData) { + if (stepData && stepData.trackingEvent) { trackUsage(stepData.trackingEvent, { triggeredBySA }); } @@ -125,7 +117,7 @@ const GuidedTourModal = () => { hasSectionAfter={hasSectionAfter} stepCount={Object.keys(layout).length} > - {stepData && 'content' in stepData && } + {stepData?.content ? : null} {!(!hasStepAfter && !hasSectionAfter) && ( @@ -154,7 +146,7 @@ const ModalWrapper = styled(Flex)` `; interface StepData { - trackingEvent?: string; + trackingEvent?: TrackingEvents; title?: GuidedTourStepperProps['title']; cta?: GuidedTourStepperProps['cta']; content?: GuidedTourContentProps; diff --git a/packages/core/admin/admin/src/components/GuidedTour/constants.ts b/packages/core/admin/admin/src/components/GuidedTour/constants.ts index 78247c41353..7bcaf984d64 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/constants.ts +++ b/packages/core/admin/admin/src/components/GuidedTour/constants.ts @@ -431,6 +431,38 @@ const SUPER_ADMIN_LAYOUT_DATA = { }, }; +type TrackingEvents = + | 'didClickGuidedTourHomepageContentManager' + | 'didClickGuidedTourStep1ContentManager' + | 'didCreateGuidedTourEntry' + | 'didClickGuidedTourHomepageMediaLibrary' + | 'didClickGuidedTourStep2MediaLibrary' + | 'didCreateGuidedTourMedia' + | 'didClickGuidedTourHomepageProfile' + | 'didClickGuidedTourStep3Profile' + | 'didUpdateGuidedTourProfile' + | 'didClickGuidedTourHomepageUsers' + | 'didClickGuidedTourStep4Users' + | 'didGenerateGuidedTourUsers' + | 'didClickGuidedTourHomepageContentTypeBuilder' + | 'didClickGuidedTourStep1CollectionType' + | 'didCreateGuidedTourCollectionType' + | 'didClickGuidedTourHomepageApiTokens' + | 'didClickGuidedTourStep3ApiTokens' + | 'didGenerateGuidedTourApiTokens' + | 'didClickGuidedTourHomepageUsers' + | 'didClickGuidedTourStep4Users' + | 'didGenerateGuidedTourUsers' + | 'didClickGuidedTourHomepageContentManager' + | 'didClickGuidedTourStep2ContentManager' + | 'didCreateGuidedTourEntry' + | 'didClickGuidedTourHomepageApiTokens' + | 'didClickGuidedTourStep3ApiTokens' + | 'didGenerateGuidedTourApiTokens' + | 'didClickGuidedTourHomepageUsers' + | 'didClickGuidedTourStep4Users' + | 'didGenerateGuidedTourUsers'; + const STATES = { IS_DONE: 'IS_DONE', IS_ACTIVE: 'IS_ACTIVE', @@ -454,4 +486,4 @@ interface SuperAdminLayoutData { type States = keyof typeof STATES; export { SUPER_ADMIN_LAYOUT_DATA, LAYOUT_DATA, STATES }; -export type { SuperAdminLayoutData, LayoutData, States }; +export type { SuperAdminLayoutData, LayoutData, States, TrackingEvents }; From 32459c657fe355402802e3a5a23eefac0b15ad60 Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Thu, 2 May 2024 11:39:41 +0200 Subject: [PATCH 08/10] Update: fix tests --- .../admin/src/components/GuidedTour/tests/Homepage.test.tsx | 2 +- .../admin/src/components/GuidedTour/tests/Modal.test.tsx | 2 +- .../admin/src/components/GuidedTour/tests/Provider.test.tsx | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/core/admin/admin/src/components/GuidedTour/tests/Homepage.test.tsx b/packages/core/admin/admin/src/components/GuidedTour/tests/Homepage.test.tsx index 57adde6620c..5083b5a29c3 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/tests/Homepage.test.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/tests/Homepage.test.tsx @@ -35,7 +35,7 @@ const App = ( - + diff --git a/packages/core/admin/admin/src/components/GuidedTour/tests/Modal.test.tsx b/packages/core/admin/admin/src/components/GuidedTour/tests/Modal.test.tsx index bb5c594112c..8ed61e5e386 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/tests/Modal.test.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/tests/Modal.test.tsx @@ -29,7 +29,7 @@ describe('', () => { it('should match the snapshot with contentTypeBuilder.create layout', async () => { render(); - expect(screen.getByText('🧠 Create a first Collection type')).toBeInTheDocument(); + expect(screen.getByText('🧠 Create a first Collection Type')).toBeInTheDocument(); expect(document.body).toMatchInlineSnapshot(` .c2 { diff --git a/packages/core/admin/admin/src/components/GuidedTour/tests/Provider.test.tsx b/packages/core/admin/admin/src/components/GuidedTour/tests/Provider.test.tsx index 7170022a6f4..8a08b0063a0 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/tests/Provider.test.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/tests/Provider.test.tsx @@ -26,7 +26,7 @@ describe('GuidedTour', () => { const { setGuidedTourVisibility, isGuidedTourVisible } = useGuidedTour(); React.useEffect(() => { - setGuidedTourVisibility(true); + setGuidedTourVisibility(true, 'super-admin'); }, [setGuidedTourVisibility]); return
{isGuidedTourVisible &&

hello guided tour

}
; @@ -46,7 +46,7 @@ describe('GuidedTour', () => { const { setGuidedTourVisibility, isGuidedTourVisible } = useGuidedTour(); React.useEffect(() => { - setGuidedTourVisibility(false); + setGuidedTourVisibility(false, 'super-admin'); }, [setGuidedTourVisibility]); return
{isGuidedTourVisible &&

hello guided tour

}
; @@ -126,7 +126,6 @@ describe('GuidedTour', () => { const { startSection, currentStep } = useGuidedTour(); React.useEffect(() => { - // @ts-expect-error – testing it doesn't do something we don't want it too. startSection('failTest'); }, [startSection]); From 82a28df4cd8c1e194a320c81c1bce8ab1967624f Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Thu, 2 May 2024 11:52:06 +0200 Subject: [PATCH 09/10] Update: fix tests --- .../admin/admin/src/components/GuidedTour/tests/Modal.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/admin/admin/src/components/GuidedTour/tests/Modal.test.tsx b/packages/core/admin/admin/src/components/GuidedTour/tests/Modal.test.tsx index 8ed61e5e386..14304e23e6b 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/tests/Modal.test.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/tests/Modal.test.tsx @@ -589,7 +589,7 @@ describe('', () => { class="c16 c21" id="title" > - 🧠 Create a first Collection type + 🧠 Create a first Collection Type
Date: Tue, 11 Jun 2024 10:09:07 +0200 Subject: [PATCH 10/10] Add: TS --- packages/core/admin/admin/src/components/GuidedTour/Modal.tsx | 2 +- .../admin/src/components/GuidedTour/tests/Homepage.test.tsx | 4 ++-- .../admin/src/components/GuidedTour/tests/Modal.test.tsx | 3 +++ .../core/admin/admin/src/pages/Auth/components/Register.tsx | 2 ++ packages/core/admin/admin/src/translations/en.json | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx index f547d4ae8ab..065ff580cf5 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Modal.tsx @@ -196,7 +196,7 @@ const GuidedTourStepper = ({ {stepCount} {formatMessage({ id: 'app.components.GuidedTour.title', - defaultMessage: 'steps to get started', + defaultMessage: ' steps to get started', })} diff --git a/packages/core/admin/admin/src/components/GuidedTour/tests/Homepage.test.tsx b/packages/core/admin/admin/src/components/GuidedTour/tests/Homepage.test.tsx index 5083b5a29c3..1cd6c2a92d0 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/tests/Homepage.test.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/tests/Homepage.test.tsx @@ -464,7 +464,7 @@ describe('GuidedTour Homepage', () => {

- 🧠 Build the content structure + 🧠 Create a first Collection Type

{

- ⚡️ What would you like to share with the world? + ⚡️ Create content

({ ...jest.requireActual('@strapi/helper-plugin'), useGuidedTour: jest.fn(() => ({ isGuidedTourVisible: true, + userRole: 'super-admin', guidedTourState: { apiTokens: { create: false, @@ -674,6 +675,7 @@ describe('', () => { // @ts-expect-error – mocking useGuidedTour.mockImplementation(() => ({ isGuidedTourVisible: true, + userRole: 'super-admin', guidedTourState: { apiTokens: { create: false, @@ -700,6 +702,7 @@ describe('', () => { // @ts-expect-error – mocking useGuidedTour.mockImplementation(() => ({ isGuidedTourVisible: false, + userRole: 'super-admin', guidedTourState: { apiTokens: { create: false, diff --git a/packages/core/admin/admin/src/pages/Auth/components/Register.tsx b/packages/core/admin/admin/src/pages/Auth/components/Register.tsx index 3122eed85e1..1dab75586ab 100644 --- a/packages/core/admin/admin/src/pages/Auth/components/Register.tsx +++ b/packages/core/admin/admin/src/pages/Auth/components/Register.tsx @@ -152,6 +152,8 @@ const Register = ({ hasAdmin }: RegisterProps) => { ) => { const res = await registerAdmin(body); + // eslint-disable-next-line no-console + console.log(news); if ('data' in res) { setToken(res.data.token); diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index aab919ebf1b..81bc632baf0 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -460,7 +460,7 @@ "app.components.GuidedTour.home.profile.title": "⚙️ Customize your experience", "app.components.GuidedTour.home.profile.cta.title": "Manage your profile", "app.components.GuidedTour.skip": "Skip the tour", - "app.components.GuidedTour.title": "{count} steps to get started", + "app.components.GuidedTour.title": " steps to get started", "app.components.HomePage.button.blog": "See more on the blog", "app.components.HomePage.community": "Join the community", "app.components.HomePage.community.content": "Discuss with team members, contributors and developers on different channels.",