chore(page-layout): remove IS_RECORD_PAGE_LAYOUT_* feature flags (#20556)
## 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
This commit is contained in:
@@ -1750,12 +1750,10 @@ enum FeatureFlagKey {
|
||||
IS_UNIQUE_INDEXES_ENABLED
|
||||
IS_JSON_FILTER_ENABLED
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
|
||||
IS_PUBLIC_DOMAIN_ENABLED
|
||||
IS_EMAILING_DOMAIN_ENABLED
|
||||
IS_EMAIL_GROUP_ENABLED
|
||||
IS_JUNCTION_RELATIONS_ENABLED
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT
|
||||
}
|
||||
|
||||
|
||||
@@ -1391,7 +1391,7 @@ export interface FeatureFlag {
|
||||
__typename: 'FeatureFlag'
|
||||
}
|
||||
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT'
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT'
|
||||
|
||||
export interface WorkspaceUrls {
|
||||
customUrl?: Scalars['String']
|
||||
@@ -8762,12 +8762,10 @@ export const enumFeatureFlagKey = {
|
||||
IS_UNIQUE_INDEXES_ENABLED: 'IS_UNIQUE_INDEXES_ENABLED' as const,
|
||||
IS_JSON_FILTER_ENABLED: 'IS_JSON_FILTER_ENABLED' as const,
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE: 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' as const,
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' as const,
|
||||
IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const,
|
||||
IS_EMAILING_DOMAIN_ENABLED: 'IS_EMAILING_DOMAIN_ENABLED' as const,
|
||||
IS_EMAIL_GROUP_ENABLED: 'IS_EMAIL_GROUP_ENABLED' as const,
|
||||
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const,
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const
|
||||
}
|
||||
|
||||
|
||||
@@ -276,8 +276,6 @@ export enum FeatureFlagKey {
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED = 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED',
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
||||
}
|
||||
|
||||
@@ -1647,8 +1647,6 @@ export enum FeatureFlagKey {
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED = 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED',
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
||||
}
|
||||
|
||||
+3
-23
@@ -18,15 +18,12 @@ 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';
|
||||
|
||||
@@ -48,10 +45,6 @@ export const useSaveLayoutCustomization = () => {
|
||||
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 {
|
||||
@@ -119,10 +112,7 @@ export const useSaveLayoutCustomization = () => {
|
||||
await createPendingRecordTableWidgetViews(pageLayoutId);
|
||||
|
||||
if (isPageLayoutStructureDirty) {
|
||||
const updateInput = convertPageLayoutDraftToUpdateInput(draft, {
|
||||
shouldFilterDynamicRelationWidgets:
|
||||
!isRecordPageLayoutEditingEnabled,
|
||||
});
|
||||
const updateInput = convertPageLayoutDraftToUpdateInput(draft);
|
||||
const result = await updatePageLayoutWithTabsAndWidgets(
|
||||
pageLayoutId,
|
||||
updateInput,
|
||||
@@ -136,26 +126,17 @@ export const useSaveLayoutCustomization = () => {
|
||||
const persistedLayout: PageLayout =
|
||||
transformPageLayout(updatedPageLayout);
|
||||
|
||||
const pageLayoutToPersist =
|
||||
!isRecordPageLayoutEditingEnabled &&
|
||||
persistedLayout.type === PageLayoutType.RECORD_PAGE
|
||||
? reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
)
|
||||
: persistedLayout;
|
||||
|
||||
store.set(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
pageLayoutToPersist,
|
||||
persistedLayout,
|
||||
);
|
||||
store.set(
|
||||
pageLayoutCurrentLayoutsComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
convertPageLayoutToTabLayouts(pageLayoutToPersist),
|
||||
convertPageLayoutToTabLayouts(persistedLayout),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -194,7 +175,6 @@ export const useSaveLayoutCustomization = () => {
|
||||
savePageLayoutWidgetsData,
|
||||
exitLayoutCustomizationMode,
|
||||
enqueueErrorSnackBar,
|
||||
isRecordPageLayoutEditingEnabled,
|
||||
store,
|
||||
t,
|
||||
]);
|
||||
|
||||
@@ -9,10 +9,8 @@ import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/u
|
||||
import { useReorderPageLayoutWidgets } from '@/page-layout/hooks/useReorderPageLayoutWidgets';
|
||||
import { StandaloneWidgetPlaceholder } from '@/page-layout/widgets/components/StandaloneWidgetPlaceholder';
|
||||
import { RecordPageAddWidgetSection } from '@/page-layout/widgets/components/RecordPageAddWidgetSection';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@@ -38,10 +36,6 @@ export const PageLayoutContent = () => {
|
||||
const isRecordPageLayout =
|
||||
currentPageLayout.type === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
);
|
||||
|
||||
const isCanvasLayout = layoutMode === PageLayoutTabLayoutMode.CANVAS;
|
||||
const isVerticalList = layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST;
|
||||
|
||||
@@ -62,19 +56,13 @@ export const PageLayoutContent = () => {
|
||||
}
|
||||
|
||||
if (isVerticalList) {
|
||||
if (
|
||||
isPageLayoutInEditMode &&
|
||||
isRecordPageLayout &&
|
||||
isRecordPageGlobalEditionEnabled
|
||||
) {
|
||||
if (isPageLayoutInEditMode && isRecordPageLayout) {
|
||||
return (
|
||||
<PageLayoutVerticalListEditor
|
||||
widgets={activeTab.widgets}
|
||||
onReorder={reorderWidgets}
|
||||
isReorderEnabled={true}
|
||||
trailingElement={
|
||||
isRecordPageLayout ? <RecordPageAddWidgetSection /> : undefined
|
||||
}
|
||||
trailingElement={<RecordPageAddWidgetSection />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-60
@@ -1,5 +1,4 @@
|
||||
import { useBasePageLayout } from '@/page-layout/hooks/useBasePageLayout';
|
||||
import { usePageLayoutWithRelationWidgets } from '@/page-layout/hooks/usePageLayoutWithRelationWidgets';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
@@ -8,27 +7,24 @@ import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayo
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import { isPageLayoutEmpty } from '@/page-layout/utils/isPageLayoutEmpty';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type PageLayoutInitializationQueryEffectProps = {
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
const PageLayoutInitializationEffect = ({
|
||||
// oxlint-disable-next-line twenty/effect-components
|
||||
export const PageLayoutInitializationQueryEffect = ({
|
||||
pageLayoutId,
|
||||
pageLayout,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
pageLayout: PageLayout | undefined;
|
||||
}) => {
|
||||
}: PageLayoutInitializationQueryEffectProps) => {
|
||||
const pageLayout = useBasePageLayout(pageLayoutId);
|
||||
|
||||
const [pageLayoutIsInitialized, setPageLayoutIsInitialized] =
|
||||
useAtomComponentState(pageLayoutIsInitializedComponentState);
|
||||
|
||||
@@ -99,53 +95,3 @@ const PageLayoutInitializationEffect = ({
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const PageLayoutInitializationWithRelationWidgets = ({
|
||||
pageLayoutId,
|
||||
basePageLayout,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
basePageLayout: PageLayout | undefined;
|
||||
}) => {
|
||||
const pageLayout = usePageLayoutWithRelationWidgets(basePageLayout);
|
||||
|
||||
return (
|
||||
<PageLayoutInitializationEffect
|
||||
pageLayoutId={pageLayoutId}
|
||||
pageLayout={pageLayout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// oxlint-disable-next-line twenty/effect-components
|
||||
export const PageLayoutInitializationQueryEffect = ({
|
||||
pageLayoutId,
|
||||
}: PageLayoutInitializationQueryEffectProps) => {
|
||||
const { layoutType } = useLayoutRenderingContext();
|
||||
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
|
||||
|
||||
const basePageLayout = useBasePageLayout(pageLayoutId);
|
||||
|
||||
const needsRelationWidgets =
|
||||
layoutType === PageLayoutType.RECORD_PAGE &&
|
||||
!isRecordPageLayoutEditingEnabled;
|
||||
|
||||
if (needsRelationWidgets) {
|
||||
return (
|
||||
<PageLayoutInitializationWithRelationWidgets
|
||||
pageLayoutId={pageLayoutId}
|
||||
basePageLayout={basePageLayout}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayoutInitializationEffect
|
||||
pageLayoutId={pageLayoutId}
|
||||
pageLayout={basePageLayout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
-193
@@ -1,193 +0,0 @@
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
|
||||
import { useBasePageLayout } from '@/page-layout/hooks/useBasePageLayout';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import { injectRelationWidgetsIntoLayout } from '@/page-layout/utils/injectRelationWidgetsIntoLayout';
|
||||
import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutType, WidgetType } from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type PageLayoutRelationWidgetsSyncEffectProps = {
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
export const PageLayoutRelationWidgetsSyncEffect = ({
|
||||
pageLayoutId,
|
||||
}: PageLayoutRelationWidgetsSyncEffectProps) => {
|
||||
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
|
||||
|
||||
const pageLayoutIsInitialized = useAtomComponentStateValue(
|
||||
pageLayoutIsInitializedComponentState,
|
||||
);
|
||||
|
||||
const basePageLayout = useBasePageLayout(pageLayoutId);
|
||||
|
||||
const { boxedRelationFieldMetadataItems } = useFieldListFieldMetadataItems({
|
||||
objectNameSingular: targetRecordIdentifier?.targetObjectNameSingular ?? '',
|
||||
});
|
||||
|
||||
const pageLayoutPersistedComponentCallbackState =
|
||||
useAtomComponentStateCallbackState(pageLayoutPersistedComponentState);
|
||||
|
||||
const pageLayoutDraftComponentCallbackState =
|
||||
useAtomComponentStateCallbackState(pageLayoutDraftComponentState);
|
||||
|
||||
const pageLayoutCurrentLayoutsComponentCallbackState =
|
||||
useAtomComponentStateCallbackState(pageLayoutCurrentLayoutsComponentState);
|
||||
|
||||
const store = useStore();
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
|
||||
const getDraftWithSyncedDynamicRelationWidgets = useCallback(
|
||||
(
|
||||
currentDraft: DraftPageLayout,
|
||||
layoutWithRelationWidgets: PageLayout,
|
||||
): DraftPageLayout => {
|
||||
return {
|
||||
...currentDraft,
|
||||
tabs: currentDraft.tabs.map((draftTab) => {
|
||||
const persistedTab = layoutWithRelationWidgets.tabs.find(
|
||||
(tab) => tab.id === draftTab.id,
|
||||
);
|
||||
|
||||
if (!isDefined(persistedTab)) {
|
||||
return draftTab;
|
||||
}
|
||||
|
||||
const dynamicRelationWidgets = persistedTab.widgets.filter(
|
||||
isDynamicRelationWidget,
|
||||
);
|
||||
|
||||
const nonDynamicWidgets = draftTab.widgets.filter(
|
||||
(widget) => !isDynamicRelationWidget(widget),
|
||||
);
|
||||
|
||||
if (dynamicRelationWidgets.length === 0) {
|
||||
return {
|
||||
...draftTab,
|
||||
widgets: nonDynamicWidgets,
|
||||
};
|
||||
}
|
||||
|
||||
const firstFieldsWidgetIndex = nonDynamicWidgets.findIndex(
|
||||
(widget) => widget.type === WidgetType.FIELDS,
|
||||
);
|
||||
|
||||
if (firstFieldsWidgetIndex === -1) {
|
||||
return {
|
||||
...draftTab,
|
||||
widgets: [...nonDynamicWidgets, ...dynamicRelationWidgets],
|
||||
};
|
||||
}
|
||||
|
||||
const widgetsBeforeFields = nonDynamicWidgets.slice(
|
||||
0,
|
||||
firstFieldsWidgetIndex + 1,
|
||||
);
|
||||
const widgetsAfterFields = nonDynamicWidgets.slice(
|
||||
firstFieldsWidgetIndex + 1,
|
||||
);
|
||||
|
||||
return {
|
||||
...draftTab,
|
||||
widgets: [
|
||||
...widgetsBeforeFields,
|
||||
...dynamicRelationWidgets,
|
||||
...widgetsAfterFields,
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const syncPageLayoutWithRelationWidgets = useCallback(
|
||||
(layout: PageLayout) => {
|
||||
const currentPersisted = store.get(
|
||||
pageLayoutPersistedComponentCallbackState,
|
||||
);
|
||||
|
||||
if (!isDeeplyEqual(layout, currentPersisted)) {
|
||||
store.set(pageLayoutPersistedComponentCallbackState, layout);
|
||||
|
||||
const currentDraft = store.get(pageLayoutDraftComponentCallbackState);
|
||||
|
||||
const nextDraft = isLayoutCustomizationModeEnabled
|
||||
? getDraftWithSyncedDynamicRelationWidgets(currentDraft, layout)
|
||||
: {
|
||||
id: layout.id,
|
||||
name: layout.name,
|
||||
type: layout.type,
|
||||
objectMetadataId: layout.objectMetadataId,
|
||||
tabs: layout.tabs,
|
||||
defaultTabToFocusOnMobileAndSidePanelId:
|
||||
layout.defaultTabToFocusOnMobileAndSidePanelId,
|
||||
};
|
||||
|
||||
if (!isDeeplyEqual(nextDraft, currentDraft)) {
|
||||
store.set(pageLayoutDraftComponentCallbackState, nextDraft);
|
||||
}
|
||||
|
||||
const tabLayouts = convertPageLayoutToTabLayouts({
|
||||
...layout,
|
||||
tabs: nextDraft.tabs,
|
||||
});
|
||||
store.set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts);
|
||||
}
|
||||
},
|
||||
[
|
||||
getDraftWithSyncedDynamicRelationWidgets,
|
||||
isLayoutCustomizationModeEnabled,
|
||||
pageLayoutCurrentLayoutsComponentCallbackState,
|
||||
pageLayoutDraftComponentCallbackState,
|
||||
pageLayoutPersistedComponentCallbackState,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageLayoutIsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(basePageLayout)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isRecordPage = layoutType === PageLayoutType.RECORD_PAGE;
|
||||
if (!isRecordPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layoutWithRelationWidgets = injectRelationWidgetsIntoLayout(
|
||||
basePageLayout,
|
||||
boxedRelationFieldMetadataItems,
|
||||
);
|
||||
|
||||
syncPageLayoutWithRelationWidgets(layoutWithRelationWidgets);
|
||||
}, [
|
||||
basePageLayout,
|
||||
boxedRelationFieldMetadataItems,
|
||||
pageLayoutIsInitialized,
|
||||
layoutType,
|
||||
syncPageLayoutWithRelationWidgets,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,16 +1,13 @@
|
||||
import { PageLayoutEditModeProvider } from '@/page-layout/components/PageLayoutEditModeProvider';
|
||||
import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/PageLayoutInitializationQueryEffect';
|
||||
import { PageLayoutRecordPageCustomizationSessionRegistrationEffect } from '@/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect';
|
||||
import { PageLayoutRelationWidgetsSyncEffect } from '@/page-layout/components/PageLayoutRelationWidgetsSyncEffect';
|
||||
import { PageLayoutRendererContent } from '@/page-layout/components/PageLayoutRendererContent';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
type PageLayoutRendererProps = {
|
||||
pageLayoutId: string;
|
||||
@@ -21,10 +18,6 @@ export const PageLayoutRenderer = ({
|
||||
}: PageLayoutRendererProps) => {
|
||||
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
|
||||
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
|
||||
|
||||
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
|
||||
pageLayoutId,
|
||||
layoutType,
|
||||
@@ -48,12 +41,6 @@ export const PageLayoutRenderer = ({
|
||||
>
|
||||
<PageLayoutInitializationQueryEffect pageLayoutId={pageLayoutId} />
|
||||
<PageLayoutRecordPageCustomizationSessionRegistrationEffect />
|
||||
{!isRecordPageLayoutEditingEnabled &&
|
||||
layoutType === PageLayoutType.RECORD_PAGE && (
|
||||
<PageLayoutRelationWidgetsSyncEffect
|
||||
pageLayoutId={pageLayoutId}
|
||||
/>
|
||||
)}
|
||||
<PageLayoutRendererContent />
|
||||
</PageLayoutEditModeProvider>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
|
||||
-10
@@ -3,7 +3,6 @@ import { PageLayoutContent } from '@/page-layout/components/PageLayoutContent';
|
||||
import { PageLayoutEditModeProvider } from '@/page-layout/components/PageLayoutEditModeProvider';
|
||||
import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/PageLayoutInitializationQueryEffect';
|
||||
import { PageLayoutRecordPageCustomizationSessionRegistrationEffect } from '@/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect';
|
||||
import { PageLayoutRelationWidgetsSyncEffect } from '@/page-layout/components/PageLayoutRelationWidgetsSyncEffect';
|
||||
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
|
||||
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
|
||||
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
|
||||
@@ -16,8 +15,6 @@ import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingC
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
type PageLayoutSingleTabRendererProps = {
|
||||
pageLayoutId: string;
|
||||
@@ -79,10 +76,6 @@ export const PageLayoutSingleTabRenderer = ({
|
||||
}: PageLayoutSingleTabRendererProps) => {
|
||||
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
|
||||
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
|
||||
|
||||
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
|
||||
pageLayoutId,
|
||||
layoutType,
|
||||
@@ -106,9 +99,6 @@ export const PageLayoutSingleTabRenderer = ({
|
||||
>
|
||||
<PageLayoutInitializationQueryEffect pageLayoutId={pageLayoutId} />
|
||||
<PageLayoutRecordPageCustomizationSessionRegistrationEffect />
|
||||
{!isRecordPageLayoutEditingEnabled && (
|
||||
<PageLayoutRelationWidgetsSyncEffect pageLayoutId={pageLayoutId} />
|
||||
)}
|
||||
<PageLayoutSingleTabRendererContent />
|
||||
</PageLayoutEditModeProvider>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
|
||||
@@ -46,14 +46,10 @@ import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab-list/components/Ta
|
||||
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
type PageLayoutType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
@@ -112,10 +108,6 @@ export const PageLayoutTabList = ({
|
||||
const { getIcon } = useIcons();
|
||||
const { t } = useLingui();
|
||||
|
||||
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
);
|
||||
|
||||
const tabsWithIcons: SingleTabProps[] = tabs.map((tab) => ({
|
||||
id: tab.id,
|
||||
title: tab.title,
|
||||
@@ -266,10 +258,7 @@ export const PageLayoutTabList = ({
|
||||
(tabId: string) => {
|
||||
const shouldOpenSettings =
|
||||
isPageLayoutInEditMode &&
|
||||
shouldEnableTabEditingFeatures(
|
||||
pageLayoutType,
|
||||
isRecordPageGlobalEditionEnabled,
|
||||
);
|
||||
shouldEnableTabEditingFeatures(pageLayoutType);
|
||||
|
||||
if (shouldOpenSettings && activeTabId === tabId) {
|
||||
openTabSettings(tabId);
|
||||
@@ -284,7 +273,6 @@ export const PageLayoutTabList = ({
|
||||
},
|
||||
[
|
||||
isPageLayoutInEditMode,
|
||||
isRecordPageGlobalEditionEnabled,
|
||||
pageLayoutType,
|
||||
activeTabId,
|
||||
isTabSettingsOpen,
|
||||
@@ -297,10 +285,7 @@ export const PageLayoutTabList = ({
|
||||
(tabId: string) => {
|
||||
const shouldOpenSettings =
|
||||
isPageLayoutInEditMode &&
|
||||
shouldEnableTabEditingFeatures(
|
||||
pageLayoutType,
|
||||
isRecordPageGlobalEditionEnabled,
|
||||
);
|
||||
shouldEnableTabEditingFeatures(pageLayoutType);
|
||||
|
||||
if (shouldOpenSettings && activeTabId === tabId) {
|
||||
openTabSettings(tabId);
|
||||
@@ -316,7 +301,6 @@ export const PageLayoutTabList = ({
|
||||
},
|
||||
[
|
||||
isPageLayoutInEditMode,
|
||||
isRecordPageGlobalEditionEnabled,
|
||||
pageLayoutType,
|
||||
activeTabId,
|
||||
isTabSettingsOpen,
|
||||
|
||||
+2
-14
@@ -26,14 +26,10 @@ import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useContext } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
type PageLayoutType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledOverflowDropdownListDraggableWrapper = styled.div`
|
||||
cursor: grab;
|
||||
@@ -79,16 +75,8 @@ export const PageLayoutTabListReorderableOverflowDropdown = ({
|
||||
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
|
||||
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
);
|
||||
|
||||
const shouldShowEditButton =
|
||||
isPageLayoutInEditMode &&
|
||||
shouldEnableTabEditingFeatures(
|
||||
pageLayoutType,
|
||||
isRecordPageGlobalEditionEnabled,
|
||||
);
|
||||
isPageLayoutInEditMode && shouldEnableTabEditingFeatures(pageLayoutType);
|
||||
|
||||
const isPageLayoutTabDragging = useAtomComponentStateValue(
|
||||
isPageLayoutTabDraggingComponentState,
|
||||
|
||||
+1
-10
@@ -21,11 +21,9 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
const StyledContainer = styled.div<{ hasPinnedTab: boolean }>`
|
||||
display: grid;
|
||||
grid-template-columns: ${({ hasPinnedTab }) =>
|
||||
@@ -73,10 +71,6 @@ export const PageLayoutTabsRenderer = () => {
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
);
|
||||
|
||||
const metadataStore = useAtomFamilyStateValue(
|
||||
metadataStoreState,
|
||||
'objectMetadataItems',
|
||||
@@ -90,10 +84,7 @@ export const PageLayoutTabsRenderer = () => {
|
||||
|
||||
const canEnableTabEditing =
|
||||
isPageLayoutInEditMode &&
|
||||
shouldEnableTabEditingFeatures(
|
||||
currentPageLayout.type,
|
||||
isRecordPageGlobalEditionEnabled,
|
||||
);
|
||||
shouldEnableTabEditingFeatures(currentPageLayout.type);
|
||||
|
||||
const tabsWithVisibleWidgets = getTabsWithVisibleWidgets({
|
||||
tabs: currentPageLayout.tabs,
|
||||
|
||||
-333
@@ -1,333 +0,0 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
|
||||
import { usePageLayoutWithRelationWidgets } from '@/page-layout/hooks/usePageLayoutWithRelationWidgets';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import {
|
||||
AggregateOperations,
|
||||
BarChartLayout,
|
||||
GraphOrderBy,
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
jest.mock('@/ui/layout/contexts/LayoutRenderingContext');
|
||||
jest.mock(
|
||||
'@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems',
|
||||
);
|
||||
|
||||
describe('usePageLayoutWithRelationWidgets', () => {
|
||||
const mockBasePageLayout: PageLayout = {
|
||||
__typename: 'PageLayout',
|
||||
id: 'test-layout',
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: 'obj-1',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'tab-1',
|
||||
title: 'Fields',
|
||||
isActive: true,
|
||||
icon: 'IconList',
|
||||
position: 100,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
pageLayoutId: 'test-layout',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
widgets: [
|
||||
{
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: 'widget-fields',
|
||||
applicationId: '',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Fields',
|
||||
isActive: true,
|
||||
type: WidgetType.FIELDS,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: 'widget-notes',
|
||||
applicationId: '',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Notes',
|
||||
isActive: true,
|
||||
type: WidgetType.NOTES,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 1,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'NotesConfiguration',
|
||||
configurationType: WidgetConfigurationType.NOTES,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: 'widget-other',
|
||||
applicationId: '',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Other',
|
||||
isActive: true,
|
||||
type: WidgetType.GRAPH,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 2,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'BarChartConfiguration',
|
||||
configurationType: WidgetConfigurationType.BAR_CHART,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
primaryAxisGroupByFieldMetadataId: 'createdAt',
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockRelationFields: FieldMetadataItem[] = [
|
||||
{
|
||||
id: 'field-1',
|
||||
universalIdentifier: 'field-1',
|
||||
label: 'Related Companies',
|
||||
name: 'relatedCompanies',
|
||||
type: 'RELATION',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isCustom: false,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
relationDefinition: null,
|
||||
settings: null,
|
||||
} as FieldMetadataItem,
|
||||
{
|
||||
id: 'field-2',
|
||||
universalIdentifier: 'field-2',
|
||||
label: 'Related People',
|
||||
name: 'relatedPeople',
|
||||
type: 'RELATION',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isCustom: false,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
relationDefinition: null,
|
||||
settings: null,
|
||||
} as FieldMetadataItem,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
(useLayoutRenderingContext as jest.Mock).mockReturnValue({
|
||||
targetRecordIdentifier: {
|
||||
targetObjectNameSingular: 'company',
|
||||
},
|
||||
layoutType: PageLayoutType.RECORD_PAGE,
|
||||
});
|
||||
|
||||
(useFieldListFieldMetadataItems as jest.Mock).mockReturnValue({
|
||||
boxedRelationFieldMetadataItems: mockRelationFields,
|
||||
});
|
||||
});
|
||||
|
||||
it('should inject relation widgets after the first FIELDS widget', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePageLayoutWithRelationWidgets(mockBasePageLayout),
|
||||
);
|
||||
|
||||
const firstTab = result.current?.tabs[0];
|
||||
expect(firstTab).toBeDefined();
|
||||
|
||||
const widgets = firstTab?.widgets || [];
|
||||
expect(widgets.length).toBe(5); // 1 FIELDS + 2 relation + 1 NOTES + 1 OTHER
|
||||
|
||||
// First widget should be FIELDS
|
||||
expect(widgets[0].type).toBe(WidgetType.FIELDS);
|
||||
expect(widgets[0].id).toBe('widget-fields');
|
||||
|
||||
// Next two should be relation widgets
|
||||
expect(widgets[1].type).toBe(WidgetType.FIELD);
|
||||
expect(widgets[1].title).toBe('Related Companies');
|
||||
expect(widgets[2].type).toBe(WidgetType.FIELD);
|
||||
expect(widgets[2].title).toBe('Related People');
|
||||
|
||||
// Then NOTES widget
|
||||
expect(widgets[3].type).toBe(WidgetType.NOTES);
|
||||
expect(widgets[3].id).toBe('widget-notes');
|
||||
|
||||
// Finally OTHER widget
|
||||
expect(widgets[4].type).toBe(WidgetType.GRAPH);
|
||||
expect(widgets[4].id).toBe('widget-other');
|
||||
});
|
||||
|
||||
it('should handle layout with no FIELDS widget by appending to end', () => {
|
||||
const layoutWithoutFields: PageLayout = {
|
||||
...mockBasePageLayout,
|
||||
tabs: [
|
||||
{
|
||||
...mockBasePageLayout.tabs[0],
|
||||
widgets: [
|
||||
{
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: 'widget-other',
|
||||
applicationId: '',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Other',
|
||||
isActive: true,
|
||||
type: WidgetType.GRAPH,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'BarChartConfiguration',
|
||||
configurationType: WidgetConfigurationType.BAR_CHART,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
aggregateFieldMetadataId: 'id',
|
||||
primaryAxisGroupByFieldMetadataId: 'createdAt',
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
displayDataLabel: false,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePageLayoutWithRelationWidgets(layoutWithoutFields),
|
||||
);
|
||||
|
||||
const firstTab = result.current?.tabs[0];
|
||||
const widgets = firstTab?.widgets || [];
|
||||
|
||||
expect(widgets.length).toBe(3); // 1 OTHER + 2 relation
|
||||
expect(widgets[0].type).toBe(WidgetType.GRAPH);
|
||||
expect(widgets[1].type).toBe(WidgetType.FIELD);
|
||||
expect(widgets[2].type).toBe(WidgetType.FIELD);
|
||||
});
|
||||
|
||||
it('should return unchanged layout when no relation fields exist', () => {
|
||||
(useFieldListFieldMetadataItems as jest.Mock).mockReturnValue({
|
||||
boxedRelationFieldMetadataItems: [],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePageLayoutWithRelationWidgets(mockBasePageLayout),
|
||||
);
|
||||
|
||||
expect(result.current).toEqual(mockBasePageLayout);
|
||||
});
|
||||
|
||||
it('should return undefined when basePageLayout is undefined', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePageLayoutWithRelationWidgets(undefined),
|
||||
);
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return unchanged layout when not a record page', () => {
|
||||
(useLayoutRenderingContext as jest.Mock).mockReturnValue({
|
||||
targetRecordIdentifier: {
|
||||
targetObjectNameSingular: 'company',
|
||||
},
|
||||
layoutType: PageLayoutType.DASHBOARD,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePageLayoutWithRelationWidgets(mockBasePageLayout),
|
||||
);
|
||||
|
||||
expect(result.current).toEqual(mockBasePageLayout);
|
||||
});
|
||||
|
||||
it('should handle layout without Note widget', () => {
|
||||
const layoutWithoutNotes: PageLayout = {
|
||||
...mockBasePageLayout,
|
||||
tabs: [
|
||||
{
|
||||
...mockBasePageLayout.tabs[0],
|
||||
widgets: [
|
||||
mockBasePageLayout.tabs[0].widgets[0], // FIELDS widget
|
||||
mockBasePageLayout.tabs[0].widgets[2], // OTHER widget
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePageLayoutWithRelationWidgets(layoutWithoutNotes),
|
||||
);
|
||||
|
||||
const firstTab = result.current?.tabs[0];
|
||||
const widgets = firstTab?.widgets || [];
|
||||
|
||||
expect(widgets.length).toBe(4); // 1 FIELDS + 2 relation + 1 OTHER
|
||||
|
||||
expect(widgets[0].type).toBe(WidgetType.FIELDS);
|
||||
expect(widgets[1].type).toBe(WidgetType.FIELD);
|
||||
expect(widgets[2].type).toBe(WidgetType.FIELD);
|
||||
expect(widgets[3].type).toBe(WidgetType.GRAPH);
|
||||
});
|
||||
});
|
||||
@@ -8,12 +8,11 @@ import { shouldEnableTabEditingFeatures } from '@/page-layout/utils/shouldEnable
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const usePageLayoutAddTabStrategy = ({
|
||||
pageLayoutId,
|
||||
@@ -25,10 +24,6 @@ export const usePageLayoutAddTabStrategy = ({
|
||||
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
|
||||
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
);
|
||||
|
||||
const { createPageLayoutTab } = useCreatePageLayoutTab({
|
||||
pageLayoutId,
|
||||
tabListInstanceId,
|
||||
@@ -66,10 +61,7 @@ export const usePageLayoutAddTabStrategy = ({
|
||||
|
||||
const isEnabled =
|
||||
isPageLayoutInEditMode &&
|
||||
shouldEnableTabEditingFeatures(
|
||||
currentPageLayout.type,
|
||||
isRecordPageGlobalEditionEnabled,
|
||||
);
|
||||
shouldEnableTabEditingFeatures(currentPageLayout.type);
|
||||
|
||||
if (!isEnabled) {
|
||||
return undefined;
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { injectRelationWidgetsIntoLayout } from '@/page-layout/utils/injectRelationWidgetsIntoLayout';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const usePageLayoutWithRelationWidgets = (
|
||||
basePageLayout: PageLayout | undefined,
|
||||
): PageLayout | undefined => {
|
||||
const { targetRecordIdentifier, layoutType } = useLayoutRenderingContext();
|
||||
|
||||
const { boxedRelationFieldMetadataItems } = useFieldListFieldMetadataItems({
|
||||
objectNameSingular: targetRecordIdentifier?.targetObjectNameSingular ?? '',
|
||||
});
|
||||
|
||||
if (!isDefined(basePageLayout)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isRecordPage = layoutType === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
if (!isRecordPage) {
|
||||
return basePageLayout;
|
||||
}
|
||||
|
||||
return injectRelationWidgetsIntoLayout(
|
||||
basePageLayout,
|
||||
boxedRelationFieldMetadataItems,
|
||||
);
|
||||
};
|
||||
@@ -8,15 +8,12 @@ import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayo
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
|
||||
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
|
||||
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
@@ -49,9 +46,6 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
const { createPendingRecordTableWidgetViews } =
|
||||
useCreatePendingRecordTableWidgetViews();
|
||||
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
|
||||
const store = useStore();
|
||||
|
||||
const savePageLayout = useCallback(async () => {
|
||||
@@ -59,9 +53,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
await createPendingRecordTableWidgetViews(pageLayoutId);
|
||||
|
||||
const pageLayoutDraft = store.get(pageLayoutDraftCallbackState);
|
||||
const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft, {
|
||||
shouldFilterDynamicRelationWidgets: !isRecordPageLayoutEditingEnabled,
|
||||
});
|
||||
const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft);
|
||||
|
||||
const result = await updatePageLayoutWithTabsAndWidgets(
|
||||
pageLayoutId,
|
||||
@@ -76,19 +68,10 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
const persistedLayout: PageLayout =
|
||||
transformPageLayout(updatedPageLayout);
|
||||
|
||||
const pageLayoutToPersist =
|
||||
!isRecordPageLayoutEditingEnabled &&
|
||||
persistedLayout.type === PageLayoutType.RECORD_PAGE
|
||||
? reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
pageLayoutDraft,
|
||||
)
|
||||
: persistedLayout;
|
||||
|
||||
store.set(pageLayoutPersistedCallbackState, pageLayoutToPersist);
|
||||
store.set(pageLayoutPersistedCallbackState, persistedLayout);
|
||||
store.set(
|
||||
pageLayoutCurrentLayoutsCallbackState,
|
||||
convertPageLayoutToTabLayouts(pageLayoutToPersist),
|
||||
convertPageLayoutToTabLayouts(persistedLayout),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -97,7 +80,6 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
}, [
|
||||
createPendingFieldsWidgetViews,
|
||||
createPendingRecordTableWidgetViews,
|
||||
isRecordPageLayoutEditingEnabled,
|
||||
pageLayoutCurrentLayoutsCallbackState,
|
||||
pageLayoutDraftCallbackState,
|
||||
pageLayoutId,
|
||||
|
||||
-78
@@ -1,7 +1,6 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
|
||||
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
@@ -350,81 +349,4 @@ describe('convertPageLayoutDraftToUpdateInput', () => {
|
||||
columnSpan: 6,
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldFilterDynamicRelationWidgets', () => {
|
||||
it('should filter out dynamic relation widgets when shouldFilterDynamicRelationWidgets is true', () => {
|
||||
const regularWidget = makeWidget({ id: 'w1' });
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
type: WidgetType.VIEW,
|
||||
});
|
||||
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [regularWidget, dynamicWidget]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft, {
|
||||
shouldFilterDynamicRelationWidgets: true,
|
||||
});
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(1);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('w1');
|
||||
});
|
||||
|
||||
it('should keep all non-dynamic widgets when shouldFilterDynamicRelationWidgets is true and multiple widget types exist', () => {
|
||||
const fieldsWidget = makeWidget({ id: 'w1', type: WidgetType.FIELDS });
|
||||
const timelineWidget = makeWidget({
|
||||
id: 'w2',
|
||||
type: WidgetType.TIMELINE,
|
||||
});
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel`,
|
||||
});
|
||||
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft, {
|
||||
shouldFilterDynamicRelationWidgets: true,
|
||||
});
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
|
||||
});
|
||||
|
||||
it('should not filter dynamic relation widgets when shouldFilterDynamicRelationWidgets is false', () => {
|
||||
const regularWidget = makeWidget({ id: 'w1' });
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
type: WidgetType.VIEW,
|
||||
});
|
||||
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [regularWidget, dynamicWidget]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft, {
|
||||
shouldFilterDynamicRelationWidgets: false,
|
||||
});
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not filter dynamic relation widgets by default', () => {
|
||||
const regularWidget = makeWidget({ id: 'w1' });
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
type: WidgetType.VIEW,
|
||||
});
|
||||
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [regularWidget, dynamicWidget]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { injectRelationWidgetsIntoLayout } from '@/page-layout/utils/injectRelationWidgetsIntoLayout';
|
||||
import {
|
||||
FieldDisplayMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const createMockWidget = (
|
||||
id: string,
|
||||
type: WidgetType = WidgetType.FIELDS,
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
__typename: 'PageLayoutWidget',
|
||||
id,
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: `Widget ${id}`,
|
||||
type,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetGridPosition',
|
||||
layoutMode: 'GRID',
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
const createMockTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
): PageLayoutTab =>
|
||||
({
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id,
|
||||
pageLayoutId: 'page-layout-1',
|
||||
title: `Tab ${id}`,
|
||||
position: 0,
|
||||
widgets,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
}) as PageLayoutTab;
|
||||
|
||||
const createMockLayout = (tabs: PageLayoutTab[]): PageLayout =>
|
||||
({
|
||||
__typename: 'PageLayout',
|
||||
id: 'layout-1',
|
||||
tabs,
|
||||
}) as PageLayout;
|
||||
|
||||
const createMockFieldMetadataItem = (
|
||||
id: string,
|
||||
label: string,
|
||||
): FieldMetadataItem =>
|
||||
({
|
||||
id,
|
||||
label,
|
||||
name: label.toLowerCase(),
|
||||
type: 'RELATION',
|
||||
}) as FieldMetadataItem;
|
||||
|
||||
describe('injectRelationWidgetsIntoLayout', () => {
|
||||
it('should return layout unchanged when relation fields array is empty', () => {
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [createMockWidget('w1')]),
|
||||
]);
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, []);
|
||||
|
||||
expect(result).toBe(layout);
|
||||
});
|
||||
|
||||
it('should return layout unchanged when there are no tabs', () => {
|
||||
const layout = createMockLayout([]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result).toBe(layout);
|
||||
});
|
||||
|
||||
it('should append relation widgets when no FIELDS widget exists', () => {
|
||||
const otherWidget = createMockWidget('w1', WidgetType.TIMELINE);
|
||||
const layout = createMockLayout([createMockTab('tab-1', [otherWidget])]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('w1');
|
||||
expect(result.tabs[0].widgets[1].id).toContain('dynamic-relation-widget-');
|
||||
expect(result.tabs[0].widgets[1].id).toContain('f1');
|
||||
});
|
||||
|
||||
it('should inject relation widgets after the first FIELDS widget', () => {
|
||||
const fieldsWidget = createMockWidget('fields-1', WidgetType.FIELDS);
|
||||
const timelineWidget = createMockWidget('timeline-1', WidgetType.TIMELINE);
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [fieldsWidget, timelineWidget]),
|
||||
]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toContain('f1');
|
||||
expect(result.tabs[0].widgets[2].id).toBe('timeline-1');
|
||||
});
|
||||
|
||||
it('should reposition NOTES widget after relation widgets', () => {
|
||||
const fieldsWidget = createMockWidget('fields-1', WidgetType.FIELDS);
|
||||
const notesWidget = createMockWidget('notes-1', WidgetType.NOTES);
|
||||
const timelineWidget = createMockWidget('timeline-1', WidgetType.TIMELINE);
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [fieldsWidget, notesWidget, timelineWidget]),
|
||||
]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
const widgetIds = result.tabs[0].widgets.map((w) => w.id);
|
||||
|
||||
expect(widgetIds[0]).toBe('fields-1');
|
||||
expect(widgetIds[1]).toContain('f1');
|
||||
expect(widgetIds[2]).toBe('notes-1');
|
||||
expect(widgetIds[3]).toBe('timeline-1');
|
||||
});
|
||||
|
||||
it('should only modify the first tab', () => {
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [createMockWidget('fields-1', WidgetType.FIELDS)]),
|
||||
createMockTab('tab-2', [createMockWidget('fields-2', WidgetType.FIELDS)]),
|
||||
]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[1].widgets).toHaveLength(1);
|
||||
expect(result.tabs[1].widgets[0].id).toBe('fields-2');
|
||||
});
|
||||
|
||||
it('should inject multiple relation widgets in order', () => {
|
||||
const fieldsWidget = createMockWidget('fields-1', WidgetType.FIELDS);
|
||||
const layout = createMockLayout([createMockTab('tab-1', [fieldsWidget])]);
|
||||
const fields = [
|
||||
createMockFieldMetadataItem('f1', 'Company'),
|
||||
createMockFieldMetadataItem('f2', 'Person'),
|
||||
];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toContain('f1');
|
||||
expect(result.tabs[0].widgets[2].id).toContain('f2');
|
||||
});
|
||||
|
||||
it('should set correct properties on injected relation widgets', () => {
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [createMockWidget('fields-1', WidgetType.FIELDS)]),
|
||||
]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
const injectedWidget = result.tabs[0].widgets[1];
|
||||
|
||||
expect(injectedWidget.type).toBe(WidgetType.FIELD);
|
||||
expect(injectedWidget.title).toBe('Company');
|
||||
expect(injectedWidget.pageLayoutTabId).toBe('tab-1');
|
||||
expect(injectedWidget.configuration).toEqual(
|
||||
expect.objectContaining({
|
||||
fieldMetadataId: 'f1',
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import {
|
||||
DYNAMIC_RELATION_WIDGET_ID_PREFIX,
|
||||
isDynamicRelationWidget,
|
||||
} from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
|
||||
const makeWidget = (id: string): PageLayoutWidget =>
|
||||
({
|
||||
id,
|
||||
title: 'Widget',
|
||||
type: 'FIELDS',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
|
||||
configuration: {},
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
describe('isDynamicRelationWidget', () => {
|
||||
it('should return true for a widget with the dynamic relation prefix', () => {
|
||||
const widget = makeWidget(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}some-relation`,
|
||||
);
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a widget without the dynamic relation prefix', () => {
|
||||
const widget = makeWidget('regular-widget-id');
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a widget with a similar but different prefix', () => {
|
||||
const widget = makeWidget('dynamic-relation-other-widget-123');
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for the prefix alone as id', () => {
|
||||
const widget = makeWidget(DYNAMIC_RELATION_WIDGET_ID_PREFIX);
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(true);
|
||||
});
|
||||
});
|
||||
-194
@@ -1,194 +0,0 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
const makeWidget = (
|
||||
overrides: Partial<PageLayoutWidget> & { id: string },
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
title: 'Widget',
|
||||
type: WidgetType.FIELDS,
|
||||
pageLayoutTabId: 'tab-1',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
|
||||
configuration: {},
|
||||
...overrides,
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
const makeDynamicRelationWidget = (suffix: string): PageLayoutWidget =>
|
||||
makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}${suffix}`,
|
||||
type: WidgetType.VIEW,
|
||||
title: `Dynamic Relation ${suffix}`,
|
||||
});
|
||||
|
||||
const makePersistedLayout = (tabs: PageLayout['tabs']): PageLayout =>
|
||||
({
|
||||
id: 'layout-1',
|
||||
name: 'Layout',
|
||||
type: 'RECORD_PAGE',
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
tabs,
|
||||
}) as PageLayout;
|
||||
|
||||
const makeDraftLayout = (tabs: DraftPageLayout['tabs']): DraftPageLayout =>
|
||||
({
|
||||
id: 'layout-1',
|
||||
name: 'Layout',
|
||||
type: 'RECORD_PAGE',
|
||||
tabs,
|
||||
}) as DraftPageLayout;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
): PageLayout['tabs'][number] =>
|
||||
({
|
||||
id,
|
||||
title: 'Tab',
|
||||
position: 0,
|
||||
pageLayoutId: 'layout-1',
|
||||
applicationId: 'app-1',
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
widgets,
|
||||
}) as PageLayout['tabs'][number];
|
||||
|
||||
describe('reInjectDynamicRelationWidgetsFromDraft', () => {
|
||||
it('should return persisted layout unchanged when draft has no dynamic widgets', () => {
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [makeWidget({ id: 'w1' })]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [makeWidget({ id: 'w1' })]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
expect(result).toBe(persistedLayout);
|
||||
});
|
||||
|
||||
it('should inject dynamic widgets after the first FIELDS widget', () => {
|
||||
const fieldsWidget = makeWidget({
|
||||
id: 'fields-1',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const timelineWidget = makeWidget({
|
||||
id: 'timeline-1',
|
||||
type: WidgetType.TIMELINE,
|
||||
});
|
||||
const dynamicWidget = makeDynamicRelationWidget('relation-1');
|
||||
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [fieldsWidget, timelineWidget]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
);
|
||||
expect(result.tabs[0].widgets[2].id).toBe('timeline-1');
|
||||
});
|
||||
|
||||
it('should append dynamic widgets if no FIELDS widget exists in the tab', () => {
|
||||
const timelineWidget = makeWidget({
|
||||
id: 'timeline-1',
|
||||
type: WidgetType.TIMELINE,
|
||||
});
|
||||
const dynamicWidget = makeDynamicRelationWidget('relation-1');
|
||||
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [timelineWidget]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [timelineWidget, dynamicWidget]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('timeline-1');
|
||||
expect(result.tabs[0].widgets[1].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple tabs independently', () => {
|
||||
const fieldsWidget1 = makeWidget({
|
||||
id: 'fields-1',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const fieldsWidget2 = makeWidget({
|
||||
id: 'fields-2',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const dynamicWidget = makeDynamicRelationWidget('rel-1');
|
||||
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [fieldsWidget1]),
|
||||
makeTab('tab-2', [fieldsWidget2]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [fieldsWidget1, dynamicWidget]),
|
||||
makeTab('tab-2', [fieldsWidget2]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
// Tab 1 should have dynamic widget injected
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
// Tab 2 should remain unchanged
|
||||
expect(result.tabs[1].widgets).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should inject multiple dynamic widgets', () => {
|
||||
const fieldsWidget = makeWidget({
|
||||
id: 'fields-1',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const dynamicWidget1 = makeDynamicRelationWidget('rel-1');
|
||||
const dynamicWidget2 = makeDynamicRelationWidget('rel-2');
|
||||
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [fieldsWidget]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [fieldsWidget, dynamicWidget1, dynamicWidget2]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel-1`,
|
||||
);
|
||||
expect(result.tabs[0].widgets[2].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel-2`,
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
-49
@@ -14,61 +14,13 @@ describe('shouldEnableTabEditingFeatures', () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for RECORD_PAGE layout type without flag', () => {
|
||||
it('should return true for RECORD_PAGE layout type', () => {
|
||||
const result = shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for RECORD_PAGE layout type with flag enabled', () => {
|
||||
const result = shouldEnableTabEditingFeatures(
|
||||
PageLayoutType.RECORD_PAGE,
|
||||
true,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for RECORD_PAGE layout type with flag disabled', () => {
|
||||
const result = shouldEnableTabEditingFeatures(
|
||||
PageLayoutType.RECORD_PAGE,
|
||||
false,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for RECORD_INDEX layout type', () => {
|
||||
const result = shouldEnableTabEditingFeatures(PageLayoutType.RECORD_INDEX);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for RECORD_INDEX layout type even with flag', () => {
|
||||
const result = shouldEnableTabEditingFeatures(
|
||||
PageLayoutType.RECORD_INDEX,
|
||||
true,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
describe('behavior validation', () => {
|
||||
it('should enable tab editing features only for dashboards, standalone pages, and record pages with flag', () => {
|
||||
expect(shouldEnableTabEditingFeatures(PageLayoutType.DASHBOARD)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
expect(
|
||||
shouldEnableTabEditingFeatures(PageLayoutType.STANDALONE_PAGE),
|
||||
).toBe(true);
|
||||
|
||||
expect(shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE)).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
expect(
|
||||
shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE, true),
|
||||
).toBe(true);
|
||||
|
||||
expect(shouldEnableTabEditingFeatures(PageLayoutType.RECORD_INDEX)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-9
@@ -1,6 +1,5 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
type UpdatePageLayoutWithTabsInput,
|
||||
@@ -40,10 +39,7 @@ const buildWidgetPosition = (
|
||||
|
||||
export const convertPageLayoutDraftToUpdateInput = (
|
||||
pageLayoutDraft: DraftPageLayout,
|
||||
options?: { shouldFilterDynamicRelationWidgets?: boolean },
|
||||
): UpdatePageLayoutWithTabsInput => {
|
||||
const shouldFilter = options?.shouldFilterDynamicRelationWidgets ?? false;
|
||||
|
||||
return {
|
||||
name: pageLayoutDraft.name,
|
||||
type: pageLayoutDraft.type,
|
||||
@@ -51,17 +47,13 @@ export const convertPageLayoutDraftToUpdateInput = (
|
||||
tabs: pageLayoutDraft.tabs
|
||||
.filter((tab) => tab.isActive)
|
||||
.map((tab) => {
|
||||
const widgets = shouldFilter
|
||||
? tab.widgets.filter((widget) => !isDynamicRelationWidget(widget))
|
||||
: tab.widgets;
|
||||
|
||||
return {
|
||||
id: tab.id,
|
||||
title: tab.title,
|
||||
position: tab.position,
|
||||
icon: tab.icon ?? null,
|
||||
layoutMode: tab.layoutMode,
|
||||
widgets: widgets.map((widget, widgetIndex) => ({
|
||||
widgets: tab.widgets.map((widget, widgetIndex) => ({
|
||||
id: widget.id,
|
||||
pageLayoutTabId: widget.pageLayoutTabId,
|
||||
title: widget.title,
|
||||
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FieldDisplayMode,
|
||||
PageLayoutTabLayoutMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const getRelationFieldWidgetToInsert = (
|
||||
field: FieldMetadataItem,
|
||||
tabId: string,
|
||||
): PageLayoutWidget => ({
|
||||
__typename: 'PageLayoutWidget' as const,
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}${field.id}-${field.label}`,
|
||||
applicationId: '',
|
||||
pageLayoutTabId: tabId,
|
||||
title: field.label,
|
||||
isActive: true,
|
||||
type: WidgetType.FIELD,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition' as const,
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetGridPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldConfiguration' as const,
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: field.id,
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const getRelationFieldWidgetsToInsert = (
|
||||
relationFields: FieldMetadataItem[],
|
||||
tabId: string,
|
||||
): PageLayoutWidget[] => {
|
||||
return relationFields.map((field) =>
|
||||
getRelationFieldWidgetToInsert(field, tabId),
|
||||
);
|
||||
};
|
||||
|
||||
export const injectRelationWidgetsIntoLayout = (
|
||||
layout: PageLayout,
|
||||
boxedRelationFieldMetadataItems: FieldMetadataItem[],
|
||||
): PageLayout => {
|
||||
if (boxedRelationFieldMetadataItems.length === 0) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const firstTab = layout.tabs[0];
|
||||
if (!isDefined(firstTab)) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const relationWidgets = getRelationFieldWidgetsToInsert(
|
||||
boxedRelationFieldMetadataItems,
|
||||
firstTab.id,
|
||||
);
|
||||
|
||||
return {
|
||||
...layout,
|
||||
tabs: layout.tabs.map((tab) => {
|
||||
if (tab.id === firstTab.id) {
|
||||
const firstFieldsWidgetIndex = tab.widgets.findIndex(
|
||||
(widget) => widget.type === WidgetType.FIELDS,
|
||||
);
|
||||
|
||||
if (firstFieldsWidgetIndex === -1) {
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...tab.widgets, ...relationWidgets],
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: This note widget repositioning logic is temporary and will be deleted soon.
|
||||
// We need this to ensure the note editor is displayed before record relations,
|
||||
// matching the behavior of the old show page.
|
||||
const noteWidgetIndex = tab.widgets.findIndex(
|
||||
(widget) => widget.type === WidgetType.NOTES,
|
||||
);
|
||||
|
||||
const widgetsBeforeRelation = tab.widgets.slice(
|
||||
0,
|
||||
firstFieldsWidgetIndex + 1,
|
||||
);
|
||||
const widgetsAfterRelation =
|
||||
noteWidgetIndex === -1
|
||||
? tab.widgets.slice(firstFieldsWidgetIndex + 1)
|
||||
: [
|
||||
...tab.widgets.slice(
|
||||
firstFieldsWidgetIndex + 1,
|
||||
noteWidgetIndex,
|
||||
),
|
||||
...tab.widgets.slice(noteWidgetIndex + 1),
|
||||
];
|
||||
|
||||
const noteWidget =
|
||||
noteWidgetIndex !== -1 ? [tab.widgets[noteWidgetIndex]] : [];
|
||||
|
||||
return {
|
||||
...tab,
|
||||
widgets: [
|
||||
...widgetsBeforeRelation,
|
||||
...relationWidgets,
|
||||
...noteWidget,
|
||||
...widgetsAfterRelation,
|
||||
],
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
|
||||
export const DYNAMIC_RELATION_WIDGET_ID_PREFIX = 'dynamic-relation-widget-';
|
||||
|
||||
export const isDynamicRelationWidget = (widget: PageLayoutWidget): boolean => {
|
||||
return widget.id.startsWith(DYNAMIC_RELATION_WIDGET_ID_PREFIX);
|
||||
};
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const reInjectDynamicRelationWidgetsFromDraft = (
|
||||
persistedLayout: PageLayout,
|
||||
previousDraft: DraftPageLayout,
|
||||
): PageLayout => {
|
||||
const dynamicWidgetsByTabId = new Map(
|
||||
previousDraft.tabs.map((tab) => [
|
||||
tab.id,
|
||||
tab.widgets.filter(isDynamicRelationWidget),
|
||||
]),
|
||||
);
|
||||
|
||||
const hasDynamicWidgets = [...dynamicWidgetsByTabId.values()].some(
|
||||
(widgets) => widgets.length > 0,
|
||||
);
|
||||
|
||||
if (!hasDynamicWidgets) {
|
||||
return persistedLayout;
|
||||
}
|
||||
|
||||
return {
|
||||
...persistedLayout,
|
||||
tabs: persistedLayout.tabs.map((tab) => {
|
||||
const relationWidgets = dynamicWidgetsByTabId.get(tab.id);
|
||||
|
||||
if (!relationWidgets || relationWidgets.length === 0) {
|
||||
return tab;
|
||||
}
|
||||
|
||||
const firstFieldsWidgetIndex = tab.widgets.findIndex(
|
||||
(widget) => widget.type === WidgetType.FIELDS,
|
||||
);
|
||||
|
||||
if (firstFieldsWidgetIndex === -1) {
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...tab.widgets, ...relationWidgets],
|
||||
};
|
||||
}
|
||||
|
||||
const widgetsBefore = tab.widgets.slice(0, firstFieldsWidgetIndex + 1);
|
||||
const widgetsAfter = tab.widgets.slice(firstFieldsWidgetIndex + 1);
|
||||
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...widgetsBefore, ...relationWidgets, ...widgetsAfter],
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
+4
-15
@@ -2,21 +2,10 @@ import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const shouldEnableTabEditingFeatures = (
|
||||
pageLayoutType: PageLayoutType,
|
||||
isRecordPageGlobalEditionEnabled?: boolean,
|
||||
): boolean => {
|
||||
if (
|
||||
return (
|
||||
pageLayoutType === PageLayoutType.DASHBOARD ||
|
||||
pageLayoutType === PageLayoutType.STANDALONE_PAGE
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
pageLayoutType === PageLayoutType.RECORD_PAGE &&
|
||||
isRecordPageGlobalEditionEnabled
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
pageLayoutType === PageLayoutType.STANDALONE_PAGE ||
|
||||
pageLayoutType === PageLayoutType.RECORD_PAGE
|
||||
);
|
||||
};
|
||||
|
||||
+4
-48
@@ -2,18 +2,6 @@ import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { WidgetCardShell } from '@/page-layout/widgets/components/WidgetCardShell';
|
||||
import { useWidgetActions } from '@/page-layout/widgets/hooks/useWidgetActions';
|
||||
import { useWidgetRendererState } from '@/page-layout/widgets/hooks/useWidgetRendererState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
PageLayoutType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledEditingWidgetWrapper = styled.div`
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
type RecordPageWidgetRendererProps = {
|
||||
widget: PageLayoutWidget;
|
||||
@@ -24,39 +12,13 @@ export const RecordPageWidgetRenderer = ({
|
||||
}: RecordPageWidgetRendererProps) => {
|
||||
const state = useWidgetRendererState(widget);
|
||||
|
||||
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
);
|
||||
|
||||
const isRecordPageLayout =
|
||||
state.currentPageLayout.type === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
const isReorderEnabled =
|
||||
!isRecordPageLayout ||
|
||||
(isRecordPageLayout && isRecordPageGlobalEditionEnabled);
|
||||
|
||||
const isDeletingWidgetEnabled =
|
||||
!isRecordPageLayout ||
|
||||
(isRecordPageLayout && isRecordPageGlobalEditionEnabled);
|
||||
|
||||
const isWidgetEditable =
|
||||
state.isPageLayoutInEditMode &&
|
||||
(!isRecordPageLayout ||
|
||||
(isRecordPageLayout && isRecordPageGlobalEditionEnabled) ||
|
||||
widget.type === WidgetType.FIELDS ||
|
||||
widget.type === WidgetType.FIELD);
|
||||
const isWidgetEditable = state.isPageLayoutInEditMode;
|
||||
|
||||
const actions = useWidgetActions({ widget });
|
||||
|
||||
// TODO: remove once all record page layouts widgets use the editable contain in edit mode
|
||||
const shouldWrapWithEditingWrapper =
|
||||
isWidgetEditable &&
|
||||
state.variant === 'side-column' &&
|
||||
!isRecordPageGlobalEditionEnabled;
|
||||
|
||||
const isCanvasVariant = state.variant === 'canvas';
|
||||
|
||||
const shell = (
|
||||
return (
|
||||
<WidgetCardShell
|
||||
widget={widget}
|
||||
variant={state.variant}
|
||||
@@ -71,18 +33,12 @@ export const RecordPageWidgetRenderer = ({
|
||||
actions={actions}
|
||||
isInVerticalListTab={state.isInVerticalListTab}
|
||||
isMobile={state.isMobile}
|
||||
isReorderEnabled={isReorderEnabled}
|
||||
isDeletingWidgetEnabled={isDeletingWidgetEnabled}
|
||||
isReorderEnabled={true}
|
||||
isDeletingWidgetEnabled={true}
|
||||
onClick={isWidgetEditable ? state.handleClick : undefined}
|
||||
onRemove={state.handleRemove}
|
||||
onMouseEnter={isCanvasVariant ? undefined : state.handleMouseEnter}
|
||||
onMouseLeave={isCanvasVariant ? undefined : state.handleMouseLeave}
|
||||
/>
|
||||
);
|
||||
|
||||
if (shouldWrapWithEditingWrapper) {
|
||||
return <StyledEditingWidgetWrapper>{shell}</StyledEditingWidgetWrapper>;
|
||||
}
|
||||
|
||||
return shell;
|
||||
};
|
||||
|
||||
@@ -81,9 +81,6 @@ export const SettingsObjectDetailPage = () => {
|
||||
const isUniqueIndexesEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_UNIQUE_INDEXES_ENABLED,
|
||||
);
|
||||
const isRecordPageLayoutEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
@@ -123,7 +120,6 @@ export const SettingsObjectDetailPage = () => {
|
||||
title: t`Layout`,
|
||||
Icon: IconLayout,
|
||||
hide:
|
||||
!isRecordPageLayoutEditingEnabled ||
|
||||
objectMetadataItem.isRemote ||
|
||||
objectMetadataItem.nameSingular === CoreObjectNameSingular.Dashboard,
|
||||
},
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { FeatureFlagKey, ViewType } from 'twenty-shared/types';
|
||||
import { type FeatureFlagKey, ViewType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
@@ -47,7 +47,7 @@ export class BackfillRecordPageLayoutsCommand extends ActiveOrSuspendedWorkspace
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' as FeatureFlagKey,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
@@ -88,12 +88,12 @@ export class BackfillRecordPageLayoutsCommand extends ActiveOrSuspendedWorkspace
|
||||
});
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED],
|
||||
['IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' as FeatureFlagKey],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED],
|
||||
['IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as FeatureFlagKey],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
|
||||
+2
-13
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type FindOneOptions, type Repository } from 'typeorm';
|
||||
|
||||
@@ -378,7 +377,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
|
||||
featureFlagsMap: existingFeatureFlagsMap,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
@@ -386,7 +384,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
'flatViewFieldMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
'featureFlagsMap',
|
||||
]);
|
||||
|
||||
const allTranspiledTranspilationInputs: Awaited<
|
||||
@@ -423,15 +420,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
{ flatFieldMetadatas: [], indexMetadatas: [] },
|
||||
);
|
||||
|
||||
let flatViewFieldsToCreate: UniversalFlatViewField[] = [];
|
||||
|
||||
if (
|
||||
existingFeatureFlagsMap[
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
|
||||
] ??
|
||||
false
|
||||
) {
|
||||
flatViewFieldsToCreate = computeFlatViewFieldsFromFieldsWidgets({
|
||||
const flatViewFieldsToCreate: UniversalFlatViewField[] =
|
||||
computeFlatViewFieldsFromFieldsWidgets({
|
||||
fieldsToCreate: flatFieldMetadatasToCreate.map((flatFieldMetadata) => ({
|
||||
objectMetadataUniversalIdentifier:
|
||||
flatFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
@@ -445,7 +435,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
applicationUniversalIdentifier:
|
||||
resolvedOwnerFlatApplication.universalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
|
||||
+67
-78
@@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
ViewKey,
|
||||
ViewOpenRecordIn,
|
||||
ViewType,
|
||||
@@ -475,13 +474,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
const resolvedOwnerFlatApplication =
|
||||
ownerFlatApplication ?? workspaceCustomFlatApplication;
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
featureFlagsMap: existingFeatureFlagsMap,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'featureFlagsMap',
|
||||
]);
|
||||
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
const {
|
||||
flatObjectMetadataToCreate,
|
||||
@@ -507,11 +503,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
viewUniversalIdentifier: flatDefaultViewToCreate.universalIdentifier,
|
||||
});
|
||||
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
existingFeatureFlagsMap[
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
|
||||
] ?? false;
|
||||
|
||||
const flatNavigationMenuItemToCreate =
|
||||
await this.computeFlatNavigationMenuItemToCreate({
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
@@ -617,77 +608,75 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
);
|
||||
}
|
||||
|
||||
if (isRecordPageLayoutEditingEnabled) {
|
||||
const flatRecordPageFieldsViewToCreate =
|
||||
this.computeFlatRecordPageFieldsViewToCreate({
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
});
|
||||
const flatRecordPageFieldsViewToCreate =
|
||||
this.computeFlatRecordPageFieldsViewToCreate({
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
const flatRecordPageFieldsViewFieldsToCreate =
|
||||
computeFlatViewFieldsToCreate({
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier:
|
||||
flatRecordPageFieldsViewToCreate.universalIdentifier,
|
||||
excludeLabelIdentifier: true,
|
||||
});
|
||||
const flatRecordPageFieldsViewFieldsToCreate =
|
||||
computeFlatViewFieldsToCreate({
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier:
|
||||
flatRecordPageFieldsViewToCreate.universalIdentifier,
|
||||
excludeLabelIdentifier: true,
|
||||
});
|
||||
|
||||
const flatDefaultRecordPageLayoutsToCreate =
|
||||
this.computeFlatDefaultRecordPageLayoutToCreate({
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
recordPageFieldsView: flatRecordPageFieldsViewToCreate,
|
||||
workspaceId,
|
||||
});
|
||||
const flatDefaultRecordPageLayoutsToCreate =
|
||||
this.computeFlatDefaultRecordPageLayoutToCreate({
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
flatApplication: twentyStandardFlatApplication,
|
||||
recordPageFieldsView: flatRecordPageFieldsViewToCreate,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const pageLayoutMigrationResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
view: {
|
||||
flatEntityToCreate: [flatRecordPageFieldsViewToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: flatRecordPageFieldsViewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayout: {
|
||||
flatEntityToCreate:
|
||||
flatDefaultRecordPageLayoutsToCreate.pageLayouts,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate:
|
||||
flatDefaultRecordPageLayoutsToCreate.pageLayoutTabs,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate:
|
||||
flatDefaultRecordPageLayoutsToCreate.pageLayoutWidgets,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
const pageLayoutMigrationResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
view: {
|
||||
flatEntityToCreate: [flatRecordPageFieldsViewToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: flatRecordPageFieldsViewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayout: {
|
||||
flatEntityToCreate:
|
||||
flatDefaultRecordPageLayoutsToCreate.pageLayouts,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate:
|
||||
flatDefaultRecordPageLayoutsToCreate.pageLayoutTabs,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate:
|
||||
flatDefaultRecordPageLayoutsToCreate.pageLayoutWidgets,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (pageLayoutMigrationResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
pageLayoutMigrationResult,
|
||||
'Multiple validation errors occurred while creating page layouts for object',
|
||||
);
|
||||
}
|
||||
if (pageLayoutMigrationResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
pageLayoutMigrationResult,
|
||||
'Multiple validation errors occurred while creating page layouts for object',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
|
||||
|
||||
-2
@@ -233,12 +233,10 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_UNIQUE_INDEXES_ENABLED: false,
|
||||
IS_JSON_FILTER_ENABLED: false,
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE: false,
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: false,
|
||||
IS_PUBLIC_DOMAIN_ENABLED: false,
|
||||
IS_EMAILING_DOMAIN_ENABLED: false,
|
||||
IS_EMAIL_GROUP_ENABLED: false,
|
||||
IS_JUNCTION_RELATIONS_ENABLED: false,
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: false,
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: false,
|
||||
},
|
||||
userWorkspaceRoleMap: {},
|
||||
|
||||
-10
@@ -50,16 +50,6 @@ export const seedFeatureFlags = async ({
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
|
||||
+1
-1
@@ -264,7 +264,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
|
||||
shortLabel: 'Edit Layout',
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
conditionalAvailabilityExpression:
|
||||
'pageType == "RECORD_PAGE" and featureFlags.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED and not isLayoutCustomizationModeEnabled and noneDefined(selectedRecords, "deletedAt") and objectPermissions.canUpdateObjectRecords and objectMetadataItem.nameSingular != "dashboard"',
|
||||
'pageType == "RECORD_PAGE" and not isLayoutCustomizationModeEnabled and noneDefined(selectedRecords, "deletedAt") and objectPermissions.canUpdateObjectRecords and objectMetadataItem.nameSingular != "dashboard"',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.EDIT_RECORD_PAGE_LAYOUT,
|
||||
|
||||
-2
@@ -1,7 +1,5 @@
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = [
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
FeatureFlagKey.IS_REST_METADATA_API_NEW_FORMAT_DIRECT,
|
||||
] as const satisfies FeatureFlagKey[];
|
||||
|
||||
@@ -2,11 +2,9 @@ export enum FeatureFlagKey {
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED = 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED',
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user