[Command menu items] Create engine commands (#18681)
## Description - Introduces a new engine command execution model that replaces the previous approach of mapping `EngineComponentKey` to React components. Instead, engine commands are now mounted headlessly via `HeadlessEngineCommandMountRoot`, with their execution context populated synchronously before mounting. - Creates new headless command components - Moves error handling from the SDK layer to the host app by wrapping all mounted commands with a new `CommandMenuItemErrorBoundary` The new flow works as follows: - When a command menu item with an `engineComponentKey` is clicked, `useCommandMenuItemFrontComponentCommands` calls `useMountEngineCommand`, which synchronously reads the current context store (object metadata, selected records, filters, view ID, etc.) and writes a `MountedEngineCommandContext` into `mountedEngineCommandsState`. - The command is then mounted into `mountedEngineCommandsState`, which triggers `HeadlessEngineCommandMountRoot` to render the corresponding headless component from `ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP`, wrapped in `CommandMenuItemErrorBoundary`, `ContextStoreComponentInstanceContext.Provider`, and `EngineCommandComponentInstanceContext.Provider`. - Each command component reads its execution context and delegates to one of the 4 execution patterns: `HeadlessEngineCommandWrapperEffect` (simple actions), `HeadlessConfirmationModalEngineCommandEffect` (destructive actions needing confirmation), `HeadlessNavigateEngineCommand` (GO_TO_* commands), or `HeadlessOpenSidePanelPageEngineCommand` (SEARCH_RECORDS, ASK_AI, VIEW_PREVIOUS_AI_CHATS). - After execution, the command self-unmounts via `useUnmountEngineCommand`, which removes the entry from `mountedEngineCommandsState` and stops rendering the component.
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
|
||||
import { CommandMenuConfirmationModalManager } from '@/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager';
|
||||
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
||||
import { CommandMenuConfirmationModalManager } from '@/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager';
|
||||
import { MinimalMetadataGater } from '@/metadata-store/components/MinimalMetadataGater';
|
||||
import { IsMinimalMetadataReadyEffect } from '@/metadata-store/effect-components/IsMinimalMetadataReadyEffect';
|
||||
|
||||
import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider';
|
||||
import { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect';
|
||||
import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect';
|
||||
import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
|
||||
import { AuthProvider } from '@/auth/components/AuthProvider';
|
||||
import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
|
||||
@@ -17,10 +15,13 @@ import { ClientConfigProviderEffect } from '@/client-config/components/ClientCon
|
||||
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
|
||||
import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffect';
|
||||
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
|
||||
import { HeadlessFrontComponentMountRoot } from '@/front-components/components/HeadlessFrontComponentMountRoot';
|
||||
import { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect';
|
||||
import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect';
|
||||
import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider';
|
||||
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
||||
|
||||
import { HeadlessEngineCommandMountRoot } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandMountRoot';
|
||||
import { HeadlessFrontComponentMountRoot } from '@/front-components/components/HeadlessFrontComponentMountRoot';
|
||||
import { SSEProvider } from '@/sse-db-event/components/SSEProvider';
|
||||
import { SupportChatEffect } from '@/support/components/SupportChatEffect';
|
||||
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
|
||||
@@ -74,6 +75,7 @@ export const AppRouterProviders = () => {
|
||||
<GlobalFilePreviewModal />
|
||||
<CommandMenuConfirmationModalManager />
|
||||
<HeadlessFrontComponentMountRoot />
|
||||
<HeadlessEngineCommandMountRoot />
|
||||
</StrictMode>
|
||||
</DialogManager>
|
||||
</DialogComponentInstanceContext.Provider>
|
||||
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
import { COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME } from '@/command-menu-item/confirmation-modal/constants/CommandMenuItemConfirmationModalResultBrowserEventName';
|
||||
import { COMMAND_MENU_CONFIRMATION_MODAL_INSTANCE_ID } from '@/command-menu-item/confirmation-modal/constants/CommandMenuItemConfirmationModalId';
|
||||
import { COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME } from '@/command-menu-item/confirmation-modal/constants/CommandMenuItemConfirmationModalResultBrowserEventName';
|
||||
import { commandMenuItemConfirmationModalConfigState } from '@/command-menu-item/confirmation-modal/states/commandMenuItemConfirmationModalState';
|
||||
import {
|
||||
type CommandMenuConfirmationModalResult,
|
||||
@@ -10,6 +10,7 @@ import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpe
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuConfirmationModalManager = () => {
|
||||
const commandMenuItemConfirmationModalConfig = useAtomStateValue(
|
||||
@@ -23,12 +24,12 @@ export const CommandMenuConfirmationModalManager = () => {
|
||||
commandMenuItemConfirmationModalConfigState,
|
||||
);
|
||||
|
||||
const callerId = commandMenuItemConfirmationModalConfig?.frontComponentId;
|
||||
const caller = commandMenuItemConfirmationModalConfig?.caller;
|
||||
|
||||
const emitConfirmationResult = (
|
||||
confirmationResult: CommandMenuConfirmationModalResult,
|
||||
) => {
|
||||
if (!callerId) {
|
||||
if (!isDefined(caller)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@ export const CommandMenuConfirmationModalManager = () => {
|
||||
COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
|
||||
{
|
||||
detail: {
|
||||
frontComponentId: callerId,
|
||||
caller,
|
||||
confirmationResult,
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export const useCommandMenuConfirmationModal = () => {
|
||||
isCommandMenuItemConfirmationModalOpened
|
||||
) {
|
||||
throw new Error(
|
||||
'Command menu item confirmation modal is already active for another front component',
|
||||
'Command menu item confirmation modal is already active for another caller',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { type ConfirmationModalCaller } from 'twenty-shared/types';
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
import { type ButtonAccent } from 'twenty-ui/input';
|
||||
|
||||
export type CommandMenuItemConfirmationModalConfig = {
|
||||
frontComponentId: string;
|
||||
caller: ConfirmationModalCaller;
|
||||
title: string;
|
||||
subtitle: ReactNode;
|
||||
confirmButtonText?: string;
|
||||
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
import { type ConfirmationModalCaller } from 'twenty-shared/types';
|
||||
|
||||
export type CommandMenuConfirmationModalResult = 'confirm' | 'cancel';
|
||||
|
||||
export type CommandMenuConfirmationModalResultBrowserEventDetail = {
|
||||
frontComponentId: string;
|
||||
caller: ConfirmationModalCaller;
|
||||
confirmationResult: CommandMenuConfirmationModalResult;
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
|
||||
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
|
||||
|
||||
export const CommandMenuItemComponent = ({
|
||||
action,
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { type ReactNode } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
type CommandMenuItemErrorBoundaryProps = {
|
||||
children: ReactNode;
|
||||
engineCommandId: string;
|
||||
shouldReportToSentry?: boolean;
|
||||
onError?: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuItemErrorBoundary = ({
|
||||
children,
|
||||
engineCommandId,
|
||||
shouldReportToSentry = false,
|
||||
onError,
|
||||
}: CommandMenuItemErrorBoundaryProps) => {
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const handleError = async (error: Error) => {
|
||||
enqueueErrorSnackBar({ message: error.message });
|
||||
|
||||
if (shouldReportToSentry) {
|
||||
try {
|
||||
const { captureException } = await import('@sentry/react');
|
||||
captureException(error, (scope) => {
|
||||
scope.setTag('component', 'CommandMenuItem');
|
||||
|
||||
return scope;
|
||||
});
|
||||
} catch {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error('Failed to capture exception with Sentry:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onError?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallbackRender={() => null}
|
||||
onError={handleError}
|
||||
resetKeys={[engineCommandId]}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { useIsHeadlessEngineCommandEffectInitialized } from '@/command-menu-item/engine-command/hooks/useIsHeadlessEngineCommandEffectInitialized';
|
||||
import { type ReactNode, useEffect } from 'react';
|
||||
|
||||
import { COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME } from '@/command-menu-item/confirmation-modal/constants/CommandMenuItemConfirmationModalResultBrowserEventName';
|
||||
import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
|
||||
import { type CommandMenuConfirmationModalResultBrowserEventDetail } from '@/command-menu-item/confirmation-modal/types/CommandMenuConfirmationModalResultBrowserEventDetail';
|
||||
import { useUnmountEngineCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
|
||||
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { type ButtonAccent } from 'twenty-ui/input';
|
||||
|
||||
export type HeadlessConfirmationModalEngineCommandEffectProps = {
|
||||
title: string;
|
||||
subtitle: ReactNode;
|
||||
confirmButtonText: string;
|
||||
confirmButtonAccent?: ButtonAccent;
|
||||
execute: () => void | Promise<unknown>;
|
||||
};
|
||||
|
||||
export const HeadlessConfirmationModalEngineCommandEffect = ({
|
||||
title,
|
||||
subtitle,
|
||||
confirmButtonText,
|
||||
confirmButtonAccent = 'danger',
|
||||
execute,
|
||||
}: HeadlessConfirmationModalEngineCommandEffectProps) => {
|
||||
const { isInitializedRef, setIsInitialized } =
|
||||
useIsHeadlessEngineCommandEffectInitialized();
|
||||
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
|
||||
EngineCommandComponentInstanceContext,
|
||||
);
|
||||
const unmountEngineCommand = useUnmountEngineCommand();
|
||||
const { openConfirmationModal } = useCommandMenuConfirmationModal();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitializedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
|
||||
openConfirmationModal({
|
||||
caller: { type: 'engineCommand', engineCommandId },
|
||||
title,
|
||||
subtitle,
|
||||
confirmButtonText,
|
||||
confirmButtonAccent,
|
||||
});
|
||||
}, [
|
||||
isInitializedRef,
|
||||
setIsInitialized,
|
||||
engineCommandId,
|
||||
openConfirmationModal,
|
||||
title,
|
||||
subtitle,
|
||||
confirmButtonText,
|
||||
confirmButtonAccent,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleConfirmationResult = async (event: Event) => {
|
||||
const customEvent =
|
||||
event as CustomEvent<CommandMenuConfirmationModalResultBrowserEventDetail>;
|
||||
|
||||
const caller = customEvent.detail.caller;
|
||||
|
||||
if (
|
||||
caller.type !== 'engineCommand' ||
|
||||
caller.engineCommandId !== engineCommandId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (customEvent.detail.confirmationResult === 'confirm') {
|
||||
await execute();
|
||||
}
|
||||
|
||||
unmountEngineCommand(engineCommandId);
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
|
||||
handleConfirmationResult,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
|
||||
handleConfirmationResult,
|
||||
);
|
||||
};
|
||||
}, [engineCommandId, execute, unmountEngineCommand]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { CommandMenuItemErrorBoundary } from '@/command-menu-item/display/components/CommandMenuItemErrorBoundary';
|
||||
import { ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP } from '@/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap';
|
||||
import { useUnmountEngineCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
|
||||
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
|
||||
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const HeadlessEngineCommandMountRoot = () => {
|
||||
const mountedEngineCommands = useAtomStateValue(mountedEngineCommandsState);
|
||||
const unmountEngineCommand = useUnmountEngineCommand();
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...mountedEngineCommands.entries()].map(
|
||||
([engineCommandId, mountContext]) => (
|
||||
<CommandMenuItemErrorBoundary
|
||||
key={engineCommandId}
|
||||
engineCommandId={engineCommandId}
|
||||
shouldReportToSentry
|
||||
onError={() => unmountEngineCommand(engineCommandId)}
|
||||
>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId: mountContext.contextStoreInstanceId }}
|
||||
>
|
||||
<EngineCommandComponentInstanceContext.Provider
|
||||
value={{ instanceId: engineCommandId }}
|
||||
>
|
||||
{
|
||||
ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP[
|
||||
mountContext.engineComponentKey
|
||||
]
|
||||
}
|
||||
</EngineCommandComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</CommandMenuItemErrorBoundary>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { useIsHeadlessEngineCommandEffectInitialized } from '@/command-menu-item/engine-command/hooks/useIsHeadlessEngineCommandEffectInitialized';
|
||||
import { useUnmountEngineCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
|
||||
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export type HeadlessEngineCommandWrapperEffectProps = {
|
||||
execute: () => void | Promise<unknown>;
|
||||
};
|
||||
|
||||
export const HeadlessEngineCommandWrapperEffect = ({
|
||||
execute,
|
||||
}: HeadlessEngineCommandWrapperEffectProps) => {
|
||||
const { isInitializedRef, setIsInitialized } =
|
||||
useIsHeadlessEngineCommandEffectInitialized();
|
||||
|
||||
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
|
||||
EngineCommandComponentInstanceContext,
|
||||
);
|
||||
|
||||
const unmountEngineCommand = useUnmountEngineCommand();
|
||||
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitializedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
|
||||
const run = async () => {
|
||||
await execute();
|
||||
|
||||
unmountEngineCommand(engineCommandId);
|
||||
};
|
||||
|
||||
run();
|
||||
}, [
|
||||
execute,
|
||||
isInitializedRef,
|
||||
setIsInitialized,
|
||||
engineCommandId,
|
||||
unmountEngineCommand,
|
||||
enqueueErrorSnackBar,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { type PathParam, useNavigate } from 'react-router-dom';
|
||||
import { type AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
|
||||
export const HeadlessNavigateEngineCommand = <T extends AppPath>({
|
||||
to,
|
||||
params,
|
||||
queryParams,
|
||||
}: {
|
||||
to: T;
|
||||
params?: { [key in PathParam<T>]: string | null };
|
||||
queryParams?: Record<string, any>;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const path = getAppPath(to, params, queryParams);
|
||||
|
||||
// eslint-disable-next-line twenty/no-navigate-prefer-link
|
||||
const onExecute = () => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={onExecute} />;
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type SidePanelPages } from 'twenty-shared/types';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export const HeadlessOpenSidePanelPageEngineCommand = ({
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
shouldResetSearchState = false,
|
||||
}: {
|
||||
page: SidePanelPages;
|
||||
pageTitle: MessageDescriptor;
|
||||
pageIcon: IconComponent;
|
||||
shouldResetSearchState?: boolean;
|
||||
}) => {
|
||||
const { navigateSidePanel } = useNavigateSidePanel();
|
||||
const setSidePanelSearch = useSetAtomState(sidePanelSearchState);
|
||||
|
||||
const onExecute = () => {
|
||||
navigateSidePanel({
|
||||
page,
|
||||
pageTitle: t(pageTitle),
|
||||
pageIcon,
|
||||
});
|
||||
|
||||
if (shouldResetSearchState) {
|
||||
setSidePanelSearch('');
|
||||
}
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={onExecute} />;
|
||||
};
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { HeadlessOpenSidePanelPageEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessOpenSidePanelPageEngineCommand';
|
||||
import { DeleteMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand';
|
||||
import { DestroyMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/DestroyMultipleRecordsCommand';
|
||||
import { ExportMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand';
|
||||
import { MergeMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/MergeMultipleRecordsCommand';
|
||||
import { RestoreMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/RestoreMultipleRecordsCommand';
|
||||
import { UpdateMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/UpdateMultipleRecordsCommand';
|
||||
import { CreateNewIndexRecordNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/CreateNewIndexRecordNoSelectionRecordCommand';
|
||||
import { CreateNewViewNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/CreateNewViewNoSelectionRecordCommand';
|
||||
import { HideDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/HideDeletedRecordsNoSelectionRecordCommand';
|
||||
import { ImportRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/ImportRecordsNoSelectionRecordCommand';
|
||||
import { SeeDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/SeeDeletedRecordsNoSelectionRecordCommand';
|
||||
import { AddToFavoritesSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand';
|
||||
import { DeleteSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/DeleteSingleRecordCommand';
|
||||
import { DestroySingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand';
|
||||
import { ExportNoteSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/ExportNoteSingleRecordCommand';
|
||||
import { ExportSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand';
|
||||
import { NavigateToNextRecordSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/NavigateToNextRecordSingleRecordCommand';
|
||||
import { NavigateToPreviousRecordSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand';
|
||||
import { RemoveFromFavoritesSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand';
|
||||
import { RestoreSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand';
|
||||
import { CancelDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/CancelDashboardSingleRecordCommand';
|
||||
import { DuplicateDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand';
|
||||
import { EditDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/EditDashboardSingleRecordCommand';
|
||||
import { SaveDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/SaveDashboardSingleRecordCommand';
|
||||
import { EditRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand';
|
||||
import { SeeVersionWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeVersionWorkflowRunSingleRecordCommand';
|
||||
import { SeeWorkflowWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeWorkflowWorkflowRunSingleRecordCommand';
|
||||
import { StopWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand';
|
||||
import { SeeRunsWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeRunsWorkflowVersionSingleRecordCommand';
|
||||
import { SeeVersionsWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeVersionsWorkflowVersionSingleRecordCommand';
|
||||
import { SeeWorkflowWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeWorkflowWorkflowVersionSingleRecordCommand';
|
||||
import { UseAsDraftWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/UseAsDraftWorkflowVersionSingleRecordCommand';
|
||||
import { ActivateWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand';
|
||||
import { AddNodeWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/AddNodeWorkflowSingleRecordCommand';
|
||||
import { DeactivateWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand';
|
||||
import { DiscardDraftWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand';
|
||||
import { DuplicateWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/DuplicateWorkflowSingleRecordCommand';
|
||||
import { SeeActiveVersionWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/SeeActiveVersionWorkflowSingleRecordCommand';
|
||||
import { SeeRunsWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/SeeRunsWorkflowSingleRecordCommand';
|
||||
import { SeeVersionsWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/SeeVersionsWorkflowSingleRecordCommand';
|
||||
import { TestWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand';
|
||||
import { TidyUpWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { AppPath, SettingsPath, SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconHistory, IconSearch, IconSparkles } from 'twenty-ui/display';
|
||||
import { EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP: Record<
|
||||
EngineComponentKey,
|
||||
React.ReactNode
|
||||
> = {
|
||||
[EngineComponentKey.CREATE_NEW_RECORD]: (
|
||||
<CreateNewIndexRecordNoSelectionRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.DELETE_SINGLE_RECORD]: <DeleteSingleRecordCommand />,
|
||||
[EngineComponentKey.DELETE_MULTIPLE_RECORDS]: (
|
||||
<DeleteMultipleRecordsCommand />
|
||||
),
|
||||
[EngineComponentKey.RESTORE_SINGLE_RECORD]: <RestoreSingleRecordCommand />,
|
||||
[EngineComponentKey.RESTORE_MULTIPLE_RECORDS]: (
|
||||
<RestoreMultipleRecordsCommand />
|
||||
),
|
||||
[EngineComponentKey.DESTROY_SINGLE_RECORD]: <DestroySingleRecordCommand />,
|
||||
[EngineComponentKey.DESTROY_MULTIPLE_RECORDS]: (
|
||||
<DestroyMultipleRecordsCommand />
|
||||
),
|
||||
[EngineComponentKey.ADD_TO_FAVORITES]: <AddToFavoritesSingleRecordCommand />,
|
||||
[EngineComponentKey.REMOVE_FROM_FAVORITES]: (
|
||||
<RemoveFromFavoritesSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.MERGE_MULTIPLE_RECORDS]: <MergeMultipleRecordsCommand />,
|
||||
[EngineComponentKey.DUPLICATE_DASHBOARD]: (
|
||||
<DuplicateDashboardSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.DUPLICATE_WORKFLOW]: (
|
||||
<DuplicateWorkflowSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.ACTIVATE_WORKFLOW]: (
|
||||
<ActivateWorkflowSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.DEACTIVATE_WORKFLOW]: (
|
||||
<DeactivateWorkflowSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.DISCARD_DRAFT_WORKFLOW]: (
|
||||
<DiscardDraftWorkflowSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.TEST_WORKFLOW]: <TestWorkflowSingleRecordCommand />,
|
||||
[EngineComponentKey.STOP_WORKFLOW_RUN]: (
|
||||
<StopWorkflowRunSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.USE_AS_DRAFT_WORKFLOW_VERSION]: (
|
||||
<UseAsDraftWorkflowVersionSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.SAVE_DASHBOARD_LAYOUT]: (
|
||||
<SaveDashboardSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.TIDY_UP_WORKFLOW]: <TidyUpWorkflowSingleRecordCommand />,
|
||||
[EngineComponentKey.NAVIGATE_TO_NEXT_RECORD]: (
|
||||
<NavigateToNextRecordSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.NAVIGATE_TO_PREVIOUS_RECORD]: (
|
||||
<NavigateToPreviousRecordSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.EXPORT_NOTE_TO_PDF]: <ExportNoteSingleRecordCommand />,
|
||||
[EngineComponentKey.EXPORT_FROM_RECORD_INDEX]: (
|
||||
<ExportMultipleRecordsCommand />
|
||||
),
|
||||
[EngineComponentKey.EXPORT_FROM_RECORD_SHOW]: <ExportSingleRecordCommand />,
|
||||
[EngineComponentKey.EXPORT_MULTIPLE_RECORDS]: (
|
||||
<ExportMultipleRecordsCommand />
|
||||
),
|
||||
[EngineComponentKey.UPDATE_MULTIPLE_RECORDS]: (
|
||||
<UpdateMultipleRecordsCommand />
|
||||
),
|
||||
[EngineComponentKey.IMPORT_RECORDS]: (
|
||||
<ImportRecordsNoSelectionRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.EXPORT_VIEW]: <ExportMultipleRecordsCommand />,
|
||||
[EngineComponentKey.SEE_DELETED_RECORDS]: (
|
||||
<SeeDeletedRecordsNoSelectionRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.CREATE_NEW_VIEW]: (
|
||||
<CreateNewViewNoSelectionRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.HIDE_DELETED_RECORDS]: (
|
||||
<HideDeletedRecordsNoSelectionRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.EDIT_RECORD_PAGE_LAYOUT]: (
|
||||
<EditRecordPageLayoutSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.EDIT_DASHBOARD_LAYOUT]: (
|
||||
<EditDashboardSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.CANCEL_DASHBOARD_LAYOUT]: (
|
||||
<CancelDashboardSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.GO_TO_PEOPLE]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.Person }}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.GO_TO_COMPANIES]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.Company }}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.GO_TO_DASHBOARDS]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.Dashboard }}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.GO_TO_OPPORTUNITIES]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{
|
||||
objectNamePlural: CoreObjectNamePlural.Opportunity,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.GO_TO_SETTINGS]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.SettingsCatchAll}
|
||||
params={{
|
||||
'*': SettingsPath.ProfilePage,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.GO_TO_TASKS]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.Task }}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.GO_TO_NOTES]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.Note }}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.GO_TO_WORKFLOWS]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.Workflow }}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.GO_TO_RUNS]: (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.WorkflowRun }}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.SEARCH_RECORDS]: (
|
||||
<HeadlessOpenSidePanelPageEngineCommand
|
||||
page={SidePanelPages.SearchRecords}
|
||||
pageTitle={msg`Search`}
|
||||
pageIcon={IconSearch}
|
||||
shouldResetSearchState={true}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.SEARCH_RECORDS_FALLBACK]: (
|
||||
<HeadlessOpenSidePanelPageEngineCommand
|
||||
page={SidePanelPages.SearchRecords}
|
||||
pageTitle={msg`Search`}
|
||||
pageIcon={IconSearch}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.ASK_AI]: (
|
||||
<HeadlessOpenSidePanelPageEngineCommand
|
||||
page={SidePanelPages.AskAI}
|
||||
pageTitle={msg`Ask AI`}
|
||||
pageIcon={IconSparkles}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.VIEW_PREVIOUS_AI_CHATS]: (
|
||||
<HeadlessOpenSidePanelPageEngineCommand
|
||||
page={SidePanelPages.ViewPreviousAIChats}
|
||||
pageTitle={msg`View Previous AI Chats`}
|
||||
pageIcon={IconHistory}
|
||||
/>
|
||||
),
|
||||
[EngineComponentKey.SEE_ACTIVE_VERSION_WORKFLOW]: (
|
||||
<SeeActiveVersionWorkflowSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.SEE_RUNS_WORKFLOW]: (
|
||||
<SeeRunsWorkflowSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.SEE_VERSIONS_WORKFLOW]: (
|
||||
<SeeVersionsWorkflowSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.ADD_NODE_WORKFLOW]: (
|
||||
<AddNodeWorkflowSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.SEE_VERSION_WORKFLOW_RUN]: (
|
||||
<SeeVersionWorkflowRunSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.SEE_WORKFLOW_WORKFLOW_RUN]: (
|
||||
<SeeWorkflowWorkflowRunSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.SEE_RUNS_WORKFLOW_VERSION]: (
|
||||
<SeeRunsWorkflowVersionSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.SEE_WORKFLOW_WORKFLOW_VERSION]: (
|
||||
<SeeWorkflowWorkflowVersionSingleRecordCommand />
|
||||
),
|
||||
[EngineComponentKey.SEE_VERSIONS_WORKFLOW_VERSION]: (
|
||||
<SeeVersionsWorkflowVersionSingleRecordCommand />
|
||||
),
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
export const useIsHeadlessEngineCommandEffectInitialized = () => {
|
||||
// eslint-disable-next-line twenty/no-state-useref
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
const setIsInitialized = (value: boolean) => {
|
||||
isInitializedRef.current = value;
|
||||
};
|
||||
|
||||
return { isInitializedRef, setIsInitialized };
|
||||
};
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
|
||||
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
|
||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useMountEngineCommand = () => {
|
||||
const store = useStore();
|
||||
|
||||
const mountEngineCommand = useCallback(
|
||||
(
|
||||
engineCommandId: string,
|
||||
contextStoreInstanceId: string,
|
||||
engineComponentKey: EngineComponentKey,
|
||||
) => {
|
||||
const objectMetadataItemId = store.get(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
|
||||
instanceId: contextStoreInstanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === objectMetadataItemId,
|
||||
);
|
||||
|
||||
const currentViewId = store.get(
|
||||
contextStoreCurrentViewIdComponentState.atomFamily({
|
||||
instanceId: contextStoreInstanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const targetedRecordsRule = store.get(
|
||||
contextStoreTargetedRecordsRuleComponentState.atomFamily({
|
||||
instanceId: contextStoreInstanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const selectedRecords = (
|
||||
targetedRecordsRule.mode === 'selection'
|
||||
? targetedRecordsRule.selectedRecordIds
|
||||
: []
|
||||
)
|
||||
.map((id) => store.get(recordStoreFamilyState.atomFamily(id)))
|
||||
.filter(isDefined);
|
||||
|
||||
const filters = store.get(
|
||||
contextStoreFiltersComponentState.atomFamily({
|
||||
instanceId: contextStoreInstanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const filterGroups = store.get(
|
||||
contextStoreFilterGroupsComponentState.atomFamily({
|
||||
instanceId: contextStoreInstanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const anyFieldFilterValue = store.get(
|
||||
contextStoreAnyFieldFilterValueComponentState.atomFamily({
|
||||
instanceId: contextStoreInstanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const currentWorkspaceMember = store.get(
|
||||
currentWorkspaceMemberState.atom,
|
||||
);
|
||||
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const userTimezone =
|
||||
currentWorkspaceMember?.timeZone !== 'system'
|
||||
? (currentWorkspaceMember?.timeZone ?? systemTimeZone)
|
||||
: systemTimeZone;
|
||||
|
||||
const graphqlFilter = isDefined(objectMetadataItem)
|
||||
? computeContextStoreFilters({
|
||||
contextStoreTargetedRecordsRule: targetedRecordsRule,
|
||||
contextStoreFilters: filters,
|
||||
contextStoreFilterGroups: filterGroups,
|
||||
objectMetadataItem,
|
||||
filterValueDependencies: {
|
||||
currentWorkspaceMemberId: currentWorkspaceMember?.id,
|
||||
timeZone: userTimezone,
|
||||
},
|
||||
contextStoreAnyFieldFilterValue: anyFieldFilterValue,
|
||||
})
|
||||
: null;
|
||||
|
||||
const recordIndexId =
|
||||
objectMetadataItem && currentViewId
|
||||
? getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
currentViewId,
|
||||
)
|
||||
: null;
|
||||
|
||||
store.set(mountedEngineCommandsState.atom, (previousMap) => {
|
||||
const newMap = new Map(previousMap);
|
||||
|
||||
newMap.set(engineCommandId, {
|
||||
engineComponentKey,
|
||||
contextStoreInstanceId,
|
||||
objectMetadataItem: objectMetadataItem ?? null,
|
||||
currentViewId,
|
||||
recordIndexId,
|
||||
targetedRecordsRule,
|
||||
selectedRecords,
|
||||
graphqlFilter,
|
||||
});
|
||||
|
||||
return newMap;
|
||||
});
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return mountEngineCommand;
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
|
||||
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useMountedEngineCommandContext = () => {
|
||||
const engineCommandId = useAvailableComponentInstanceIdOrThrow(
|
||||
EngineCommandComponentInstanceContext,
|
||||
);
|
||||
|
||||
const mountedEngineCommands = useAtomStateValue(mountedEngineCommandsState);
|
||||
const context = mountedEngineCommands.get(engineCommandId);
|
||||
|
||||
if (!isDefined(context)) {
|
||||
throw new Error(
|
||||
'Engine command mount context not found. Make sure the command was mounted via the engine command mount flow.',
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useUnmountEngineCommand = () => {
|
||||
const store = useStore();
|
||||
|
||||
const unmountEngineCommand = useCallback(
|
||||
(engineCommandId: string) => {
|
||||
store.set(mountedEngineCommandsState.atom, (previousMap) => {
|
||||
const newMap = new Map(previousMap);
|
||||
|
||||
newMap.delete(engineCommandId);
|
||||
|
||||
return newMap;
|
||||
});
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return unmountEngineCommand;
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useIncrementalDeleteManyRecords } from '@/object-record/hooks/useIncrementalDeleteManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const DeleteMultipleRecordsCommand = () => {
|
||||
const { recordIndexId, objectMetadataItem, graphqlFilter } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
if (
|
||||
!isDefined(recordIndexId) ||
|
||||
!isDefined(objectMetadataItem) ||
|
||||
!isDefined(graphqlFilter)
|
||||
) {
|
||||
throw new Error(
|
||||
'Record index ID, object metadata item, and graphql filter are required to delete multiple records',
|
||||
);
|
||||
}
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { incrementalDeleteManyRecords } = useIncrementalDeleteManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
filter: graphqlFilter,
|
||||
pageSize: DEFAULT_QUERY_PAGE_SIZE,
|
||||
delayInMsBetweenMutations: 50,
|
||||
});
|
||||
|
||||
const handleExecute = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
await incrementalDeleteManyRecords();
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useIncrementalDestroyManyRecords } from '@/object-record/hooks/useIncrementalDestroyManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const DestroyMultipleRecordsCommand = () => {
|
||||
const { recordIndexId, objectMetadataItem, graphqlFilter } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
if (
|
||||
!isDefined(recordIndexId) ||
|
||||
!isDefined(objectMetadataItem) ||
|
||||
!isDefined(graphqlFilter)
|
||||
) {
|
||||
throw new Error(
|
||||
'Record index ID, object metadata item, and graphql filter are required to destroy multiple records',
|
||||
);
|
||||
}
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const deletedAtFilter: RecordGqlOperationFilter = {
|
||||
deletedAt: { is: 'NOT_NULL' },
|
||||
};
|
||||
|
||||
const combinedFilter = {
|
||||
...graphqlFilter,
|
||||
...deletedAtFilter,
|
||||
};
|
||||
|
||||
const { incrementalDestroyManyRecords } = useIncrementalDestroyManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
filter: combinedFilter,
|
||||
pageSize: DEFAULT_QUERY_PAGE_SIZE,
|
||||
delayInMsBetweenMutations: 50,
|
||||
});
|
||||
|
||||
const handleExecute = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
|
||||
resetTableRowSelection();
|
||||
|
||||
await incrementalDestroyManyRecords();
|
||||
};
|
||||
|
||||
return (
|
||||
<HeadlessConfirmationModalEngineCommandEffect
|
||||
title={t`Permanently Destroy Records`}
|
||||
subtitle={t`Are you sure you want to destroy these records? They won't be recoverable anymore.`}
|
||||
confirmButtonText={t`Destroy Records`}
|
||||
execute={handleExecute}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useRecordIndexExportRecords } from '@/object-record/record-index/export/hooks/useRecordIndexExportRecords';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const ExportMultipleRecordsCommand = () => {
|
||||
const { objectMetadataItem, recordIndexId } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
|
||||
throw new Error(
|
||||
'Object metadata item and record index ID are required to export multiple records',
|
||||
);
|
||||
}
|
||||
|
||||
const { download } = useRecordIndexExportRecords({
|
||||
delayMs: 100,
|
||||
objectMetadataItem,
|
||||
recordIndexId,
|
||||
filename: `${objectMetadataItem.nameSingular}.csv`,
|
||||
});
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={download} />;
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useOpenMergeRecordsPageInSidePanel } from '@/side-panel/hooks/useOpenMergeRecordsPageInSidePanel';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const MergeMultipleRecordsCommand = () => {
|
||||
const { objectMetadataItem, selectedRecords } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const selectedRecordIds = selectedRecords.map((record) => record.id);
|
||||
|
||||
if (!isDefined(objectMetadataItem) || selectedRecordIds.length === 0) {
|
||||
throw new Error(
|
||||
'Object metadata item and selected records are required to merge multiple records',
|
||||
);
|
||||
}
|
||||
|
||||
const { openMergeRecordsPageInSidePanel } =
|
||||
useOpenMergeRecordsPageInSidePanel({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectRecordIds: selectedRecordIds,
|
||||
});
|
||||
|
||||
const handleExecute = () => {
|
||||
openMergeRecordsPageInSidePanel();
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RestoreMultipleRecordsCommand = () => {
|
||||
const { recordIndexId, objectMetadataItem, graphqlFilter } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
if (
|
||||
!isDefined(recordIndexId) ||
|
||||
!isDefined(objectMetadataItem) ||
|
||||
!isDefined(graphqlFilter)
|
||||
) {
|
||||
throw new Error(
|
||||
'Record index ID, object metadata item, and graphql filter are required to restore multiple records',
|
||||
);
|
||||
}
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { restoreManyRecords } = useRestoreManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const deletedAtFilter: RecordGqlOperationFilter = {
|
||||
deletedAt: { is: 'NOT_NULL' },
|
||||
};
|
||||
|
||||
const combinedFilter = {
|
||||
...graphqlFilter,
|
||||
...deletedAtFilter,
|
||||
};
|
||||
|
||||
const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
filter: combinedFilter,
|
||||
limit: DEFAULT_QUERY_PAGE_SIZE,
|
||||
recordGqlFields: { id: true },
|
||||
});
|
||||
|
||||
const handleExecute = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
const recordsToRestore = await fetchAllRecordIds();
|
||||
const recordIdsToRestore = recordsToRestore.map((record) => record.id);
|
||||
|
||||
resetTableRowSelection();
|
||||
|
||||
await restoreManyRecords({
|
||||
idsToRestore: recordIdsToRestore,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<HeadlessConfirmationModalEngineCommandEffect
|
||||
title={t`Restore Records`}
|
||||
subtitle={t`Are you sure you want to restore these records?`}
|
||||
confirmButtonText={t`Restore Records`}
|
||||
confirmButtonAccent="default"
|
||||
execute={handleExecute}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useOpenUpdateMultipleRecordsPageInSidePanel } from '@/side-panel/hooks/useOpenUpdateMultipleRecordsPageInSidePanel';
|
||||
|
||||
export const UpdateMultipleRecordsCommand = () => {
|
||||
const { contextStoreInstanceId } = useMountedEngineCommandContext();
|
||||
|
||||
const { openUpdateMultipleRecordsPageInSidePanel } =
|
||||
useOpenUpdateMultipleRecordsPageInSidePanel({
|
||||
contextStoreInstanceId,
|
||||
});
|
||||
|
||||
const handleExecute = () => {
|
||||
openUpdateMultipleRecordsPageInSidePanel();
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CreateNewIndexRecordNoSelectionRecordCommand = () => {
|
||||
const { objectMetadataItem, recordIndexId } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
|
||||
throw new Error(
|
||||
'Object metadata item and record index ID are required to create new index record',
|
||||
);
|
||||
}
|
||||
|
||||
const { createNewIndexRecord } = useCreateNewIndexRecord({
|
||||
objectMetadataItem,
|
||||
instanceId: recordIndexId,
|
||||
});
|
||||
|
||||
return (
|
||||
<HeadlessEngineCommandWrapperEffect
|
||||
execute={() => createNewIndexRecord({ position: 'first' })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
|
||||
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
|
||||
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CreateNewViewNoSelectionRecordCommand = () => {
|
||||
const { currentViewId, recordIndexId } = useMountedEngineCommandContext();
|
||||
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
|
||||
if (!isDefined(currentViewId) || !isDefined(recordIndexId)) {
|
||||
throw new Error(
|
||||
'Current view ID and record index ID are required to create new view',
|
||||
);
|
||||
}
|
||||
|
||||
const setViewPickerReferenceViewId = useSetAtomComponentState(
|
||||
viewPickerReferenceViewIdComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const { setViewPickerMode } = useViewPickerMode(recordIndexId);
|
||||
|
||||
const handleExecute = () => {
|
||||
if (currentViewId) {
|
||||
setViewPickerReferenceViewId(currentViewId);
|
||||
}
|
||||
setViewPickerMode('create-empty');
|
||||
openDropdown({
|
||||
dropdownComponentInstanceIdFromProps: VIEW_PICKER_DROPDOWN_ID,
|
||||
});
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useCheckIsSoftDeleteFilter } from '@/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter';
|
||||
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const HideDeletedRecordsNoSelectionRecordCommand = () => {
|
||||
const { objectMetadataItem, recordIndexId } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
|
||||
throw new Error(
|
||||
'Object metadata item and record index ID are required to hide deleted records',
|
||||
);
|
||||
}
|
||||
|
||||
const { toggleSoftDeleteFilterState } = useHandleToggleTrashColumnFilter({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
viewBarId: recordIndexId,
|
||||
recordFiltersInstanceId: recordIndexId,
|
||||
});
|
||||
|
||||
const { isRecordFilterAboutSoftDelete } = useCheckIsSoftDeleteFilter();
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const deletedFilter = currentRecordFilters.find(
|
||||
isRecordFilterAboutSoftDelete,
|
||||
);
|
||||
|
||||
const { removeRecordFilter } = useRemoveRecordFilter(recordIndexId);
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!isDefined(deletedFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeRecordFilter({ recordFilterId: deletedFilter.id });
|
||||
toggleSoftDeleteFilterState(false);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useOpenObjectRecordsSpreadsheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const ImportRecordsNoSelectionRecordCommand = () => {
|
||||
const { objectMetadataItem } = useMountedEngineCommandContext();
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error('Object metadata item is required to import records');
|
||||
}
|
||||
|
||||
const { openObjectRecordsSpreadsheetImportDialog } =
|
||||
useOpenObjectRecordsSpreadsheetImportDialog(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
return (
|
||||
<HeadlessEngineCommandWrapperEffect
|
||||
execute={openObjectRecordsSpreadsheetImportDialog}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SeeDeletedRecordsNoSelectionRecordCommand = () => {
|
||||
const { objectMetadataItem, recordIndexId } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
|
||||
throw new Error(
|
||||
'Object metadata item and record index ID are required to see deleted records',
|
||||
);
|
||||
}
|
||||
|
||||
const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } =
|
||||
useHandleToggleTrashColumnFilter({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
viewBarId: recordIndexId,
|
||||
recordFiltersInstanceId: recordIndexId,
|
||||
});
|
||||
|
||||
return (
|
||||
<HeadlessEngineCommandWrapperEffect
|
||||
execute={() => {
|
||||
handleToggleTrashColumnFilter();
|
||||
toggleSoftDeleteFilterState(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useCreateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useCreateNavigationMenuItem';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const AddToFavoritesSingleRecordCommand = () => {
|
||||
const { objectMetadataItem, selectedRecords } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error('Object metadata item is required to add to favorites');
|
||||
}
|
||||
|
||||
const { createNavigationMenuItem } = useCreateNavigationMenuItem();
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!isDefined(selectedRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
createNavigationMenuItem(selectedRecord, objectMetadataItem.nameSingular);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/common/hooks/useRemoveNavigationMenuItemByTargetRecordId';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const DeleteSingleRecordCommand = () => {
|
||||
const { recordIndexId, objectMetadataItem, selectedRecords } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
if (
|
||||
!isDefined(recordId) ||
|
||||
!isDefined(recordIndexId) ||
|
||||
!isDefined(objectMetadataItem)
|
||||
) {
|
||||
throw new Error(
|
||||
'Record ID, record index ID, and object metadata are required to delete single record',
|
||||
);
|
||||
}
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { deleteOneRecord } = useDeleteOneRecord({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { navigationMenuItems, workspaceNavigationMenuItems } =
|
||||
useNavigationMenuItemsData();
|
||||
|
||||
const { removeNavigationMenuItemsByTargetRecordIds } =
|
||||
useRemoveNavigationMenuItemByTargetRecordId();
|
||||
|
||||
const handleExecute = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
|
||||
resetTableRowSelection();
|
||||
|
||||
const foundNavigationMenuItem = [
|
||||
...navigationMenuItems,
|
||||
...workspaceNavigationMenuItems,
|
||||
].find((item) => item.targetRecordId === recordId);
|
||||
|
||||
if (isDefined(foundNavigationMenuItem)) {
|
||||
removeNavigationMenuItemsByTargetRecordIds([recordId]);
|
||||
}
|
||||
|
||||
await deleteOneRecord(recordId);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
export const DestroySingleRecordCommand = () => {
|
||||
const { recordIndexId, objectMetadataItem, selectedRecords } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
if (
|
||||
!isDefined(recordId) ||
|
||||
!isDefined(recordIndexId) ||
|
||||
!isDefined(objectMetadataItem)
|
||||
) {
|
||||
throw new Error(
|
||||
'Record ID, record index ID, and object metadata are required to destroy single record',
|
||||
);
|
||||
}
|
||||
|
||||
const navigateApp = useNavigateApp();
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { destroyOneRecord } = useDestroyOneRecord({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const handleExecute = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
|
||||
await destroyOneRecord(recordId);
|
||||
navigateApp(AppPath.RecordIndexPage, {
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<HeadlessConfirmationModalEngineCommandEffect
|
||||
title={t`Permanently Destroy Record`}
|
||||
subtitle={t`Are you sure you want to destroy this record? It cannot be recovered anymore.`}
|
||||
confirmButtonText={t`Permanently Destroy Record`}
|
||||
execute={handleExecute}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const ExportNoteSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
const recordId = selectedRecord?.id;
|
||||
|
||||
if (!isDefined(recordId) || !isDefined(selectedRecord)) {
|
||||
throw new Error(
|
||||
'Record ID and selected record are required to export note to PDF',
|
||||
);
|
||||
}
|
||||
|
||||
const filename = `${(selectedRecord.title || 'Untitled Note').replace(/[<>:"/\\|?*]/g, '-')}`;
|
||||
|
||||
const handleExecute = async () => {
|
||||
const initialBody = selectedRecord.bodyV2?.blocknote;
|
||||
|
||||
let parsedBody = [];
|
||||
|
||||
// TODO: Remove this once we have removed the old rich text
|
||||
try {
|
||||
parsedBody = JSON.parse(initialBody);
|
||||
} catch {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Failed to parse body for record ${recordId}, for rich text version 'v2'`,
|
||||
);
|
||||
// oxlint-disable-next-line no-console
|
||||
console.warn(initialBody);
|
||||
}
|
||||
|
||||
const { exportBlockNoteEditorToPdf } = await import(
|
||||
'@/command-menu-item/record/single-record/utils/exportBlockNoteEditorToPdf'
|
||||
);
|
||||
|
||||
await exportBlockNoteEditorToPdf(parsedBody, filename);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useExportSingleRecord } from '@/object-record/record-show/hooks/useExportSingleRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const ExportSingleRecordCommand = () => {
|
||||
const { objectMetadataItem, currentViewId, selectedRecords } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
if (
|
||||
!isDefined(currentViewId) ||
|
||||
!isDefined(recordId) ||
|
||||
!isDefined(objectMetadataItem)
|
||||
) {
|
||||
throw new Error(
|
||||
'Current view ID, record ID, and object metadata are required to export single record',
|
||||
);
|
||||
}
|
||||
|
||||
const filename = `${objectMetadataItem.nameSingular}.csv`;
|
||||
const { download } = useExportSingleRecord({
|
||||
filename,
|
||||
objectMetadataItem,
|
||||
recordId,
|
||||
});
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={download} />;
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const NavigateToNextRecordSingleRecordCommand = () => {
|
||||
const { objectMetadataItem, selectedRecords } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
if (!isDefined(recordId) || !isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
'Record ID and object metadata are required to navigate to next record',
|
||||
);
|
||||
}
|
||||
|
||||
const { navigateToNextRecord } = useRecordShowPagePagination(
|
||||
objectMetadataItem.nameSingular,
|
||||
recordId,
|
||||
);
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={navigateToNextRecord} />;
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const NavigateToPreviousRecordSingleRecordCommand = () => {
|
||||
const { objectMetadataItem, selectedRecords } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
if (!isDefined(recordId) || !isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
'Record ID and object metadata are required to navigate to previous record',
|
||||
);
|
||||
}
|
||||
|
||||
const { navigateToPreviousRecord } = useRecordShowPagePagination(
|
||||
objectMetadataItem.nameSingular,
|
||||
recordId,
|
||||
);
|
||||
|
||||
return (
|
||||
<HeadlessEngineCommandWrapperEffect execute={navigateToPreviousRecord} />
|
||||
);
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RemoveFromFavoritesSingleRecordCommand = () => {
|
||||
const { selectedRecords, objectMetadataItem } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
if (!isDefined(recordId) || !isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
'Record ID and object metadata are required to remove from favorites',
|
||||
);
|
||||
}
|
||||
|
||||
const { navigationMenuItems, workspaceNavigationMenuItems } =
|
||||
useNavigationMenuItemsData();
|
||||
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
|
||||
const foundNavigationMenuItem = [
|
||||
...navigationMenuItems,
|
||||
...workspaceNavigationMenuItems,
|
||||
].find(
|
||||
(item) =>
|
||||
item.targetRecordId === recordId &&
|
||||
item.targetObjectMetadataId === objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!isDefined(foundNavigationMenuItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteNavigationMenuItem(foundNavigationMenuItem.id);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RestoreSingleRecordCommand = () => {
|
||||
const { recordIndexId, objectMetadataItem, selectedRecords } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
if (
|
||||
!isDefined(recordId) ||
|
||||
!isDefined(recordIndexId) ||
|
||||
!isDefined(objectMetadataItem)
|
||||
) {
|
||||
throw new Error(
|
||||
'Record ID, record index ID, and object metadata are required to restore single record',
|
||||
);
|
||||
}
|
||||
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
|
||||
const { removeSelectedRecordsFromRecordBoard } =
|
||||
useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
|
||||
|
||||
const { restoreManyRecords } = useRestoreManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const handleExecute = async () => {
|
||||
removeSelectedRecordsFromRecordBoard();
|
||||
resetTableRowSelection();
|
||||
|
||||
await restoreManyRecords({
|
||||
idsToRestore: [recordId],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<HeadlessConfirmationModalEngineCommandEffect
|
||||
title={t`Restore Record`}
|
||||
subtitle={t`Are you sure you want to restore this record?`}
|
||||
confirmButtonText={t`Restore Record`}
|
||||
confirmButtonAccent="default"
|
||||
execute={handleExecute}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CancelDashboardSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
if (!isDefined(selectedRecord)) {
|
||||
throw new Error('Selected record is required to cancel dashboard');
|
||||
}
|
||||
|
||||
const pageLayoutId = selectedRecord.pageLayoutId;
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { resetDraftPageLayoutToPersistedPageLayout } =
|
||||
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
|
||||
|
||||
const handleExecute = () => {
|
||||
closeSidePanelMenu();
|
||||
|
||||
resetDraftPageLayoutToPersistedPageLayout();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useDuplicateDashboard } from '@/dashboards/hooks/useDuplicateDashboard';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
export const DuplicateDashboardSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const { duplicateDashboard } = useDuplicateDashboard();
|
||||
const navigate = useNavigateApp();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { t } = useLingui();
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to duplicate dashboard');
|
||||
}
|
||||
|
||||
const handleExecute = async () => {
|
||||
const result = await duplicateDashboard(recordId);
|
||||
|
||||
if (isDefined(result) && isNonEmptyString(result.id)) {
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Dashboard duplicated successfully`,
|
||||
});
|
||||
|
||||
navigate(AppPath.RecordShowPage, {
|
||||
objectNameSingular: CoreObjectNameSingular.Dashboard,
|
||||
objectRecordId: result.id,
|
||||
});
|
||||
} else {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to duplicate dashboard`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useResetLocationHash } from 'twenty-ui/utilities';
|
||||
|
||||
export const EditDashboardSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
if (!isDefined(selectedRecord)) {
|
||||
throw new Error('Selected record is required to edit dashboard');
|
||||
}
|
||||
|
||||
const pageLayoutId = selectedRecord.pageLayoutId;
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { resetLocationHash } = useResetLocationHash();
|
||||
|
||||
const handleExecute = () => {
|
||||
setIsPageLayoutInEditMode(true);
|
||||
resetLocationHash();
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SaveDashboardSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
if (!isDefined(selectedRecord)) {
|
||||
throw new Error('Selected record is required to save dashboard');
|
||||
}
|
||||
|
||||
const pageLayoutId = selectedRecord.pageLayoutId;
|
||||
|
||||
const { savePageLayout } = useSavePageLayout(pageLayoutId);
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const handleExecute = async () => {
|
||||
const result = await savePageLayout();
|
||||
|
||||
if (result.status === 'successful') {
|
||||
closeSidePanelMenu();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CancelRecordPageLayoutSingleRecordCommand = () => {
|
||||
const { objectMetadataItem } = useMountedEngineCommandContext();
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
'Object metadata item is required to cancel record page layout',
|
||||
);
|
||||
}
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { resetDraftPageLayoutToPersistedPageLayout } =
|
||||
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
|
||||
|
||||
const handleExecute = () => {
|
||||
closeSidePanelMenu();
|
||||
|
||||
resetDraftPageLayoutToPersistedPageLayout();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useResetLocationHash } from 'twenty-ui/utilities';
|
||||
|
||||
export const EditRecordPageLayoutSingleRecordCommand = () => {
|
||||
const { objectMetadataItem } = useMountedEngineCommandContext();
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
'Object metadata item is required to edit record page layout',
|
||||
);
|
||||
}
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { resetLocationHash } = useResetLocationHash();
|
||||
|
||||
const handleExecute = () => {
|
||||
setIsPageLayoutInEditMode(true);
|
||||
resetLocationHash();
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
|
||||
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SaveRecordPageLayoutSingleRecordCommand = () => {
|
||||
const { objectMetadataItem } = useMountedEngineCommandContext();
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
'Object metadata item is required to save record page layout',
|
||||
);
|
||||
}
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { savePageLayout } = useSavePageLayout(pageLayoutId);
|
||||
|
||||
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const handleExecute = async () => {
|
||||
const result = await savePageLayout();
|
||||
|
||||
if (result.status === 'successful') {
|
||||
await saveFieldsWidgetGroups(pageLayoutId);
|
||||
|
||||
closeSidePanelMenu();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SeeVersionWorkflowRunSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
if (
|
||||
!isDefined(selectedRecord) ||
|
||||
!isDefined(selectedRecord?.workflowVersion?.id)
|
||||
) {
|
||||
throw new Error('Selected record is required to see version workflow run');
|
||||
}
|
||||
|
||||
return (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordShowPage}
|
||||
params={{
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
objectRecordId: selectedRecord.workflowVersion.id,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { CoreObjectNameSingular, AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SeeWorkflowWorkflowRunSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
if (!isDefined(selectedRecord) || !isDefined(selectedRecord?.workflow?.id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordShowPage}
|
||||
params={{
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
objectRecordId: selectedRecord.workflow.id,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { useStopWorkflowRun } from '@/workflow/hooks/useStopWorkflowRun';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const StopWorkflowRunSingleRecordCommand = () => {
|
||||
const { targetedRecordsRule, graphqlFilter } =
|
||||
useMountedEngineCommandContext();
|
||||
|
||||
const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
|
||||
filter: isDefined(graphqlFilter) ? graphqlFilter : undefined,
|
||||
limit: DEFAULT_QUERY_PAGE_SIZE,
|
||||
recordGqlFields: { id: true },
|
||||
});
|
||||
|
||||
const { stopWorkflowRun } = useStopWorkflowRun();
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (targetedRecordsRule.mode === 'selection') {
|
||||
for (const selectedRecordId of targetedRecordsRule.selectedRecordIds) {
|
||||
await stopWorkflowRun(selectedRecordId);
|
||||
}
|
||||
} else {
|
||||
const records = await fetchAllRecordIds();
|
||||
|
||||
for (const record of records) {
|
||||
await stopWorkflowRun(record.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const SeeRunsWorkflowVersionSingleRecordCommandContent = ({
|
||||
workflowId,
|
||||
recordId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
recordId: string;
|
||||
}) => {
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
return (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.WorkflowRun }}
|
||||
queryParams={{
|
||||
filter: {
|
||||
workflow: {
|
||||
[ViewFilterOperand.IS]: {
|
||||
selectedRecordIds: [workflowWithCurrentVersion?.id],
|
||||
},
|
||||
},
|
||||
recordStore: {
|
||||
[ViewFilterOperand.IS]: {
|
||||
selectedRecordIds: [recordId],
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SeeRunsWorkflowVersionSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
const workflowId = selectedRecord?.workflow?.id;
|
||||
|
||||
if (!isDefined(recordId) || !isDefined(workflowId)) {
|
||||
throw new Error(
|
||||
'Record ID and workflow ID are required to see runs workflow version',
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SeeRunsWorkflowVersionSingleRecordCommandContent
|
||||
workflowId={workflowId}
|
||||
recordId={recordId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const SeeVersionsWorkflowVersionSingleRecordCommandContent = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
return (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.WorkflowVersion }}
|
||||
queryParams={{
|
||||
filter: {
|
||||
workflow: {
|
||||
[ViewFilterOperand.IS]: {
|
||||
selectedRecordIds: [workflowWithCurrentVersion?.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SeeVersionsWorkflowVersionSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
if (!isDefined(selectedRecord) || !isDefined(selectedRecord.workflowId)) {
|
||||
throw new Error(
|
||||
'Selected record and workflow ID are required to see versions workflow version',
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SeeVersionsWorkflowVersionSingleRecordCommandContent
|
||||
workflowId={selectedRecord.workflowId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SeeWorkflowWorkflowVersionSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
const selectedRecord = selectedRecords[0];
|
||||
|
||||
if (!isDefined(selectedRecord) || !isDefined(selectedRecord?.workflow?.id)) {
|
||||
throw new Error(
|
||||
'Selected record and workflow ID are required to see workflow workflow version',
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordShowPage}
|
||||
params={{
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
objectRecordId: selectedRecord.workflow.id,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal';
|
||||
import { OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID } from '@/workflow/constants/OverrideWorkflowDraftConfirmationModalId';
|
||||
import { useCreateDraftFromWorkflowVersion } from '@/workflow/hooks/useCreateDraftFromWorkflowVersion';
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { useState } from 'react';
|
||||
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
const UseAsDraftWorkflowVersionSingleRecordCommandContent = ({
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
const { openModal } = useModal();
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
const { createDraftFromWorkflowVersion } =
|
||||
useCreateDraftFromWorkflowVersion();
|
||||
const navigate = useNavigateApp();
|
||||
const [hasNavigated, setHasNavigated] = useState(false);
|
||||
|
||||
const hasAlreadyDraftVersion =
|
||||
workflow?.versions.some((version) => version.status === 'DRAFT') || false;
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!isDefined(workflow) || hasNavigated) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAlreadyDraftVersion) {
|
||||
openModal(OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID);
|
||||
} else {
|
||||
const executeCommandWithoutWaiting = async () => {
|
||||
await createDraftFromWorkflowVersion({
|
||||
workflowId,
|
||||
workflowVersionIdToCopy: workflowVersionId,
|
||||
});
|
||||
|
||||
navigate(AppPath.RecordShowPage, {
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
objectRecordId: workflowId,
|
||||
});
|
||||
|
||||
setHasNavigated(true);
|
||||
};
|
||||
|
||||
executeCommandWithoutWaiting();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadlessEngineCommandWrapperEffect execute={handleExecute} />
|
||||
<OverrideWorkflowDraftConfirmationModal
|
||||
workflowId={workflowId}
|
||||
workflowVersionIdToCopy={workflowVersionId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const UseAsDraftWorkflowVersionSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const workflowVersion = useWorkflowVersion(recordId ?? '');
|
||||
|
||||
if (!recordId || !isDefined(workflowVersion?.workflow?.id)) {
|
||||
throw new Error(
|
||||
'Record ID and workflow ID are required to use as draft workflow version',
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UseAsDraftWorkflowVersionSingleRecordCommandContent
|
||||
workflowId={workflowVersion.workflow.id}
|
||||
workflowVersionId={workflowVersion.id}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const ActivateWorkflowSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
const { activateWorkflowVersion } = useActivateWorkflowVersion();
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
|
||||
recordId ?? '',
|
||||
);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to activate workflow');
|
||||
}
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!isDefined(workflowWithCurrentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
activateWorkflowVersion({
|
||||
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
|
||||
workflowId: workflowWithCurrentVersion.id,
|
||||
});
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useSidePanelWorkflowNavigation } from '@/side-panel/pages/workflow/hooks/useSidePanelWorkflowNavigation';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const AddNodeWorkflowSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
|
||||
recordId ?? '',
|
||||
);
|
||||
const { openWorkflowCreateStepInSidePanel } =
|
||||
useSidePanelWorkflowNavigation();
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to add node workflow');
|
||||
}
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!isDefined(workflowWithCurrentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
openWorkflowCreateStepInSidePanel(workflowWithCurrentVersion.id);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const DeactivateWorkflowSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
|
||||
recordId ?? '',
|
||||
);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to deactivate workflow');
|
||||
}
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!isDefined(workflowWithCurrentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deactivateWorkflowVersion({
|
||||
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
|
||||
});
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const DiscardDraftWorkflowSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
|
||||
recordId ?? '',
|
||||
);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to discard draft workflow');
|
||||
}
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!isDefined(workflowWithCurrentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteOneWorkflowVersion({
|
||||
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
|
||||
});
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useDuplicateWorkflow } from '@/workflow/hooks/useDuplicateWorkflow';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
export const DuplicateWorkflowSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const workflow = useWorkflowWithCurrentVersion(recordId ?? '');
|
||||
const { duplicateWorkflow } = useDuplicateWorkflow();
|
||||
const navigate = useNavigateApp();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { t } = useLingui();
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to duplicate workflow');
|
||||
}
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (!isDefined(workflow) || !isDefined(workflow.currentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await duplicateWorkflow({
|
||||
workflowIdToDuplicate: workflow.id,
|
||||
workflowVersionIdToCopy: workflow.currentVersion.id,
|
||||
});
|
||||
|
||||
if (isDefined(result) && isNonEmptyString(result.workflowId)) {
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Workflow duplicated successfully`,
|
||||
});
|
||||
|
||||
navigate(AppPath.RecordShowPage, {
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
objectRecordId: result.workflowId,
|
||||
});
|
||||
} else {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to duplicate workflow`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return isDefined(workflow) ? (
|
||||
<HeadlessEngineCommandWrapperEffect execute={handleExecute} />
|
||||
) : null;
|
||||
};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useActiveWorkflowVersion } from '@/workflow/hooks/useActiveWorkflowVersion';
|
||||
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SeeActiveVersionWorkflowSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
|
||||
const { workflowVersion, loading } = useActiveWorkflowVersion({
|
||||
workflowId: recordId ?? '',
|
||||
});
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to see active version workflow');
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordShowPage}
|
||||
params={{
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
objectRecordId: workflowVersion.id,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SeeRunsWorkflowSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
|
||||
recordId ?? '',
|
||||
);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to see runs workflow');
|
||||
}
|
||||
|
||||
return (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.WorkflowRun }}
|
||||
queryParams={{
|
||||
filter: {
|
||||
workflow: {
|
||||
[ViewFilterOperand.IS]: {
|
||||
selectedRecordIds: [workflowWithCurrentVersion?.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SeeVersionsWorkflowSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
|
||||
recordId ?? '',
|
||||
);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to see versions workflow');
|
||||
}
|
||||
|
||||
return (
|
||||
<HeadlessNavigateEngineCommand
|
||||
to={AppPath.RecordIndexPage}
|
||||
params={{ objectNamePlural: CoreObjectNamePlural.WorkflowVersion }}
|
||||
queryParams={{
|
||||
filter: {
|
||||
workflow: {
|
||||
[ViewFilterOperand.IS]: {
|
||||
selectedRecordIds: [workflowWithCurrentVersion?.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const TestWorkflowSingleRecordCommand = () => {
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const { runWorkflowVersion } = useRunWorkflowVersion();
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
|
||||
recordId ?? '',
|
||||
);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to test workflow');
|
||||
}
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!isDefined(workflowWithCurrentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
runWorkflowVersion({
|
||||
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
|
||||
workflowId: workflowWithCurrentVersion.id,
|
||||
});
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
|
||||
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
|
||||
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
import { useTidyUpWorkflowVersion } from '@/workflow/workflow-version/hooks/useTidyUpWorkflowVersion';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const TidyUpWorkflowSingleRecordCommand = () => {
|
||||
const store = useStore();
|
||||
const { selectedRecords } = useMountedEngineCommandContext();
|
||||
|
||||
const recordId = selectedRecords[0]?.id;
|
||||
const { tidyUpWorkflowVersion } = useTidyUpWorkflowVersion();
|
||||
const instanceId = getWorkflowVisualizerComponentInstanceId({
|
||||
recordId: recordId ?? '',
|
||||
});
|
||||
const { getUpdatableWorkflowVersion } =
|
||||
useGetUpdatableWorkflowVersionOrThrow(instanceId);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
throw new Error('Record ID is required to tidy up workflow');
|
||||
}
|
||||
|
||||
const handleExecute = async () => {
|
||||
const workflowDiagramAtom = workflowDiagramComponentState.atomFamily({
|
||||
instanceId,
|
||||
});
|
||||
const workflowDiagram = store.get(workflowDiagramAtom);
|
||||
|
||||
if (!isDefined(workflowDiagram)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowVersionId = await getUpdatableWorkflowVersion();
|
||||
|
||||
await tidyUpWorkflowVersion(workflowVersionId, workflowDiagram);
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const EngineCommandComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { type MountedEngineCommandState } from '@/command-menu-item/engine-command/types/MountedEngineCommandContext';
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const mountedEngineCommandsState = createAtomState<
|
||||
Map<string, MountedEngineCommandState>
|
||||
>({
|
||||
key: 'mountedEngineCommandsState',
|
||||
defaultValue: new Map(),
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import {
|
||||
type Nullable,
|
||||
type RecordGqlOperationFilter,
|
||||
} from 'twenty-shared/types';
|
||||
import { type EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export type MountedEngineCommandState = {
|
||||
engineComponentKey: EngineComponentKey;
|
||||
contextStoreInstanceId: string;
|
||||
objectMetadataItem: Nullable<ObjectMetadataItem>;
|
||||
currentViewId: Nullable<string>;
|
||||
recordIndexId: Nullable<string>;
|
||||
targetedRecordsRule: ContextStoreTargetedRecordsRule;
|
||||
selectedRecords: ObjectRecord[];
|
||||
graphqlFilter: Nullable<RecordGqlOperationFilter>;
|
||||
};
|
||||
+5
-4
@@ -2,7 +2,7 @@ import {
|
||||
CommandMenuContext,
|
||||
type CommandMenuContextType,
|
||||
} from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { useCommandMenuItemFrontComponentCommands } from '@/command-menu-item/server-items/hooks/useCommandMenuItemFrontComponentCommands';
|
||||
import { useCommandMenuItemsFromBackend } from '@/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend';
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
|
||||
type CommandMenuContextProviderServerItemsContentProps = {
|
||||
@@ -21,8 +21,9 @@ export const CommandMenuContextProviderServerItemsContent = ({
|
||||
}: CommandMenuContextProviderServerItemsContentProps & {
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
}) => {
|
||||
const commandMenuItemFrontComponentActions =
|
||||
useCommandMenuItemFrontComponentCommands(commandMenuContextApi);
|
||||
const commandMenuItems = useCommandMenuItemsFromBackend(
|
||||
commandMenuContextApi,
|
||||
);
|
||||
|
||||
return (
|
||||
<CommandMenuContext.Provider
|
||||
@@ -30,7 +31,7 @@ export const CommandMenuContextProviderServerItemsContent = ({
|
||||
isInSidePanel,
|
||||
displayType,
|
||||
containerType,
|
||||
commandMenuItems: commandMenuItemFrontComponentActions,
|
||||
commandMenuItems,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
+23
-4
@@ -1,14 +1,16 @@
|
||||
import { ENGINE_COMPONENT_KEY_COMPONENT_MAP } from '@/command-menu-item/constants/EngineComponentKeyComponentMap';
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { HeadlessFrontComponentCommandMenuItem } from '@/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem';
|
||||
import { useMountEngineCommand } from '@/command-menu-item/engine-command/hooks/useMountEngineCommand';
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useMountHeadlessFrontComponent } from '@/front-components/hooks/useMountHeadlessFrontComponent';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
@@ -129,6 +131,12 @@ type BuildCommandMenuItemFromStandardKeyParams = {
|
||||
isPinned: boolean;
|
||||
getIcon: ReturnType<typeof useIcons>['getIcon'];
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
mountEngineCommand: (
|
||||
engineCommandId: string,
|
||||
contextStoreInstanceId: string,
|
||||
engineComponentKey: EngineComponentKey,
|
||||
) => void;
|
||||
contextStoreInstanceId: string;
|
||||
};
|
||||
|
||||
const buildCommandItemFromEngineKey = ({
|
||||
@@ -139,10 +147,14 @@ const buildCommandItemFromEngineKey = ({
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
mountEngineCommand,
|
||||
contextStoreInstanceId,
|
||||
}: BuildCommandMenuItemFromStandardKeyParams) => {
|
||||
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
const component = ENGINE_COMPONENT_KEY_COMPONENT_MAP[engineComponentKey];
|
||||
const handleClick = () => {
|
||||
mountEngineCommand(item.id, contextStoreInstanceId, engineComponentKey);
|
||||
};
|
||||
|
||||
return {
|
||||
type,
|
||||
@@ -159,16 +171,21 @@ const buildCommandItemFromEngineKey = ({
|
||||
item.conditionalAvailabilityExpression,
|
||||
commandMenuContextApi,
|
||||
),
|
||||
component,
|
||||
component: <Command onClick={handleClick} />,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCommandMenuItemFrontComponentCommands = (
|
||||
export const useCommandMenuItemsFromBackend = (
|
||||
commandMenuContextApi: CommandMenuContextApi,
|
||||
) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
|
||||
const mountHeadlessFrontComponent = useMountHeadlessFrontComponent();
|
||||
const mountEngineCommand = useMountEngineCommand();
|
||||
|
||||
const contextStoreInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
ContextStoreComponentInstanceContext,
|
||||
);
|
||||
|
||||
const contextStoreIsPageInEditMode = useAtomComponentStateValue(
|
||||
contextStoreIsPageInEditModeComponentState,
|
||||
@@ -236,6 +253,8 @@ export const useCommandMenuItemFrontComponentCommands = (
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
mountEngineCommand,
|
||||
contextStoreInstanceId,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useContextStoreObjectMetadataItemOrThrow = (
|
||||
contextStoreInstanceId?: string,
|
||||
|
||||
+28
-23
@@ -1,5 +1,7 @@
|
||||
import { Suspense, lazy } from 'react';
|
||||
|
||||
import { CommandMenuItemErrorBoundary } from '@/command-menu-item/display/components/CommandMenuItemErrorBoundary';
|
||||
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
|
||||
import { mountedHeadlessFrontComponentMapsState } from '@/front-components/states/mountedHeadlessFrontComponentMapsState';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -16,34 +18,37 @@ export const HeadlessFrontComponentMountRoot = () => {
|
||||
const mountedHeadlessFrontComponentMaps = useAtomStateValue(
|
||||
mountedHeadlessFrontComponentMapsState,
|
||||
);
|
||||
|
||||
if (mountedHeadlessFrontComponentMaps.size === 0) {
|
||||
return null;
|
||||
}
|
||||
const unmountHeadlessFrontComponent = useUnmountHeadlessFrontComponent();
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...mountedHeadlessFrontComponentMaps.entries()].map(
|
||||
([frontComponentId, mountContext]) => (
|
||||
<Suspense key={frontComponentId} fallback={null}>
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier:
|
||||
isDefined(mountContext) &&
|
||||
isDefined(mountContext.objectNameSingular)
|
||||
? {
|
||||
id: mountContext.recordId,
|
||||
targetObjectNameSingular:
|
||||
mountContext.objectNameSingular,
|
||||
}
|
||||
: undefined,
|
||||
layoutType: PageLayoutType.DASHBOARD,
|
||||
isInSidePanel: false,
|
||||
}}
|
||||
>
|
||||
<FrontComponentRenderer frontComponentId={frontComponentId} />
|
||||
</LayoutRenderingProvider>
|
||||
</Suspense>
|
||||
<CommandMenuItemErrorBoundary
|
||||
key={frontComponentId}
|
||||
engineCommandId={frontComponentId}
|
||||
onError={() => unmountHeadlessFrontComponent(frontComponentId)}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier:
|
||||
isDefined(mountContext) &&
|
||||
isDefined(mountContext.objectNameSingular)
|
||||
? {
|
||||
id: mountContext.recordId,
|
||||
targetObjectNameSingular:
|
||||
mountContext.objectNameSingular,
|
||||
}
|
||||
: undefined,
|
||||
layoutType: PageLayoutType.DASHBOARD,
|
||||
isInSidePanel: false,
|
||||
}}
|
||||
>
|
||||
<FrontComponentRenderer frontComponentId={frontComponentId} />
|
||||
</LayoutRenderingProvider>
|
||||
</Suspense>
|
||||
</CommandMenuItemErrorBoundary>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
|
||||
+5
-5
@@ -5,13 +5,13 @@ import {
|
||||
} from 'twenty-sdk/front-component-renderer';
|
||||
import { type AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types';
|
||||
|
||||
import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
|
||||
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
|
||||
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
|
||||
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 { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -75,7 +75,7 @@ export const useFrontComponentExecutionContext = ({
|
||||
const openCommandConfirmationModal: FrontComponentHostCommunicationApi['openCommandConfirmationModal'] =
|
||||
async ({ title, subtitle, confirmButtonText, confirmButtonAccent }) => {
|
||||
openConfirmationModal({
|
||||
frontComponentId,
|
||||
caller: { type: 'frontComponent', frontComponentId },
|
||||
title,
|
||||
subtitle,
|
||||
confirmButtonText,
|
||||
|
||||
+2
-1
@@ -6,9 +6,10 @@ import { useStore } from 'jotai';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useRemoveRecordFilter = () => {
|
||||
export const useRemoveRecordFilter = (instanceId?: string) => {
|
||||
const currentRecordFilters = useAtomComponentStateCallbackState(
|
||||
currentRecordFiltersComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
+2
-1
@@ -5,9 +5,10 @@ import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/h
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
export const useUpsertRecordFilter = () => {
|
||||
export const useUpsertRecordFilter = (instanceId?: string) => {
|
||||
const currentRecordFilters = useAtomComponentStateCallbackState(
|
||||
currentRecordFiltersComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
+2
@@ -89,6 +89,7 @@ export const useRecordIndexLazyFetchRecords = ({
|
||||
|
||||
const findManyRecordsParams = useFindManyRecordIndexTableParams(
|
||||
objectMetadataItem.nameSingular,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const queryFilter = computeContextStoreFilters({
|
||||
@@ -102,6 +103,7 @@ export const useRecordIndexLazyFetchRecords = ({
|
||||
|
||||
const visibleRecordFields = useAtomComponentSelectorValue(
|
||||
visibleRecordFieldsComponentSelector,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const finalColumns: Pick<
|
||||
|
||||
+5
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
export const useFindManyRecordIndexTableParams = (
|
||||
objectNameSingular: string,
|
||||
instanceId?: string,
|
||||
) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@@ -31,14 +32,17 @@ export const useFindManyRecordIndexTableParams = (
|
||||
|
||||
const currentRecordFilterGroups = useAtomComponentStateValue(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const currentRecordSorts = useAtomComponentStateValue(
|
||||
currentRecordSortsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
@@ -52,6 +56,7 @@ export const useFindManyRecordIndexTableParams = (
|
||||
|
||||
const anyFieldFilterValue = useAtomComponentStateValue(
|
||||
anyFieldFilterValueComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { recordGqlOperationFilter: anyFieldFilter } =
|
||||
|
||||
+3
-1
@@ -16,11 +16,13 @@ import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
type UseHandleToggleTrashColumnFilterProps = {
|
||||
objectNameSingular: string;
|
||||
viewBarId: string;
|
||||
recordFiltersInstanceId?: string;
|
||||
};
|
||||
|
||||
export const useHandleToggleTrashColumnFilter = ({
|
||||
viewBarId,
|
||||
objectNameSingular,
|
||||
recordFiltersInstanceId,
|
||||
}: UseHandleToggleTrashColumnFilterProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@@ -35,7 +37,7 @@ export const useHandleToggleTrashColumnFilter = ({
|
||||
viewBarId,
|
||||
);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter(recordFiltersInstanceId);
|
||||
|
||||
const handleToggleTrashColumnFilter = useCallback(() => {
|
||||
const trashFieldMetadata = objectMetadataItem.fields.find(
|
||||
|
||||
+3
@@ -9,11 +9,14 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
|
||||
export const useBuildRecordInputFromFilters = ({
|
||||
objectMetadataItem,
|
||||
instanceId,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
instanceId?: string;
|
||||
}) => {
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
|
||||
+6
@@ -25,23 +25,28 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
type UseCreateNewIndexRecordProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
instanceId?: string;
|
||||
};
|
||||
|
||||
export const useCreateNewIndexRecord = ({
|
||||
objectMetadataItem,
|
||||
instanceId,
|
||||
}: UseCreateNewIndexRecordProps) => {
|
||||
const recordGroupDefinitions = useAtomComponentSelectorValue(
|
||||
recordGroupDefinitionsComponentSelector,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
const recordIndexRecordIdsByGroupCallbackState =
|
||||
useAtomComponentFamilyStateCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const recordIndexGroupFieldMetadataItem = useAtomComponentStateValue(
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { openRecordInSidePanel } = useOpenRecordInSidePanel();
|
||||
@@ -59,6 +64,7 @@ export const useCreateNewIndexRecord = ({
|
||||
|
||||
const { buildRecordInputFromFilters } = useBuildRecordInputFromFilters({
|
||||
objectMetadataItem,
|
||||
instanceId,
|
||||
});
|
||||
|
||||
const { buildRecordInputFromRLSPredicates } =
|
||||
|
||||
+7
-3
@@ -1,6 +1,7 @@
|
||||
import { ThreadWebWorker, release, retain } from '@quilted/threads';
|
||||
import { RemoteReceiver } from '@remote-dom/core/receivers';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { type ConfirmationModalCaller } from 'twenty-shared/types';
|
||||
import { type CommandConfirmationModalResult } from '../../../sdk/front-component-api/globals/frontComponentHostCommunicationApi';
|
||||
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
|
||||
import { type WorkerExports } from '../../types/WorkerExports';
|
||||
@@ -11,7 +12,7 @@ const COMMAND_MENU_ITEM_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME =
|
||||
'command-menu-item-confirmation-modal-result';
|
||||
|
||||
type CommandMenuItemConfirmationModalResultBrowserEventDetail = {
|
||||
frontComponentId: string;
|
||||
caller: ConfirmationModalCaller;
|
||||
confirmationResult: CommandConfirmationModalResult;
|
||||
};
|
||||
|
||||
@@ -73,9 +74,12 @@ export const FrontComponentWorkerEffect = ({
|
||||
const commandMenuItemConfirmationModalResultBrowserEventDetail =
|
||||
event.detail;
|
||||
|
||||
const caller =
|
||||
commandMenuItemConfirmationModalResultBrowserEventDetail.caller;
|
||||
|
||||
if (
|
||||
commandMenuItemConfirmationModalResultBrowserEventDetail.frontComponentId !==
|
||||
frontComponentId
|
||||
caller.type !== 'frontComponent' ||
|
||||
caller.frontComponentId !== frontComponentId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
enqueueSnackbar,
|
||||
getFrontComponentCommandErrorDedupeKey,
|
||||
unmountFrontComponent,
|
||||
useFrontComponentId,
|
||||
} from '../front-component-api';
|
||||
@@ -24,20 +22,9 @@ export const Command = ({ execute }: CommandProps) => {
|
||||
setHasExecuted(true);
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await execute();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await enqueueSnackbar({
|
||||
message: 'Command failed',
|
||||
detailedMessage: error.message,
|
||||
variant: 'error',
|
||||
dedupeKey: getFrontComponentCommandErrorDedupeKey(frontComponentId),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await unmountFrontComponent();
|
||||
}
|
||||
await execute();
|
||||
|
||||
await unmountFrontComponent();
|
||||
};
|
||||
|
||||
run();
|
||||
|
||||
@@ -4,8 +4,6 @@ import { type NavigateOptions } from 'react-router-dom';
|
||||
import { type AppPath } from 'twenty-shared/types';
|
||||
import { type getAppPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
enqueueSnackbar,
|
||||
getFrontComponentCommandErrorDedupeKey,
|
||||
navigate,
|
||||
unmountFrontComponent,
|
||||
useFrontComponentId,
|
||||
@@ -36,20 +34,9 @@ export const CommandLink = <T extends AppPath>({
|
||||
setHasExecuted(true);
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await navigate(to, params, queryParams, options);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await enqueueSnackbar({
|
||||
message: 'Command failed',
|
||||
detailedMessage: error.message,
|
||||
variant: 'error',
|
||||
dedupeKey: getFrontComponentCommandErrorDedupeKey(frontComponentId),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await unmountFrontComponent();
|
||||
}
|
||||
await navigate(to, params, queryParams, options);
|
||||
|
||||
await unmountFrontComponent();
|
||||
};
|
||||
|
||||
run();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
enqueueSnackbar,
|
||||
getFrontComponentCommandErrorDedupeKey,
|
||||
openCommandConfirmationModal,
|
||||
unmountFrontComponent,
|
||||
useFrontComponentId,
|
||||
@@ -36,30 +34,20 @@ export const CommandModal = ({
|
||||
setHasExecuted(true);
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
const commandConfirmationModalResult =
|
||||
await openCommandConfirmationModal({
|
||||
title,
|
||||
subtitle,
|
||||
confirmButtonText,
|
||||
confirmButtonAccent,
|
||||
});
|
||||
const commandConfirmationModalResult = await openCommandConfirmationModal(
|
||||
{
|
||||
title,
|
||||
subtitle,
|
||||
confirmButtonText,
|
||||
confirmButtonAccent,
|
||||
},
|
||||
);
|
||||
|
||||
if (commandConfirmationModalResult === 'confirm') {
|
||||
await execute();
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Command failed',
|
||||
detailedMessage: message,
|
||||
variant: 'error',
|
||||
dedupeKey: getFrontComponentCommandErrorDedupeKey(frontComponentId),
|
||||
});
|
||||
} finally {
|
||||
await unmountFrontComponent();
|
||||
if (commandConfirmationModalResult === 'confirm') {
|
||||
await execute();
|
||||
}
|
||||
|
||||
await unmountFrontComponent();
|
||||
};
|
||||
|
||||
run();
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
enqueueSnackbar,
|
||||
getFrontComponentCommandErrorDedupeKey,
|
||||
openSidePanelPage,
|
||||
unmountFrontComponent,
|
||||
useFrontComponentId,
|
||||
@@ -38,25 +36,14 @@ export const CommandOpenSidePanelPage = ({
|
||||
const run = async () => {
|
||||
onClick?.();
|
||||
|
||||
try {
|
||||
await openSidePanelPage({
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
shouldResetSearchState,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await enqueueSnackbar({
|
||||
message: 'Command failed',
|
||||
detailedMessage: error.message,
|
||||
variant: 'error',
|
||||
dedupeKey: getFrontComponentCommandErrorDedupeKey(frontComponentId),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await unmountFrontComponent();
|
||||
}
|
||||
await openSidePanelPage({
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
shouldResetSearchState,
|
||||
});
|
||||
|
||||
await unmountFrontComponent();
|
||||
};
|
||||
|
||||
run();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type ConfirmationModalCaller =
|
||||
| { type: 'frontComponent'; frontComponentId: string }
|
||||
| { type: 'engineCommand'; engineCommandId: string };
|
||||
@@ -53,6 +53,7 @@ export {
|
||||
} from './composite-types/rich-text.composite-type';
|
||||
export type { CompositeFieldSubFieldName } from './CompositeFieldSubFieldNameType';
|
||||
export type { ConfigVariableValue } from './ConfigVariableValue';
|
||||
export type { ConfirmationModalCaller } from './ConfirmationModalCaller';
|
||||
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
|
||||
export { CoreObjectNameSingular } from './CoreObjectNameSingular';
|
||||
export { CrudOperationType } from './CrudOperationType';
|
||||
|
||||
Reference in New Issue
Block a user