From e662f6ccb3971be5776a78abe23fb60f9e9658e2 Mon Sep 17 00:00:00 2001 From: sid0-0 <43578323+sid0-0@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:09:41 +0530 Subject: [PATCH] fix: fixed shortcuts population (#7016) This PR fixes #6776 Screenshots: image --------- Co-authored-by: sid0-0 Co-authored-by: Lucas Bordeau --- packages/twenty-front/src/App.tsx | 171 ------------------ ...{App.stories.tsx => AppRouter.stories.tsx} | 13 +- .../effect-components/GotoHotkeysEffect.tsx | 11 -- packages/twenty-front/src/index.tsx | 35 +--- .../src/modules/app/components/App.tsx | 32 ++++ .../src/modules/app/components/AppRouter.tsx | 27 +++ .../app/components/AppRouterProviders.tsx | 66 +++++++ .../app/components}/SettingsRoutes.tsx | 0 .../effect-components/CommandMenuEffect.tsx | 2 +- .../GoToHotkeyItemEffect.tsx | 12 ++ .../effect-components/GotoHotkeysEffect.tsx | 18 ++ .../effect-components/PageChangeEffect.tsx | 19 +- .../src/modules/app/utils/createAppRouter.tsx | 78 ++++++++ .../__stories__/CommandMenu.stories.tsx | 21 ++- .../constants/CommandMenuCommands.ts | 14 +- .../hooks/__test__/useCommandMenu.test.tsx | 34 +++- .../command-menu/hooks/useCommandMenu.ts | 34 +++- .../useNonSystemActiveObjectMetadataItems.ts | 20 ++ .../src/utils/array/sortByProperty.ts | 18 ++ 19 files changed, 380 insertions(+), 245 deletions(-) delete mode 100644 packages/twenty-front/src/App.tsx rename packages/twenty-front/src/__stories__/{App.stories.tsx => AppRouter.stories.tsx} (92%) delete mode 100644 packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx create mode 100644 packages/twenty-front/src/modules/app/components/App.tsx create mode 100644 packages/twenty-front/src/modules/app/components/AppRouter.tsx create mode 100644 packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx rename packages/twenty-front/src/{ => modules/app/components}/SettingsRoutes.tsx (100%) rename packages/twenty-front/src/{ => modules/app}/effect-components/CommandMenuEffect.tsx (89%) create mode 100644 packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx create mode 100644 packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx rename packages/twenty-front/src/{ => modules/app}/effect-components/PageChangeEffect.tsx (90%) create mode 100644 packages/twenty-front/src/modules/app/utils/createAppRouter.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts create mode 100644 packages/twenty-front/src/utils/array/sortByProperty.ts diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx deleted file mode 100644 index e8757d1b9..000000000 --- a/packages/twenty-front/src/App.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { StrictMode } from 'react'; -import { - createBrowserRouter, - createRoutesFromElements, - Outlet, - Route, - RouterProvider, - useLocation, -} from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; - -import { ApolloProvider } from '@/apollo/components/ApolloProvider'; -import { AuthProvider } from '@/auth/components/AuthProvider'; -import { VerifyEffect } from '@/auth/components/VerifyEffect'; -import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect'; -import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider'; -import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; -import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; -import { billingState } from '@/client-config/states/billingState'; -import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; -import indexAppPath from '@/navigation/utils/indexAppPath'; -import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; -import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; -import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; -import { AppPath } from '@/types/AppPath'; -import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; -import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; -import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; -import { BlankLayout } from '@/ui/layout/page/BlankLayout'; -import { DefaultLayout } from '@/ui/layout/page/DefaultLayout'; -import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; -import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; -import { UserProvider } from '@/users/components/UserProvider'; -import { UserProviderEffect } from '@/users/components/UserProviderEffect'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect'; -import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect'; -import { PageChangeEffect } from '~/effect-components/PageChangeEffect'; -import { Authorize } from '~/pages/auth/Authorize'; -import { Invite } from '~/pages/auth/Invite'; -import { PasswordReset } from '~/pages/auth/PasswordReset'; -import { SignInUp } from '~/pages/auth/SignInUp'; -import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect'; -import { NotFound } from '~/pages/not-found/NotFound'; -import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage'; -import { RecordShowPage } from '~/pages/object-record/RecordShowPage'; -import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan'; -import { CreateProfile } from '~/pages/onboarding/CreateProfile'; -import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace'; -import { InviteTeam } from '~/pages/onboarding/InviteTeam'; -import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess'; -import { SyncEmails } from '~/pages/onboarding/SyncEmails'; -import { SettingsRoutes } from '~/SettingsRoutes'; -import { getPageTitleFromPath } from '~/utils/title-utils'; - -const ProvidersThatNeedRouterContext = () => { - const { pathname } = useLocation(); - const pageTitle = getPageTitleFromPath(pathname); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -const createRouter = ( - isBillingEnabled?: boolean, - isCRMMigrationEnabled?: boolean, - isServerlessFunctionSettingsEnabled?: boolean, -) => - createBrowserRouter( - createRoutesFromElements( - } - // To switch state to `loading` temporarily to enable us - // to set scroll position before the page is rendered - loader={async () => Promise.resolve(null)} - > - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - - } - /> - } /> - - }> - } /> - - , - ), - ); - -export const App = () => { - const billing = useRecoilValue(billingState); - const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED'); - const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED'); - const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled( - 'IS_FUNCTION_SETTINGS_ENABLED', - ); - - const isBillingPageEnabled = - billing?.isBillingEnabled && !isFreeAccessEnabled; - - return ( - - ); -}; diff --git a/packages/twenty-front/src/__stories__/App.stories.tsx b/packages/twenty-front/src/__stories__/AppRouter.stories.tsx similarity index 92% rename from packages/twenty-front/src/__stories__/App.stories.tsx rename to packages/twenty-front/src/__stories__/AppRouter.stories.tsx index c5314b565..9d2fe91a6 100644 --- a/packages/twenty-front/src/__stories__/App.stories.tsx +++ b/packages/twenty-front/src/__stories__/AppRouter.stories.tsx @@ -1,8 +1,8 @@ -import { HelmetProvider } from 'react-helmet-async'; import { getOperationName } from '@apollo/client/utilities'; import { jest } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { graphql, HttpResponse } from 'msw'; +import { HelmetProvider } from 'react-helmet-async'; import { RecoilRoot } from 'recoil'; import { IconsProvider } from 'twenty-ui'; @@ -11,13 +11,14 @@ import indexAppPath from '@/navigation/utils/indexAppPath'; import { AppPath } from '@/types/AppPath'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; -import { App } from '~/App'; + +import { AppRouter } from '@/app/components/AppRouter'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { mockedUserData } from '~/testing/mock-data/users'; -const meta: Meta = { - title: 'App/App', - component: App, +const meta: Meta = { + title: 'App/AppRouter', + component: AppRouter, decorators: [ (Story) => { return ( @@ -41,7 +42,7 @@ const meta: Meta = { }; export default meta; -export type Story = StoryObj; +export type Story = StoryObj; export const Default: Story = { play: async () => { diff --git a/packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx deleted file mode 100644 index 1109066af..000000000 --- a/packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys'; - -export const GotoHotkeysEffect = () => { - useGoToHotkeys('p', '/objects/people'); - useGoToHotkeys('c', '/objects/companies'); - useGoToHotkeys('o', '/objects/opportunities'); - useGoToHotkeys('s', '/settings/profile'); - useGoToHotkeys('t', '/objects/tasks'); - - return <>; -}; diff --git a/packages/twenty-front/src/index.tsx b/packages/twenty-front/src/index.tsx index 06527d800..2a9ce791f 100644 --- a/packages/twenty-front/src/index.tsx +++ b/packages/twenty-front/src/index.tsx @@ -1,42 +1,13 @@ import ReactDOM from 'react-dom/client'; -import { HelmetProvider } from 'react-helmet-async'; -import { RecoilRoot } from 'recoil'; -import { IconsProvider } from 'twenty-ui'; - -import { CaptchaProvider } from '@/captcha/components/CaptchaProvider'; -import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect'; -import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; -import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; -import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import '@emotion/react'; -import { App } from './App'; - -import './index.css'; +import { App } from '@/app/components/App'; import 'react-loading-skeleton/dist/skeleton.css'; +import './index.css'; const root = ReactDOM.createRoot( document.getElementById('root') ?? document.body, ); -root.render( - - - - - - - - - - - - - - - - - , -); +root.render(); diff --git a/packages/twenty-front/src/modules/app/components/App.tsx b/packages/twenty-front/src/modules/app/components/App.tsx new file mode 100644 index 000000000..f760ee9f6 --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/App.tsx @@ -0,0 +1,32 @@ +import { AppRouter } from '@/app/components/AppRouter'; +import { CaptchaProvider } from '@/captcha/components/CaptchaProvider'; +import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect'; +import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; +import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; +import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider'; +import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; +import { HelmetProvider } from 'react-helmet-async'; +import { RecoilRoot } from 'recoil'; +import { IconsProvider } from 'twenty-ui'; + +export const App = () => { + return ( + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx new file mode 100644 index 000000000..d8985e676 --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx @@ -0,0 +1,27 @@ +import { createAppRouter } from '@/app/utils/createAppRouter'; +import { billingState } from '@/client-config/states/billingState'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { RouterProvider } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; + +export const AppRouter = () => { + const billing = useRecoilValue(billingState); + const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED'); + const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED'); + const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled( + 'IS_FUNCTION_SETTINGS_ENABLED', + ); + + const isBillingPageEnabled = + billing?.isBillingEnabled && !isFreeAccessEnabled; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx new file mode 100644 index 000000000..e5a24da40 --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx @@ -0,0 +1,66 @@ +import { ApolloProvider } from '@/apollo/components/ApolloProvider'; +import { CommandMenuEffect } from '@/app/effect-components/CommandMenuEffect'; +import { GotoHotkeys } from '@/app/effect-components/GotoHotkeysEffect'; +import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect'; +import { AuthProvider } from '@/auth/components/AuthProvider'; +import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect'; +import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider'; +import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; +import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; +import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; +import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; +import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; +import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; +import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; +import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; +import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; +import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; +import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { UserProvider } from '@/users/components/UserProvider'; +import { UserProviderEffect } from '@/users/components/UserProviderEffect'; +import { StrictMode } from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; +import { getPageTitleFromPath } from '~/utils/title-utils'; + +export const AppRouterProviders = () => { + const { pathname } = useLocation(); + const pageTitle = getPageTitleFromPath(pathname); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx similarity index 100% rename from packages/twenty-front/src/SettingsRoutes.tsx rename to packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx diff --git a/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx similarity index 89% rename from packages/twenty-front/src/effect-components/CommandMenuEffect.tsx rename to packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx index ece319312..b210ae724 100644 --- a/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx @@ -7,7 +7,7 @@ import { commandMenuCommandsState } from '@/command-menu/states/commandMenuComma export const CommandMenuEffect = () => { const setCommands = useSetRecoilState(commandMenuCommandsState); - const commands = COMMAND_MENU_COMMANDS; + const commands = Object.values(COMMAND_MENU_COMMANDS); useEffect(() => { setCommands(commands); }, [commands, setCommands]); diff --git a/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx new file mode 100644 index 000000000..a0b545302 --- /dev/null +++ b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx @@ -0,0 +1,12 @@ +import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys'; + +export const GoToHotkeyItemEffect = (props: { + hotkey: string; + pathToNavigateTo: string; +}) => { + const { hotkey, pathToNavigateTo } = props; + + useGoToHotkeys(hotkey, pathToNavigateTo); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx new file mode 100644 index 000000000..15d371f9f --- /dev/null +++ b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx @@ -0,0 +1,18 @@ +import { GoToHotkeyItemEffect } from '@/app/effect-components/GoToHotkeyItemEffect'; +import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems'; +import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys'; + +export const GotoHotkeys = () => { + const { nonSystemActiveObjectMetadataItems } = + useNonSystemActiveObjectMetadataItems(); + + // Hardcoded since settings is static + useGoToHotkeys('s', '/settings/profile'); + + return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => ( + + )); +}; diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx similarity index 90% rename from packages/twenty-front/src/effect-components/PageChangeEffect.tsx rename to packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index 05c99cc89..a8b05f4c0 100644 --- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -12,6 +12,8 @@ import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCapt import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { CommandType } from '@/command-menu/types/Command'; +import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { AppBasePath } from '@/types/AppBasePath'; @@ -43,7 +45,9 @@ export const PageChangeEffect = () => { const eventTracker = useEventTracker(); - const { addToCommandMenu, setToInitialCommandMenu } = useCommandMenu(); + const { addToCommandMenu, setObjectsInCommandMenu } = useCommandMenu(); + + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const openCreateActivity = useOpenCreateActivityDrawer({ activityObjectNameSingular: CoreObjectNameSingular.Task, @@ -146,8 +150,11 @@ export const PageChangeEffect = () => { } }, [isMatchingLocation, setHotkeyScope]); + const { nonSystemActiveObjectMetadataItems } = + useNonSystemActiveObjectMetadataItems(); + useEffect(() => { - setToInitialCommandMenu(); + setObjectsInCommandMenu(nonSystemActiveObjectMetadataItems); addToCommandMenu([ { @@ -162,7 +169,13 @@ export const PageChangeEffect = () => { }), }, ]); - }, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]); + }, [ + nonSystemActiveObjectMetadataItems, + addToCommandMenu, + setObjectsInCommandMenu, + openCreateActivity, + objectMetadataItems, + ]); useEffect(() => { setTimeout(() => { diff --git a/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx b/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx new file mode 100644 index 000000000..0ddb70ac1 --- /dev/null +++ b/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx @@ -0,0 +1,78 @@ +import { AppRouterProviders } from '@/app/components/AppRouterProviders'; +import { SettingsRoutes } from '@/app/components/SettingsRoutes'; +import { VerifyEffect } from '@/auth/components/VerifyEffect'; +import indexAppPath from '@/navigation/utils/indexAppPath'; +import { AppPath } from '@/types/AppPath'; +import { BlankLayout } from '@/ui/layout/page/BlankLayout'; +import { DefaultLayout } from '@/ui/layout/page/DefaultLayout'; +import { + createBrowserRouter, + createRoutesFromElements, + Route, +} from 'react-router-dom'; +import { Authorize } from '~/pages/auth/Authorize'; +import { Invite } from '~/pages/auth/Invite'; +import { PasswordReset } from '~/pages/auth/PasswordReset'; +import { SignInUp } from '~/pages/auth/SignInUp'; +import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect'; +import { NotFound } from '~/pages/not-found/NotFound'; +import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage'; +import { RecordShowPage } from '~/pages/object-record/RecordShowPage'; +import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan'; +import { CreateProfile } from '~/pages/onboarding/CreateProfile'; +import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace'; +import { InviteTeam } from '~/pages/onboarding/InviteTeam'; +import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess'; +import { SyncEmails } from '~/pages/onboarding/SyncEmails'; + +export const createAppRouter = ( + isBillingEnabled?: boolean, + isCRMMigrationEnabled?: boolean, + isServerlessFunctionSettingsEnabled?: boolean, +) => + createBrowserRouter( + createRoutesFromElements( + } + // To switch state to `loading` temporarily to enable us + // to set scroll position before the page is rendered + loader={async () => Promise.resolve(null)} + > + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + + } + /> + } /> + + }> + } /> + + , + ), + ); diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index 16c8bf7f4..a7e1dc95e 100644 --- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryObj } from '@storybook/react'; import { expect, userEvent, within } from '@storybook/test'; import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { IconCheckbox, IconNotes } from 'twenty-ui'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -20,6 +20,7 @@ import { } from '~/testing/mock-data/users'; import { sleep } from '~/utils/sleep'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CommandMenu } from '../CommandMenu'; const companiesMock = getCompaniesMock(); @@ -35,14 +36,21 @@ const meta: Meta = { const setCurrentWorkspaceMember = useSetRecoilState( currentWorkspaceMemberState, ); - const { addToCommandMenu, setToInitialCommandMenu, openCommandMenu } = + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const { addToCommandMenu, setObjectsInCommandMenu, openCommandMenu } = useCommandMenu(); setCurrentWorkspace(mockDefaultWorkspace); setCurrentWorkspaceMember(mockedWorkspaceMemberData); useEffect(() => { - setToInitialCommandMenu(); + const nonSystemActiveObjects = objectMetadataItems.filter( + (object) => !object.isSystem && object.isActive, + ); + + setObjectsInCommandMenu(nonSystemActiveObjects); + addToCommandMenu([ { id: 'create-task', @@ -62,7 +70,12 @@ const meta: Meta = { }, ]); openCommandMenu(); - }, [addToCommandMenu, setToInitialCommandMenu, openCommandMenu]); + }, [ + addToCommandMenu, + setObjectsInCommandMenu, + openCommandMenu, + objectMetadataItems, + ]); return ; }, diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts index 3c7f03168..711fbff88 100644 --- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts +++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts @@ -8,8 +8,8 @@ import { import { Command, CommandType } from '../types/Command'; -export const COMMAND_MENU_COMMANDS: Command[] = [ - { +export const COMMAND_MENU_COMMANDS: { [key: string]: Command } = { + people: { id: 'go-to-people', to: '/objects/people', label: 'Go to People', @@ -18,7 +18,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'P', Icon: IconUser, }, - { + companies: { id: 'go-to-companies', to: '/objects/companies', label: 'Go to Companies', @@ -27,7 +27,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'C', Icon: IconBuildingSkyscraper, }, - { + opportunities: { id: 'go-to-activities', to: '/objects/opportunities', label: 'Go to Opportunities', @@ -36,7 +36,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'O', Icon: IconTargetArrow, }, - { + settings: { id: 'go-to-settings', to: '/settings/profile', label: 'Go to Settings', @@ -45,7 +45,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'S', Icon: IconSettings, }, - { + tasks: { id: 'go-to-tasks', to: '/objects/tasks', label: 'Go to Tasks', @@ -54,4 +54,4 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'T', Icon: IconCheckbox, }, -]; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx index e1ee55013..b0502e583 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx @@ -1,6 +1,6 @@ +import { renderHook } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { MemoryRouter } from 'react-router-dom'; -import { renderHook } from '@testing-library/react'; import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; @@ -107,13 +107,39 @@ describe('useCommandMenu', () => { expect(onClickMock).toHaveBeenCalledTimes(1); }); - it('should setToInitialCommandMenu command menu', () => { + it('should setObjectsInCommandMenu command menu', () => { const { result } = renderHooks(); act(() => { - result.current.commandMenu.setToInitialCommandMenu(); + result.current.commandMenu.setObjectsInCommandMenu([]); }); - expect(result.current.commandMenuCommands.length).toBe(5); + expect(result.current.commandMenuCommands.length).toBe(1); + + act(() => { + result.current.commandMenu.setObjectsInCommandMenu([ + { + id: 'b88745ce-9021-4316-a018-8884e02d05ca', + nameSingular: 'task', + namePlural: 'tasks', + labelSingular: 'Task', + labelPlural: 'Tasks', + description: 'A task', + icon: 'IconCheckbox', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: '2024-09-12T20:23:46.041Z', + updatedAt: '2024-09-13T08:36:53.426Z', + labelIdentifierFieldMetadataId: + 'ab7901eb-43e1-4dc7-8f3b-cdee2857eb9a', + imageIdentifierFieldMetadataId: null, + fields: [], + }, + ]); + }); + + expect(result.current.commandMenuCommands.length).toBe(2); }); }); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index 1a8e08506..d19c314a1 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -1,6 +1,6 @@ +import { isNonEmptyString } from '@sniptt/guards'; import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { isNonEmptyString } from '@sniptt/guards'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; @@ -9,10 +9,13 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { isDefined } from '~/utils/isDefined'; -import { COMMAND_MENU_COMMANDS } from '../constants/CommandMenuCommands'; +import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons'; +import { sortByProperty } from '~/utils/array/sortByProperty'; import { commandMenuCommandsState } from '../states/commandMenuCommandsState'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; -import { Command } from '../types/Command'; +import { Command, CommandType } from '../types/Command'; export const useCommandMenu = () => { const navigate = useNavigate(); @@ -70,8 +73,27 @@ export const useCommandMenu = () => { [setCommands], ); - const setToInitialCommandMenu = () => { - setCommands(COMMAND_MENU_COMMANDS); + const setObjectsInCommandMenu = (menuItems: ObjectMetadataItem[]) => { + const formattedItems = [ + ...[ + ...menuItems.map( + (item) => + ({ + id: item.id, + to: `/objects/${item.namePlural}`, + label: `Go to ${item.labelPlural}`, + type: CommandType.Navigate, + firstHotKey: 'G', + secondHotKey: item.labelPlural[0], + Icon: ALL_ICONS[ + (item?.icon as keyof typeof ALL_ICONS) ?? 'IconArrowUpRight' + ], + }) as Command, + ), + ].sort(sortByProperty('label', 'asc')), + COMMAND_MENU_COMMANDS.settings, + ]; + setCommands(formattedItems); }; const onItemClick = useCallback( @@ -96,6 +118,6 @@ export const useCommandMenu = () => { toggleCommandMenu, addToCommandMenu, onItemClick, - setToInitialCommandMenu, + setObjectsInCommandMenu, }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts new file mode 100644 index 000000000..a33b80e1d --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts @@ -0,0 +1,20 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; + +export const useNonSystemActiveObjectMetadataItems = () => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const nonSystemActiveObjectMetadataItems = useMemo( + () => + objectMetadataItems.filter( + (objectMetadataItem) => + !objectMetadataItem.isSystem && objectMetadataItem.isActive, + ), + [objectMetadataItems], + ); + + return { + nonSystemActiveObjectMetadataItems, + }; +}; diff --git a/packages/twenty-front/src/utils/array/sortByProperty.ts b/packages/twenty-front/src/utils/array/sortByProperty.ts new file mode 100644 index 000000000..7cdb9b3b4 --- /dev/null +++ b/packages/twenty-front/src/utils/array/sortByProperty.ts @@ -0,0 +1,18 @@ +export const sortByProperty = + (propertyName: K, sortBy: 'asc' | 'desc' = 'asc') => + (objectA: T, objectB: T) => { + const a = sortBy === 'asc' ? objectA : objectB; + const b = sortBy === 'asc' ? objectB : objectA; + + if (typeof a[propertyName] === 'string') { + return (a[propertyName] as string).localeCompare( + b[propertyName] as string, + ); + } else if (typeof a[propertyName] === 'number') { + return (a[propertyName] as number) - (b[propertyName] as number); + } else { + throw new Error( + 'Property type not supported in sortByProperty, only string and number are supported', + ); + } + };