feat(apps): let front components open a record in the side panel (#22140)
## 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
This commit is contained in:
@@ -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 |
|
||||
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
+195
-1
@@ -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({
|
||||
|
||||
+89
-6
@@ -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('');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+48
-6
@@ -13,12 +13,54 @@ export type NavigateFunction = <T extends AppPath>(
|
||||
options?: NavigateOptions,
|
||||
) => Promise<void>;
|
||||
|
||||
export type OpenSidePanelPageFunction = (params: {
|
||||
page: SidePanelPages;
|
||||
pageTitle: string;
|
||||
pageIcon?: string;
|
||||
shouldResetSearchState?: boolean;
|
||||
}) => Promise<void>;
|
||||
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<void>;
|
||||
|
||||
export type CommandConfirmationModalResult = 'confirm' | 'cancel';
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export type {
|
||||
OpenCommandConfirmationModalFunction,
|
||||
OpenCommandConfirmationModalHostFunction,
|
||||
OpenSidePanelPageFunction,
|
||||
OpenSidePanelPageParams,
|
||||
RequestAccessTokenRefreshFunction,
|
||||
UnmountFrontComponentFunction,
|
||||
UpdateProgressFunction,
|
||||
|
||||
Reference in New Issue
Block a user