Files
twenty/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts
T

204 lines
8.3 KiB
TypeScript

import { useSaveCommandMenuItemsDraft } from '@/command-menu-item/edit/hooks/useSaveCommandMenuItemsDraft';
import { useCommandMenuItemsDraftState } from '@/command-menu-item/hooks/useCommandMenuItemsDraftState';
import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews';
import { useCreatePendingRecordTableWidgetViews } from '@/page-layout/hooks/useCreatePendingRecordTableWidgetViews';
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { isDefaultPageLayoutId } from '@/page-layout/utils/isDefaultPageLayoutId';
import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useLingui } from '@lingui/react/macro';
import { useStore } from 'jotai';
import { useCallback, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { logError } from '~/utils/logError';
export const useSaveLayoutCustomization = () => {
const [isSaving, setIsSaving] = useState(false);
const store = useStore();
const { t } = useLingui();
const { saveDraft } = useSaveNavigationMenuItemsDraft();
const { saveCommandMenuItemsDraft } = useSaveCommandMenuItemsDraft();
const { isDirty: isCommandMenuItemsDirty } = useCommandMenuItemsDraftState();
const { enqueueErrorSnackBar } = useSnackBar();
const { updatePageLayoutWithTabsAndWidgets } =
useUpdatePageLayoutWithTabsAndWidgets();
const { createPendingFieldsWidgetViews } =
useCreatePendingFieldsWidgetViews();
const { createPendingRecordTableWidgetViews } =
useCreatePendingRecordTableWidgetViews();
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
const featureFlags = useFeatureFlagsMap();
const isRecordPageLayoutEditingEnabled =
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
const save = useCallback(async () => {
setIsSaving(true);
try {
const navigationDraft = store.get(navigationMenuItemsDraftState.atom);
const prefetchItems = store.get(navigationMenuItemsSelector.atom);
const workspaceItems = filterWorkspaceNavigationMenuItems(prefetchItems);
const isNavigationDirty =
isDefined(navigationDraft) &&
!isDeeplyEqual(navigationDraft, workspaceItems);
// TODO: consider a single server mutation (e.g. saveLayoutCustomization)
// that saves navigation + page layouts + field widgets in one transaction.
// Currently, partial failure leaves mixed state — navigation may commit
// while page layouts fail.
if (isNavigationDirty) {
await saveDraft();
}
if (isCommandMenuItemsDirty) {
await saveCommandMenuItemsDraft();
}
const activePageLayoutIds = store.get(
activeCustomizationPageLayoutIdsState.atom,
);
let hasAnyFailure = false;
for (const pageLayoutId of activePageLayoutIds) {
if (isDefaultPageLayoutId(pageLayoutId)) {
continue;
}
const draft = store.get(
pageLayoutDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const persisted = store.get(
pageLayoutPersistedComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
if (!isDefined(draft) || !isDefined(persisted)) {
continue;
}
const persistedAsDraft: DraftPageLayout = {
id: persisted.id,
name: persisted.name,
type: persisted.type,
objectMetadataId: persisted.objectMetadataId,
tabs: persisted.tabs,
defaultTabToFocusOnMobileAndSidePanelId:
persisted.defaultTabToFocusOnMobileAndSidePanelId,
};
const isPageLayoutStructureDirty = !isDeeplyEqual(
draft,
persistedAsDraft,
);
await createPendingFieldsWidgetViews(pageLayoutId);
await createPendingRecordTableWidgetViews(pageLayoutId);
if (isPageLayoutStructureDirty) {
const updateInput = convertPageLayoutDraftToUpdateInput(draft, {
shouldFilterDynamicRelationWidgets:
!isRecordPageLayoutEditingEnabled,
});
const result = await updatePageLayoutWithTabsAndWidgets(
pageLayoutId,
updateInput,
);
if (result.status === 'successful') {
const updatedPageLayout =
result.response.data?.updatePageLayoutWithTabsAndWidgets;
if (isDefined(updatedPageLayout)) {
const persistedLayout: PageLayout =
transformPageLayout(updatedPageLayout);
const pageLayoutToPersist =
!isRecordPageLayoutEditingEnabled &&
persistedLayout.type === PageLayoutType.RECORD_PAGE
? reInjectDynamicRelationWidgetsFromDraft(
persistedLayout,
draft,
)
: persistedLayout;
store.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: pageLayoutId,
}),
pageLayoutToPersist,
);
store.set(
pageLayoutCurrentLayoutsComponentState.atomFamily({
instanceId: pageLayoutId,
}),
convertPageLayoutToTabLayouts(pageLayoutToPersist),
);
}
} else {
// goes away with a single server mutation (see TODO above)
hasAnyFailure = true;
continue;
}
}
await savePageLayoutWidgetsData(pageLayoutId);
}
if (hasAnyFailure) {
enqueueErrorSnackBar({
message: t`Some layout changes could not be saved`,
});
return;
}
exitLayoutCustomizationMode();
} catch (error) {
logError(error);
enqueueErrorSnackBar({
message: t`Failed to save layout customization`,
});
} finally {
setIsSaving(false);
}
}, [
saveDraft,
saveCommandMenuItemsDraft,
isCommandMenuItemsDirty,
createPendingFieldsWidgetViews,
createPendingRecordTableWidgetViews,
updatePageLayoutWithTabsAndWidgets,
savePageLayoutWidgetsData,
exitLayoutCustomizationMode,
enqueueErrorSnackBar,
isRecordPageLayoutEditingEnabled,
store,
t,
]);
return { save, isSaving };
};