From 5242ddf458c73567f3533561223e86fcb43a086e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 25 Jun 2026 10:47:39 +0200 Subject: [PATCH] feat(apps): let front components open a record in the side panel (#22140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Front components (apps) could `navigate()` to a record's **full page**, but there was no way to open a specific record in the **side panel**. More generally, `openSidePanelPage` could navigate to a `SidePanelPages` enum page but couldn't pass the context most pages need. ## What `openSidePanelPage`'s params are now a **discriminated union keyed on `page`**, so each page declares its own typed payload (instead of a flat bag of optionals whose validity silently depends on `page`). This is also safer: pages that can't render without context can't be "opened" into a broken panel. Wired the param-bearing pages host-side, each bridging to its existing internal hook: | `page` | Params | Bridges to | |---|---|---| | `ViewRecord` | `recordId`, `objectNameSingular`, `resetNavigationStack?` | `useOpenRecordInSidePanel` (full-page fallback on mobile / unsupported objects) | | `EditRichText` | `recordId`, `objectNameSingular`, `fieldName?` | `useOpenRichTextInSidePanel` | | `ComposeEmail` | `connectedAccountId`, `threadId?`, `defaultTo?`, `defaultSubject?`, `defaultInReplyTo?`, `pageTitle?`, `pageIcon?` | `useOpenComposeEmailInSidePanel` | | `ViewFrontComponent` | `frontComponentId`, optional `recordId`+`objectNameSingular`, `pageTitle`, `pageIcon?`, `resetNavigationStack?` | `useOpenFrontComponentInSidePanel` | | *(any other page)* | `pageTitle`, `pageIcon?`, `shouldResetSearchState?` | `navigateSidePanel` | `CommandOpenSidePanelPage` now takes the union directly, so headless command-menu items can open any of these. Threaded through `twenty-sdk` → `twenty-front-component-renderer` → host (`useFrontComponentExecutionContext`), with unit tests per page and the mobile/unsupported fallbacks. ## Deliberately deferred: `MergeRecords` `useOpenMergeRecordsPageInSidePanel` takes `objectNameSingular` / `objectRecordIds` at **hook-init** (it calls `useObjectMetadataItem` / `useLazyFindManyRecords` at render), so it can't be driven by runtime app params without refactoring that hook + its current caller. Left out of this PR — better as its own change. ## Worth a second look (reviewers) - **`ViewFrontComponent`** lets an app open a front component by id. Within an app that's clean composition; whether an app should be able to target *another* app's component is a scoping/security question. The render still runs under the app's access token, so cross-app fetches would fail auth — but flagging it explicitly. ## Security note Side-panel record/page views render natively under the **user's** session/Apollo client, not the app's scoped token — RLS/field permissions are enforced as if the user opened it themselves. Same trust model as `navigate(AppPath.RecordShowPage, …)`. ## Follow-up A separate PR will centralize the mobile + `canOpenObjectInSidePanel` guard inside `useOpenRecordInSidePanel` (currently duplicated across callers, missing in others). ## Validation > [!NOTE] > Dependencies wouldn't install in this environment (flaky network during `yarn install`), so lint / typecheck / jest weren't run locally — relying on CI. The diff was reviewed manually for type-consistency, including the discriminated-union narrowing in the host switch. https://claude.ai/code/session_01AAJFXzsCeoj6BeP3ofiTKQ --- .../extend/apps/layout/front-components.mdx | 4 +- ...st-api-side-panel-open.front-component.tsx | 3 +- ...useFrontComponentExecutionContext.test.tsx | 196 +++++++++++++++++- .../useFrontComponentExecutionContext.ts | 95 ++++++++- .../command/CommandOpenSidePanelPage.tsx | 40 +--- .../frontComponentHostCommunicationApi.ts | 54 ++++- .../src/sdk/front-component/index.ts | 1 + 7 files changed, 344 insertions(+), 49 deletions(-) diff --git a/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx index ee4ecd240b..37124078b9 100644 --- a/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx +++ b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx @@ -121,7 +121,7 @@ Import them from `twenty-sdk/command`: - **`Command`** — Runs an async callback via the `execute` prop. - **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`. - **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. -- **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`. +- **`CommandOpenSidePanelPage`** — Opens a side panel page. Props depend on `page` — e.g. `ViewRecord` takes `recordId` + `objectNameSingular`, other pages take `pageTitle` + `pageIcon`. Here is a full example of a headless front component using `Command` to run an action from the command menu: @@ -381,7 +381,7 @@ The following system variables are always available via `process.env`: | Variable | Description | |----------|-------------| -| `TWENTY_FUNCTIONS_URL` | Base URL your app's HTTP logic functions are served from (used by `RestApiClient`) | +| `TWENTY_FUNCTIONS_URL` | Base URL your app's HTTP logic functions are served from | | `TWENTY_API_URL` | Base URL of the Twenty core API | | `TWENTY_APP_ACCESS_TOKEN` | Short-lived token scoped to your app's role | diff --git a/packages/twenty-front-component-renderer/src/__stories__/host-api/host-api-side-panel-open.front-component.tsx b/packages/twenty-front-component-renderer/src/__stories__/host-api/host-api-side-panel-open.front-component.tsx index d192a91f6c..daaae4b375 100644 --- a/packages/twenty-front-component-renderer/src/__stories__/host-api/host-api-side-panel-open.front-component.tsx +++ b/packages/twenty-front-component-renderer/src/__stories__/host-api/host-api-side-panel-open.front-component.tsx @@ -18,7 +18,8 @@ const HostApiSidePanelOpenFrontComponent = () => { try { await openSidePanelPage({ page: SidePanelPages.ViewRecord, - pageTitle: 'Test Record', + recordId: 'test-record-id', + objectNameSingular: 'company', }); setStatus('sidePanel:success'); } catch (error) { diff --git a/packages/twenty-front/src/modules/front-components/hooks/__tests__/useFrontComponentExecutionContext.test.tsx b/packages/twenty-front/src/modules/front-components/hooks/__tests__/useFrontComponentExecutionContext.test.tsx index d84432ccab..bad5168ef3 100644 --- a/packages/twenty-front/src/modules/front-components/hooks/__tests__/useFrontComponentExecutionContext.test.tsx +++ b/packages/twenty-front/src/modules/front-components/hooks/__tests__/useFrontComponentExecutionContext.test.tsx @@ -2,7 +2,7 @@ import { i18n } from '@lingui/core'; import { I18nProvider } from '@lingui/react'; import { act, renderHook } from '@testing-library/react'; import { getDefaultStore } from 'jotai'; -import { AppPath } from 'twenty-shared/types'; +import { AppPath, SidePanelPages } from 'twenty-shared/types'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState'; @@ -12,6 +12,10 @@ const mockNavigateApp = jest.fn(); const mockRequestAccessTokenRefresh = jest.fn(); const mockOpenConfirmationModal = jest.fn(); const mockNavigateSidePanel = jest.fn(); +const mockOpenRecordInSidePanel = jest.fn(); +const mockOpenRichTextInSidePanel = jest.fn(); +const mockOpenComposeEmailInSidePanel = jest.fn(); +const mockOpenFrontComponentInSidePanel = jest.fn(); const mockSetSidePanelSearch = jest.fn(); const mockGetIcon = jest.fn((name: string) => `icon-${name}`); const mockUnmountEngineCommand = jest.fn(); @@ -24,6 +28,7 @@ const mockSetCommandMenuItemProgress = jest.fn(); const mockCopyToClipboard = jest.fn(); let mockCurrentUser: { id: string } | null = { id: 'user-123' }; +let mockIsMobile = false; jest.mock('~/hooks/useNavigateApp', () => ({ useNavigateApp: () => mockNavigateApp, @@ -50,6 +55,30 @@ jest.mock('@/side-panel/hooks/useNavigateSidePanel', () => ({ }), })); +jest.mock('@/side-panel/hooks/useOpenRecordInSidePanel', () => ({ + useOpenRecordInSidePanel: () => ({ + openRecordInSidePanel: mockOpenRecordInSidePanel, + }), +})); + +jest.mock('@/side-panel/hooks/useOpenRichTextInSidePanel', () => ({ + useOpenRichTextInSidePanel: () => ({ + openRichTextInSidePanel: mockOpenRichTextInSidePanel, + }), +})); + +jest.mock('@/side-panel/hooks/useOpenComposeEmailInSidePanel', () => ({ + useOpenComposeEmailInSidePanel: () => ({ + openComposeEmailInSidePanel: mockOpenComposeEmailInSidePanel, + }), +})); + +jest.mock('@/side-panel/hooks/useOpenFrontComponentInSidePanel', () => ({ + useOpenFrontComponentInSidePanel: () => ({ + openFrontComponentInSidePanel: mockOpenFrontComponentInSidePanel, + }), +})); + jest.mock( '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand', () => ({ @@ -78,6 +107,10 @@ jest.mock('twenty-ui/icon', () => ({ }), })); +jest.mock('twenty-ui/utilities', () => ({ + useIsMobile: () => mockIsMobile, +})); + jest.mock('@/ui/utilities/state/jotai/hooks/useAtomStateValue', () => ({ useAtomStateValue: () => mockCurrentUser, })); @@ -130,6 +163,7 @@ describe('useFrontComponentExecutionContext', () => { beforeEach(() => { jest.clearAllMocks(); mockCurrentUser = { id: 'user-123' }; + mockIsMobile = false; getDefaultStore().set(parentViewAtom, undefined); }); @@ -334,6 +368,166 @@ describe('useFrontComponentExecutionContext', () => { }); }); + describe('openSidePanelPage with a record context', () => { + it('should open the record in the side panel when the object is supported', async () => { + const { result } = renderUseFrontComponentExecutionContext({ + frontComponentId: FRONT_COMPONENT_ID, + }); + + await act(async () => { + await result.current.frontComponentHostCommunicationApi.openSidePanelPage( + { + page: SidePanelPages.ViewRecord, + recordId: 'lead-1', + objectNameSingular: 'lead', + resetNavigationStack: true, + }, + ); + }); + + expect(mockOpenRecordInSidePanel).toHaveBeenCalledWith({ + recordId: 'lead-1', + objectNameSingular: 'lead', + resetNavigationStack: true, + }); + expect(mockNavigateApp).not.toHaveBeenCalled(); + expect(mockNavigateSidePanel).not.toHaveBeenCalled(); + }); + + it('should fall back to full-page navigation on mobile', async () => { + mockIsMobile = true; + + const { result } = renderUseFrontComponentExecutionContext({ + frontComponentId: FRONT_COMPONENT_ID, + }); + + await act(async () => { + await result.current.frontComponentHostCommunicationApi.openSidePanelPage( + { + page: SidePanelPages.ViewRecord, + recordId: 'lead-1', + objectNameSingular: 'lead', + }, + ); + }); + + expect(mockNavigateApp).toHaveBeenCalledWith( + AppPath.RecordShowPage, + { objectNameSingular: 'lead', objectRecordId: 'lead-1' }, + undefined, + undefined, + ); + expect(mockOpenRecordInSidePanel).not.toHaveBeenCalled(); + }); + + it('should fall back to full-page navigation when the object cannot open in the side panel', async () => { + const { result } = renderUseFrontComponentExecutionContext({ + frontComponentId: FRONT_COMPONENT_ID, + }); + + await act(async () => { + await result.current.frontComponentHostCommunicationApi.openSidePanelPage( + { + page: SidePanelPages.ViewRecord, + recordId: 'workflow-1', + objectNameSingular: 'workflow', + }, + ); + }); + + expect(mockNavigateApp).toHaveBeenCalledWith( + AppPath.RecordShowPage, + { objectNameSingular: 'workflow', objectRecordId: 'workflow-1' }, + undefined, + undefined, + ); + expect(mockOpenRecordInSidePanel).not.toHaveBeenCalled(); + }); + }); + + describe('openSidePanelPage with EditRichText', () => { + it('should open the rich text editor for a record field', async () => { + const { result } = renderUseFrontComponentExecutionContext({ + frontComponentId: FRONT_COMPONENT_ID, + }); + + await act(async () => { + await result.current.frontComponentHostCommunicationApi.openSidePanelPage( + { + page: SidePanelPages.EditRichText, + recordId: 'note-1', + objectNameSingular: 'note', + fieldName: 'body', + }, + ); + }); + + expect(mockOpenRichTextInSidePanel).toHaveBeenCalledWith( + 'note-1', + 'note', + 'body', + ); + }); + }); + + describe('openSidePanelPage with ComposeEmail', () => { + it('should open the email composer with the provided params', async () => { + const { result } = renderUseFrontComponentExecutionContext({ + frontComponentId: FRONT_COMPONENT_ID, + }); + + await act(async () => { + await result.current.frontComponentHostCommunicationApi.openSidePanelPage( + { + page: SidePanelPages.ComposeEmail, + connectedAccountId: 'account-1', + defaultTo: 'lead@example.com', + pageIcon: 'IconMail', + }, + ); + }); + + expect(mockOpenComposeEmailInSidePanel).toHaveBeenCalledWith({ + connectedAccountId: 'account-1', + threadId: undefined, + defaultTo: 'lead@example.com', + defaultSubject: undefined, + defaultInReplyTo: undefined, + pageTitle: undefined, + pageIcon: 'icon-IconMail', + }); + }); + }); + + describe('openSidePanelPage with ViewFrontComponent', () => { + it('should open a front component with optional record context', async () => { + const { result } = renderUseFrontComponentExecutionContext({ + frontComponentId: FRONT_COMPONENT_ID, + }); + + await act(async () => { + await result.current.frontComponentHostCommunicationApi.openSidePanelPage( + { + page: SidePanelPages.ViewFrontComponent, + frontComponentId: 'fc-1', + pageTitle: 'My Component', + pageIcon: 'IconBolt', + recordId: 'lead-1', + objectNameSingular: 'lead', + }, + ); + }); + + expect(mockOpenFrontComponentInSidePanel).toHaveBeenCalledWith({ + frontComponentId: 'fc-1', + pageTitle: 'My Component', + pageIcon: 'icon-IconBolt', + resetNavigationStack: undefined, + recordContext: { recordId: 'lead-1', objectNameSingular: 'lead' }, + }); + }); + }); + describe('openCommandConfirmationModal', () => { it('should call openConfirmationModal with frontComponent caller', async () => { const { result } = renderUseFrontComponentExecutionContext({ diff --git a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts index 3f7a4c6bb2..236c60beda 100644 --- a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts +++ b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts @@ -6,7 +6,11 @@ import { type FrontComponentExecutionContext, type FrontComponentHostCommunicationApi, } from 'twenty-front-component-renderer'; -import { AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types'; +import { + AppPath, + SidePanelPages, + type EnqueueSnackbarParams, +} from 'twenty-shared/types'; import { currentUserState } from '@/auth/states/currentUserState'; import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal'; @@ -15,7 +19,12 @@ import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/c import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState'; import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh'; +import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel'; import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; +import { useOpenComposeEmailInSidePanel } from '@/side-panel/hooks/useOpenComposeEmailInSidePanel'; +import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel'; +import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel'; +import { useOpenRichTextInSidePanel } from '@/side-panel/hooks/useOpenRichTextInSidePanel'; import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -24,6 +33,7 @@ import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAt import { useStore } from 'jotai'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/icon'; +import { useIsMobile } from 'twenty-ui/utilities'; import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; import { useNavigateApp } from '~/hooks/useNavigateApp'; @@ -53,6 +63,12 @@ export const useFrontComponentExecutionContext = ({ }); const { openConfirmationModal } = useCommandMenuConfirmationModal(); const { navigateSidePanel } = useNavigateSidePanel(); + const { openRecordInSidePanel: openRecordInSidePanelInternal } = + useOpenRecordInSidePanel(); + const { openRichTextInSidePanel } = useOpenRichTextInSidePanel(); + const { openComposeEmailInSidePanel } = useOpenComposeEmailInSidePanel(); + const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel(); + const isMobile = useIsMobile(); const setSidePanelSearch = useSetAtomState(sidePanelSearchState); const { getIcon } = useIcons(); const unmountEngineCommand = useUnmountCommand(); @@ -108,14 +124,81 @@ export const useFrontComponentExecutionContext = ({ }; const openSidePanelPage: FrontComponentHostCommunicationApi['openSidePanelPage'] = - async ({ page, pageTitle, pageIcon, shouldResetSearchState }) => { + async (params) => { + if (params.page === SidePanelPages.ViewRecord) { + const { recordId, objectNameSingular, resetNavigationStack } = params; + + if (isMobile || !canOpenObjectInSidePanel(objectNameSingular)) { + await navigate(AppPath.RecordShowPage, { + objectNameSingular, + objectRecordId: recordId, + }); + + return; + } + + openRecordInSidePanelInternal({ + recordId, + objectNameSingular, + resetNavigationStack, + }); + + return; + } + + if (params.page === SidePanelPages.EditRichText) { + openRichTextInSidePanel( + params.recordId, + params.objectNameSingular, + params.fieldName, + ); + + return; + } + + if (params.page === SidePanelPages.ComposeEmail) { + openComposeEmailInSidePanel({ + connectedAccountId: params.connectedAccountId, + threadId: params.threadId, + defaultTo: params.defaultTo, + defaultSubject: params.defaultSubject, + defaultInReplyTo: params.defaultInReplyTo, + pageTitle: params.pageTitle, + pageIcon: isDefined(params.pageIcon) + ? getIcon(params.pageIcon) + : undefined, + }); + + return; + } + + if (params.page === SidePanelPages.ViewFrontComponent) { + const recordContext = + isDefined(params.recordId) && isDefined(params.objectNameSingular) + ? { + recordId: params.recordId, + objectNameSingular: params.objectNameSingular, + } + : undefined; + + openFrontComponentInSidePanel({ + frontComponentId: params.frontComponentId, + pageTitle: params.pageTitle, + pageIcon: getIcon(params.pageIcon), + resetNavigationStack: params.resetNavigationStack, + recordContext, + }); + + return; + } + navigateSidePanel({ - page, - pageTitle, - pageIcon: getIcon(pageIcon), + page: params.page, + pageTitle: params.pageTitle, + pageIcon: getIcon(params.pageIcon), }); - if (shouldResetSearchState === true) { + if (params.shouldResetSearchState === true) { setSidePanelSearch(''); } }; diff --git a/packages/twenty-sdk/src/sdk/front-component/command/CommandOpenSidePanelPage.tsx b/packages/twenty-sdk/src/sdk/front-component/command/CommandOpenSidePanelPage.tsx index 6295e21e5c..f0c0b7ea14 100644 --- a/packages/twenty-sdk/src/sdk/front-component/command/CommandOpenSidePanelPage.tsx +++ b/packages/twenty-sdk/src/sdk/front-component/command/CommandOpenSidePanelPage.tsx @@ -1,27 +1,16 @@ import { openSidePanelPage, + type OpenSidePanelPageParams, unmountFrontComponent, useFrontComponentId, } from '@/sdk/front-component'; import { useEffect, useState } from 'react'; -import { type SidePanelPages } from 'twenty-shared/types'; +export type CommandOpenSidePanelPageProps = OpenSidePanelPageParams; -export type CommandOpenSidePanelPageProps = { - page: SidePanelPages; - pageTitle: string; - pageIcon: string; - onClick?: () => void; - shouldResetSearchState?: boolean; -}; - -export const CommandOpenSidePanelPage = ({ - page, - pageTitle, - pageIcon, - onClick, - shouldResetSearchState = false, -}: CommandOpenSidePanelPageProps) => { +export const CommandOpenSidePanelPage = ( + props: CommandOpenSidePanelPageProps, +) => { const [hasExecuted, setHasExecuted] = useState(false); const frontComponentId = useFrontComponentId(); @@ -34,28 +23,13 @@ export const CommandOpenSidePanelPage = ({ setHasExecuted(true); const run = async () => { - onClick?.(); - - await openSidePanelPage({ - page, - pageTitle, - pageIcon, - shouldResetSearchState, - }); + await openSidePanelPage(props); await unmountFrontComponent(); }; run(); - }, [ - page, - pageTitle, - pageIcon, - shouldResetSearchState, - onClick, - hasExecuted, - frontComponentId, - ]); + }, [props, hasExecuted, frontComponentId]); return null; }; diff --git a/packages/twenty-sdk/src/sdk/front-component/globals/frontComponentHostCommunicationApi.ts b/packages/twenty-sdk/src/sdk/front-component/globals/frontComponentHostCommunicationApi.ts index 2f0d7a40d5..7472752e00 100644 --- a/packages/twenty-sdk/src/sdk/front-component/globals/frontComponentHostCommunicationApi.ts +++ b/packages/twenty-sdk/src/sdk/front-component/globals/frontComponentHostCommunicationApi.ts @@ -13,12 +13,54 @@ export type NavigateFunction = ( options?: NavigateOptions, ) => Promise; -export type OpenSidePanelPageFunction = (params: { - page: SidePanelPages; - pageTitle: string; - pageIcon?: string; - shouldResetSearchState?: boolean; -}) => Promise; +export type OpenSidePanelPageParams = + | { + page: SidePanelPages.ViewRecord; + recordId: string; + objectNameSingular: string; + resetNavigationStack?: boolean; + } + | { + page: SidePanelPages.EditRichText; + recordId: string; + objectNameSingular: string; + fieldName?: string; + } + | { + page: SidePanelPages.ComposeEmail; + connectedAccountId: string; + threadId?: string; + defaultTo?: string; + defaultSubject?: string; + defaultInReplyTo?: string; + pageTitle?: string; + pageIcon?: string; + } + | { + page: SidePanelPages.ViewFrontComponent; + frontComponentId: string; + recordId?: string; + objectNameSingular?: string; + pageTitle: string; + pageIcon?: string; + resetNavigationStack?: boolean; + } + | { + page: Exclude< + SidePanelPages, + | SidePanelPages.ViewRecord + | SidePanelPages.EditRichText + | SidePanelPages.ComposeEmail + | SidePanelPages.ViewFrontComponent + >; + pageTitle: string; + pageIcon?: string; + shouldResetSearchState?: boolean; + }; + +export type OpenSidePanelPageFunction = ( + params: OpenSidePanelPageParams, +) => Promise; export type CommandConfirmationModalResult = 'confirm' | 'cancel'; diff --git a/packages/twenty-sdk/src/sdk/front-component/index.ts b/packages/twenty-sdk/src/sdk/front-component/index.ts index c03ac6da7b..3e708f1887 100644 --- a/packages/twenty-sdk/src/sdk/front-component/index.ts +++ b/packages/twenty-sdk/src/sdk/front-component/index.ts @@ -25,6 +25,7 @@ export type { OpenCommandConfirmationModalFunction, OpenCommandConfirmationModalHostFunction, OpenSidePanelPageFunction, + OpenSidePanelPageParams, RequestAccessTokenRefreshFunction, UnmountFrontComponentFunction, UpdateProgressFunction,