Files
twenty/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts
T
Raphaël Bosi a0c0e7de25 [FRONT COMPONENTS] Add snackbar to the API (#18136)
## PR description

- Adds `enqueueSnackbar` to the front component host communication API,
allowing front components to display snack bar notifications (success,
error, info, warning) to the user.
- Introduces `frontComponentId` to the `FrontComponentExecutionContext`
and a `useFrontComponentId` hook, enabling front components to identify
themselves (used for dedupe keys).
- Wires error/success notifications into the `Action`, `ActionLink`, and
`ActionOpenSidePanelPage` SDK components -> actions now catch errors and
display them as snack bars, and `Action` supports an optional
`notifyOnEnd` prop for success feedback.

## Video QA

### Success example


https://github.com/user-attachments/assets/8cc53d31-d9eb-49a8-9220-f7866ec1b415

### Error example


https://github.com/user-attachments/assets/a37d65b8-0b5f-4adb-bcdb-571bb2b997c3

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-23 12:25:05 +01:00

129 lines
3.9 KiB
TypeScript

import { useSetRecoilState } from 'recoil';
import {
type FrontComponentExecutionContext,
type FrontComponentHostCommunicationApi,
} from 'twenty-sdk/front-component-renderer';
import { type AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types';
import { currentUserState } from '@/auth/states/currentUserState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { assertUnreachable } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useFrontComponentExecutionContext = ({
frontComponentId,
}: {
frontComponentId: string;
}): {
executionContext: FrontComponentExecutionContext;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
} => {
const currentUser = useRecoilValueV2(currentUserState);
const navigateApp = useNavigateApp();
const { navigateCommandMenu } = useNavigateCommandMenu();
const setCommandMenuSearchState = useSetRecoilState(commandMenuSearchState);
const { getIcon } = useIcons();
const unmountHeadlessFrontComponent = useUnmountHeadlessFrontComponent();
const {
enqueueSuccessSnackBar,
enqueueErrorSnackBar,
enqueueInfoSnackBar,
enqueueWarningSnackBar,
} = useSnackBar();
const { closeCommandMenu } = useCommandMenu();
const navigate: FrontComponentHostCommunicationApi['navigate'] = async (
to,
params,
queryParams,
options,
) => {
navigateApp(
to as AppPath,
params as Parameters<typeof navigateApp>[1],
queryParams,
options,
);
};
const openSidePanelPage: FrontComponentHostCommunicationApi['openSidePanelPage'] =
async ({ page, pageTitle, pageIcon, shouldResetSearchState }) => {
navigateCommandMenu({
page,
pageTitle,
pageIcon: getIcon(pageIcon),
});
if (shouldResetSearchState === true) {
setCommandMenuSearchState('');
}
};
const enqueueSnackbar: FrontComponentHostCommunicationApi['enqueueSnackbar'] =
async ({
message,
variant,
duration,
detailedMessage,
dedupeKey,
}: EnqueueSnackbarParams) => {
const snackBarOptions = {
duration,
detailedMessage,
dedupeKey,
};
switch (variant) {
case 'error':
enqueueErrorSnackBar({ message, options: snackBarOptions });
break;
case 'info':
enqueueInfoSnackBar({ message, options: snackBarOptions });
break;
case 'warning':
enqueueWarningSnackBar({ message, options: snackBarOptions });
break;
case 'success':
enqueueSuccessSnackBar({ message, options: snackBarOptions });
break;
default:
assertUnreachable(variant);
}
};
const executionContext: FrontComponentExecutionContext = {
frontComponentId,
userId: currentUser?.id ?? null,
};
const unmountFrontComponent: FrontComponentHostCommunicationApi['unmountFrontComponent'] =
async () => {
unmountHeadlessFrontComponent(frontComponentId);
};
const closeSidePanel: FrontComponentHostCommunicationApi['closeSidePanel'] =
async () => {
closeCommandMenu();
};
const frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi =
{
navigate,
openSidePanelPage,
enqueueSnackbar,
unmountFrontComponent,
closeSidePanel,
};
return {
executionContext,
frontComponentHostCommunicationApi,
};
};