## Summary
- Both \`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED\` and
\`IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED\` are force-enabled on
every existing workspace by the 1.23.0 upgrade command
\`BackfillRecordPageLayoutsCommand\` and seeded enabled for new
workspaces via \`DEFAULT_FEATURE_FLAGS\` +
\`seed-feature-flags.util.ts\`. They are no longer load-bearing.
- Unwrap all \`if (flag) { … }\` conditionals to their enabled branch on
both server and front.
- Delete legacy fallback files that only the disabled branch reached:
\`PageLayoutRelationWidgetsSyncEffect\`,
\`usePageLayoutWithRelationWidgets\`,
\`reInjectDynamicRelationWidgetsFromDraft\`,
\`injectRelationWidgetsIntoLayout\`, \`isDynamicRelationWidget\` (and
their tests).
- Strip the two \`enableFeatureFlags\` calls from the 1.23 upgrade
command — the page-layout backfill data logic itself is kept intact
since old workspaces upgrading from < 1.23 still need it.
- No DB cleanup migration: stale \`featureFlag\` rows are left in place,
matching the precedent set by #20531 and #20460.
Net diff: 37 files, +106 / -1727.
## Test plan
- [x] \`npx nx typecheck twenty-shared twenty-server twenty-front\` —
all pass
- [x] \`npx nx lint:diff-with-main twenty-server twenty-front\` — all
pass
- [x] \`cd packages/twenty-front && npx jest page-layout\` — 1240 tests,
all pass
- [x] \`cd packages/twenty-server && npx jest
workspace-entity-manager.spec\` — pass
- [ ] Manual smoke: open a record page, verify tabs render and \"Edit
Layout\" command-menu action is available
- [ ] Manual smoke: Settings → Data model → object → Layout tab is
visible (and hidden for remote / Dashboard objects)
- [ ] Manual smoke: edit a tab title, save, reload — confirm persistence
184 lines
7.3 KiB
TypeScript
184 lines
7.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 { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
|
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { useStore } from 'jotai';
|
|
import { useCallback, useState } from 'react';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
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 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);
|
|
const result = await updatePageLayoutWithTabsAndWidgets(
|
|
pageLayoutId,
|
|
updateInput,
|
|
);
|
|
|
|
if (result.status === 'successful') {
|
|
const updatedPageLayout =
|
|
result.response.data?.updatePageLayoutWithTabsAndWidgets;
|
|
|
|
if (isDefined(updatedPageLayout)) {
|
|
const persistedLayout: PageLayout =
|
|
transformPageLayout(updatedPageLayout);
|
|
|
|
store.set(
|
|
pageLayoutPersistedComponentState.atomFamily({
|
|
instanceId: pageLayoutId,
|
|
}),
|
|
persistedLayout,
|
|
);
|
|
store.set(
|
|
pageLayoutCurrentLayoutsComponentState.atomFamily({
|
|
instanceId: pageLayoutId,
|
|
}),
|
|
convertPageLayoutToTabLayouts(persistedLayout),
|
|
);
|
|
}
|
|
} 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,
|
|
store,
|
|
t,
|
|
]);
|
|
|
|
return { save, isSaving };
|
|
};
|