8034c7725f
Reorganizes `twenty-ui`'s component organization to follow how the best
UI libraries (MUI, Mantine, Base UI, Polaris) structure their source,
now that the package has stabilized.
**Taxonomy** — dissolves the meaningless `components/` junk-drawer and
the 107-file `display/` mega-category. New domains/subpaths:
`data-display`, `typography`, `icon`, `surfaces`; `feedback` and
`layout` absorb the rest (banners/callout/info + placeholders →
feedback; modal/card → surfaces; motion + separators → layout).
**Per-component layout** — every component is now
`<domain>/<ComponentName>/<ComponentName>.tsx` with colocated
styles/stories/types, `internal/` for private helpers and `parts/` for
re-exported compound sub-parts. The redundant inner `/components/` is
gone. `icon` and `json-visualizer` are kept as cohesive subsystems.
**Also:** adds a tree-shakeable root barrel (`import { Button } from
'twenty-ui'`), the generator now owns `individual-entry.ts`, and a real
barrel-leak bug is fixed (private `internals/` parts were leaking into
the public API).
Consumer imports (~1.2k files) and the `twenty-sdk` UI aggregator were
updated by codemod. The change is **export-neutral** except 16
intentionally-removed private internals symbols (all verified
unconsumed). Gates green: typecheck, lint, build, size-limit, storybook.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21745?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
218 lines
7.1 KiB
TypeScript
218 lines
7.1 KiB
TypeScript
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
|
import { isNonEmptyString } from '@sniptt/guards';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { useRef } from 'react';
|
|
import {
|
|
type FrontComponentExecutionContext,
|
|
type FrontComponentHostCommunicationApi,
|
|
} from 'twenty-front-component-renderer';
|
|
import { type AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types';
|
|
|
|
import { currentUserState } from '@/auth/states/currentUserState';
|
|
import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
|
|
import { useUnmountCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
|
|
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
|
|
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
|
|
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
|
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
|
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
|
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
|
|
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
|
import { useIcons } from 'twenty-ui/icon';
|
|
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
|
|
|
const FRONT_COMPONENT_CLIPBOARD_MAX_LENGTH = 64 * 1024;
|
|
const FRONT_COMPONENT_CLIPBOARD_RATE_LIMIT_MS = 1000;
|
|
const FRONT_COMPONENT_CLIPBOARD_PREVIEW_LENGTH = 30;
|
|
|
|
export const useFrontComponentExecutionContext = ({
|
|
frontComponentId,
|
|
commandMenuItemId,
|
|
selectedRecordIds,
|
|
colorScheme,
|
|
}: {
|
|
frontComponentId: string;
|
|
commandMenuItemId?: string;
|
|
selectedRecordIds?: string[];
|
|
colorScheme: 'light' | 'dark';
|
|
}): {
|
|
executionContext: FrontComponentExecutionContext;
|
|
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
|
|
} => {
|
|
const currentUser = useAtomStateValue(currentUserState);
|
|
const navigateApp = useNavigateApp();
|
|
const { requestAccessTokenRefresh } = useRequestApplicationTokenRefresh({
|
|
frontComponentId,
|
|
});
|
|
const { openConfirmationModal } = useCommandMenuConfirmationModal();
|
|
const { navigateSidePanel } = useNavigateSidePanel();
|
|
const setSidePanelSearch = useSetAtomState(sidePanelSearchState);
|
|
const { getIcon } = useIcons();
|
|
const unmountEngineCommand = useUnmountCommand();
|
|
const {
|
|
enqueueSuccessSnackBar,
|
|
enqueueErrorSnackBar,
|
|
enqueueInfoSnackBar,
|
|
enqueueWarningSnackBar,
|
|
} = useSnackBar();
|
|
const { closeSidePanelMenu } = useSidePanelMenu();
|
|
const { copyToClipboard: copyToClipboardWithSnackbar } = useCopyToClipboard();
|
|
const { t } = useLingui();
|
|
// oxlint-disable-next-line twenty/no-state-useref
|
|
const lastCopyToClipboardCallAtRef = useRef<number>(Number.NEGATIVE_INFINITY);
|
|
const setCommandMenuItemProgress = useSetAtomFamilyState(
|
|
commandMenuItemProgressFamilyState,
|
|
commandMenuItemId ?? '',
|
|
);
|
|
|
|
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 }) => {
|
|
navigateSidePanel({
|
|
page,
|
|
pageTitle,
|
|
pageIcon: getIcon(pageIcon),
|
|
});
|
|
|
|
if (shouldResetSearchState === true) {
|
|
setSidePanelSearch('');
|
|
}
|
|
};
|
|
|
|
const openCommandConfirmationModal: FrontComponentHostCommunicationApi['openCommandConfirmationModal'] =
|
|
async ({ title, subtitle, confirmButtonText, confirmButtonAccent }) => {
|
|
openConfirmationModal({
|
|
caller: { type: 'frontComponent', frontComponentId },
|
|
title,
|
|
subtitle,
|
|
confirmButtonText,
|
|
confirmButtonAccent,
|
|
});
|
|
};
|
|
|
|
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,
|
|
recordId: selectedRecordIds?.length === 1 ? selectedRecordIds[0] : null,
|
|
selectedRecordIds: selectedRecordIds ?? [],
|
|
colorScheme,
|
|
};
|
|
|
|
const unmountFrontComponent: FrontComponentHostCommunicationApi['unmountFrontComponent'] =
|
|
async () => {
|
|
if (isDefined(commandMenuItemId)) {
|
|
unmountEngineCommand(commandMenuItemId);
|
|
}
|
|
};
|
|
|
|
const closeSidePanel: FrontComponentHostCommunicationApi['closeSidePanel'] =
|
|
async () => {
|
|
closeSidePanelMenu();
|
|
};
|
|
|
|
const updateProgress: FrontComponentHostCommunicationApi['updateProgress'] =
|
|
async (progress) => {
|
|
if (!isDefined(commandMenuItemId)) {
|
|
return;
|
|
}
|
|
|
|
setCommandMenuItemProgress(Math.max(0, Math.min(100, progress)));
|
|
};
|
|
|
|
const copyToClipboard: FrontComponentHostCommunicationApi['copyToClipboard'] =
|
|
async (text) => {
|
|
if (!isNonEmptyString(text)) {
|
|
return;
|
|
}
|
|
|
|
if (text.length > FRONT_COMPONENT_CLIPBOARD_MAX_LENGTH) {
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
if (
|
|
now - lastCopyToClipboardCallAtRef.current <
|
|
FRONT_COMPONENT_CLIPBOARD_RATE_LIMIT_MS
|
|
) {
|
|
return;
|
|
}
|
|
lastCopyToClipboardCallAtRef.current = now;
|
|
|
|
const preview =
|
|
text.length > FRONT_COMPONENT_CLIPBOARD_PREVIEW_LENGTH
|
|
? `${text.slice(0, FRONT_COMPONENT_CLIPBOARD_PREVIEW_LENGTH)}…`
|
|
: text;
|
|
|
|
await copyToClipboardWithSnackbar(
|
|
text,
|
|
t`Application copied "${preview}" to your clipboard`,
|
|
);
|
|
};
|
|
|
|
const frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi =
|
|
{
|
|
navigate,
|
|
requestAccessTokenRefresh,
|
|
openSidePanelPage,
|
|
openCommandConfirmationModal,
|
|
enqueueSnackbar,
|
|
unmountFrontComponent,
|
|
closeSidePanel,
|
|
updateProgress,
|
|
copyToClipboard,
|
|
};
|
|
|
|
return {
|
|
executionContext,
|
|
frontComponentHostCommunicationApi,
|
|
};
|
|
};
|