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:
---------
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',
+ );
+ }
+ };