Fields widget draft view (#19562)

- Defer backend view creation for FIELDS widgets to prevent orphan views
when users cancel or leave before saving. Views are now created
optimistically in Jotai state (client-side UUID) and only persisted to
the database when the page layout is saved.
- Align the frontend's default field groups fallback with the backend
logic: split into General/Other groups, hide relation fields by default,
and filter invisible fields consistently across all code paths.
This commit is contained in:
Weiko
2026-04-12 20:26:47 +02:00
committed by GitHub
parent 63806b24fe
commit 9e385e44f9
15 changed files with 253 additions and 236 deletions
@@ -46,7 +46,7 @@ const LayoutCustomizationBarContent = () => {
ease: 'easeInOut',
}}
>
<StyledContainer>
<StyledContainer data-globally-prevent-click-outside="true">
<StyledTitle>
<IconPaint size={theme.icon.size.md} />
{t`Layout customization`}
@@ -6,6 +6,7 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/sta
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews';
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
@@ -38,6 +39,8 @@ export const useSaveLayoutCustomization = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const { updatePageLayoutWithTabsAndWidgets } =
useUpdatePageLayoutWithTabsAndWidgets();
const { createPendingFieldsWidgetViews } =
useCreatePendingFieldsWidgetViews();
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
@@ -104,6 +107,8 @@ export const useSaveLayoutCustomization = () => {
persistedAsDraft,
);
await createPendingFieldsWidgetViews(pageLayoutId);
if (isPageLayoutStructureDirty) {
const updateInput = convertPageLayoutDraftToUpdateInput(draft, {
shouldFilterDynamicRelationWidgets:
@@ -174,6 +179,7 @@ export const useSaveLayoutCustomization = () => {
saveDraft,
saveCommandMenuItemsDraft,
isCommandMenuItemsDirty,
createPendingFieldsWidgetViews,
updatePageLayoutWithTabsAndWidgets,
savePageLayoutWidgetsData,
exitLayoutCustomizationMode,
@@ -0,0 +1,94 @@
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ViewType, WidgetType } from '~/generated-metadata/graphql';
export const useCreatePendingFieldsWidgetViews = () => {
const { performViewAPICreate } = usePerformViewAPIPersist();
const store = useStore();
const createPendingFieldsWidgetViews = useCallback(
async (pageLayoutId: string) => {
const draft = store.get(
pageLayoutDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const persisted = store.get(
pageLayoutPersistedComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const persistedWidgetIds = new Set(
persisted?.tabs.flatMap((tab) =>
tab.widgets.map((widget) => widget.id),
) ?? [],
);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const newFieldsWidgets = draft.tabs
.flatMap((tab) => tab.widgets)
.filter((widget) => {
if (widget.type !== WidgetType.FIELDS) {
return false;
}
const viewId = getWidgetConfigurationViewId(widget.configuration);
return isDefined(viewId) && !persistedWidgetIds.has(widget.id);
});
for (const widget of newFieldsWidgets) {
const viewId = getWidgetConfigurationViewId(widget.configuration);
if (!isDefined(viewId)) {
continue;
}
const objectMetadataId =
widget.objectMetadataId ?? draft.objectMetadataId;
if (!isDefined(objectMetadataId)) {
continue;
}
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataId,
);
const viewName = isDefined(objectMetadataItem)
? `${objectMetadataItem.labelSingular} Fields`
: 'Fields';
const result = await performViewAPICreate(
{
input: {
id: viewId,
name: viewName,
icon: 'IconList',
objectMetadataId,
type: ViewType.FIELDS_WIDGET,
},
},
objectMetadataId,
);
if (result.status === 'failed') {
throw new Error(
`Failed to create view for FIELDS widget ${widget.id}`,
);
}
}
},
[performViewAPICreate, store],
);
return { createPendingFieldsWidgetViews };
};
@@ -5,7 +5,6 @@ import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDr
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget';
import { useCreateViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
@@ -25,8 +24,6 @@ export const useCreateRecordPageFieldsWidget = () => {
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
const { createViewForFieldsWidget } = useCreateViewForFieldsWidget();
const pageLayoutDraftState = useAtomComponentStateCallbackState(
pageLayoutDraftComponentState,
);
@@ -39,15 +36,8 @@ export const useCreateRecordPageFieldsWidget = () => {
const store = useStore();
const createRecordPageFieldsWidget = useCallback(async () => {
const viewId = await createViewForFieldsWidget({
objectMetadataId: objectMetadataItem.id,
viewName: `${objectMetadataItem.labelSingular} Fields`,
});
if (viewId === null) {
return;
}
const createRecordPageFieldsWidget = useCallback(() => {
const viewId = uuidv4();
const activeTab = currentPageLayout.tabs.find((tab) => tab.id === tabId);
const positionIndex = activeTab?.widgets.length ?? 0;
@@ -75,11 +65,9 @@ export const useCreateRecordPageFieldsWidget = () => {
resetNavigationStack: true,
});
}, [
createViewForFieldsWidget,
currentPageLayout.tabs,
navigatePageLayoutSidePanel,
objectMetadataItem.id,
objectMetadataItem.labelSingular,
pageLayoutDraftState,
pageLayoutEditingWidgetIdState,
store,
@@ -2,6 +2,7 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget';
@@ -42,6 +43,11 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const { deleteViewForRecordTableWidget } =
useDeleteViewForRecordTableWidget();
const pageLayoutPersistedState = useAtomComponentStateCallbackState(
pageLayoutPersistedComponentState,
pageLayoutId,
);
const store = useStore();
const deletePageLayoutWidget = useCallback(
@@ -59,7 +65,17 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
(widget) => widget.id === widgetId,
);
const persisted = store.get(pageLayoutPersistedState);
const persistedWidgetIds = new Set(
persisted?.tabs.flatMap((tab) =>
tab.widgets.map((widget) => widget.id),
) ?? [],
);
const isWidgetPersisted = persistedWidgetIds.has(widgetId);
if (
isWidgetPersisted &&
isDefined(widgetToDelete) &&
widgetToDelete.type === WidgetType.RECORD_TABLE &&
'viewId' in widgetToDelete.configuration &&
@@ -71,6 +87,7 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
}
if (
isWidgetPersisted &&
isDefined(widgetToDelete) &&
widgetToDelete.type === WidgetType.FIELDS &&
'viewId' in widgetToDelete.configuration &&
@@ -112,6 +129,7 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutEditingWidgetIdState,
pageLayoutPersistedState,
store,
],
);
@@ -3,6 +3,7 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
@@ -48,6 +49,11 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
const { deleteViewForRecordTableWidget } =
useDeleteViewForRecordTableWidget();
const pageLayoutPersistedState = useAtomComponentStateCallbackState(
pageLayoutPersistedComponentState,
pageLayoutId,
);
const store = useStore();
const removePageLayoutWidgetAndPreservePosition = useCallback(
@@ -63,7 +69,16 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
(widget) => widget.id === widgetId,
);
if (isDefined(widgetToRemove)) {
const persisted = store.get(pageLayoutPersistedState);
const persistedWidgetIds = new Set(
persisted?.tabs.flatMap((tab) =>
tab.widgets.map((widget) => widget.id),
) ?? [],
);
const isWidgetPersisted = persistedWidgetIds.has(widgetId);
if (isWidgetPersisted && isDefined(widgetToRemove)) {
const viewId = getWidgetConfigurationViewId(
widgetToRemove.configuration,
);
@@ -122,6 +137,7 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
pageLayoutDraftState,
pageLayoutDraggedAreaState,
pageLayoutEditingWidgetIdState,
pageLayoutPersistedState,
store,
],
);
@@ -1,3 +1,4 @@
import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews';
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
@@ -41,12 +42,17 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const { updatePageLayoutWithTabsAndWidgets } =
useUpdatePageLayoutWithTabsAndWidgets();
const { createPendingFieldsWidgetViews } =
useCreatePendingFieldsWidgetViews();
const featureFlags = useFeatureFlagsMap();
const isRecordPageLayoutEditingEnabled =
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
const store = useStore();
const savePageLayout = useCallback(async () => {
await createPendingFieldsWidgetViews(pageLayoutId);
const pageLayoutDraft = store.get(pageLayoutDraftCallbackState);
const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft, {
shouldFilterDynamicRelationWidgets: !isRecordPageLayoutEditingEnabled,
@@ -84,6 +90,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
return result;
}, [
createPendingFieldsWidgetViews,
isRecordPageLayoutEditingEnabled,
pageLayoutCurrentLayoutsCallbackState,
pageLayoutDraftCallbackState,
@@ -368,15 +368,9 @@ export const WithViewFieldGroups: Story = {
},
};
export const WithInlineViewFields: Story = {
export const WithDefaultGroups: Story = {
render: () => {
const view = createView({
viewFields: [
createViewField('vf-name', nameField.id, 0),
createViewField('vf-employees', employeesField.id, 1),
createViewField('vf-address', addressField.id, 2),
],
});
const view = createView();
const widget = createFieldsWidget(FIELDS_VIEW_ID);
@@ -443,14 +437,12 @@ export const WithInlineViewFields: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const companyName = await canvas.findByText('Acme Corporation');
expect(companyName).toBeVisible();
const generalHeader = await canvas.findByText('General');
expect(generalHeader).toBeVisible();
const contactInfoHeader = canvas.queryByText('Contact Info');
expect(contactInfoHeader).toBeNull();
const generalHeader = canvas.queryByText('General');
expect(generalHeader).toBeNull();
const creationDateElements = await canvas.findAllByText('Creation date');
expect(creationDateElements.length).toBeGreaterThan(0);
expect(creationDateElements[0]).toBeVisible();
},
};
@@ -1,42 +0,0 @@
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { useCallback } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { ViewType } from '~/generated-metadata/graphql';
export const useCreateViewForFieldsWidget = () => {
const { performViewAPICreate } = usePerformViewAPIPersist();
const createViewForFieldsWidget = useCallback(
async ({
objectMetadataId,
viewName,
}: {
objectMetadataId: string;
viewName: string;
}) => {
const viewId = uuidv4();
const result = await performViewAPICreate(
{
input: {
id: viewId,
name: viewName,
icon: 'IconList',
objectMetadataId,
type: ViewType.FIELDS_WIDGET,
},
},
objectMetadataId,
);
if (result.status === 'failed') {
return null;
}
return viewId;
},
[performViewAPICreate],
);
return { createViewForFieldsWidget };
};
@@ -5,6 +5,7 @@ import {
type FieldsWidgetGroup,
type FieldsWidgetGroupField,
} from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { buildDefaultFieldsWidgetGroups } from '@/page-layout/widgets/fields/utils/buildDefaultFieldsWidgetGroups';
import { useViewById } from '@/views/hooks/useViewById';
import { useMemo } from 'react';
import {
@@ -199,8 +200,22 @@ export const useFieldsWidgetEditorGroupsData = ({
}
}
return { groups: [], ungroupedFields: [], editorMode: 'ungrouped' };
}, [objectMetadataItem, view, labelIdentifierFieldMetadataItem]);
return {
groups: buildDefaultFieldsWidgetGroups({
fields: objectMetadataItem.fields,
objectNameSingular,
labelIdentifierFieldMetadataItemId:
labelIdentifierFieldMetadataItem?.id,
}),
ungroupedFields: [],
editorMode: 'grouped' as const,
};
}, [
objectMetadataItem,
objectNameSingular,
view,
labelIdentifierFieldMetadataItem,
]);
return {
...result,
@@ -1,22 +0,0 @@
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
type UseFieldsWidgetFieldMetadataItemsProps = {
objectNameSingular: string;
};
export const useFieldsWidgetFieldMetadataItems = ({
objectNameSingular,
}: UseFieldsWidgetFieldMetadataItemsProps) => {
const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } =
useFieldListFieldMetadataItems({
objectNameSingular,
excludeFieldMetadataIds: [],
excludeCreatedAtAndUpdatedAt: true,
showRelationSections: true,
});
return {
inlineFieldMetadataItems,
legacyActivityTargetFieldMetadataItems,
};
};
@@ -1,12 +1,13 @@
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useFieldsWidgetFieldMetadataItems } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems';
import { type FieldsWidgetDisplayMode } from '@/page-layout/widgets/fields/types/FieldsWidgetDisplayMode';
import {
type FieldsWidgetGroup,
type FieldsWidgetGroupField,
} from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { buildDefaultFieldsWidgetGroups } from '@/page-layout/widgets/fields/utils/buildDefaultFieldsWidgetGroups';
import { filterDraftGroupsForDisplay } from '@/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay';
import { useViewById } from '@/views/hooks/useViewById';
import { useLingui } from '@lingui/react/macro';
import { useMemo } from 'react';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
@@ -19,14 +20,12 @@ export const useFieldsWidgetGroups = ({
viewId,
objectNameSingular,
}: UseFieldsWidgetGroupsParams) => {
const { t } = useLingui();
const { view } = useViewById(viewId);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } =
useFieldsWidgetFieldMetadataItems({
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({
objectNameSingular,
});
@@ -82,133 +81,26 @@ export const useFieldsWidgetGroups = ({
.filter((group) => group.fields.length > 0);
}
if (isDefined(view) && view.viewFields.length > 0) {
let globalIndex = 0;
const fields: FieldsWidgetGroupField[] = [...view.viewFields]
.sort((a, b) => a.position - b.position)
.filter((viewField) => viewField.isVisible)
.map((viewField) => {
const fieldMetadataItem = objectMetadataItem.fields.find(
(f) => f.id === viewField.fieldMetadataId,
);
if (!isDefined(fieldMetadataItem)) {
return null;
}
return {
fieldMetadataItem,
position: viewField.position,
isVisible: viewField.isVisible,
globalIndex: globalIndex++,
};
})
.filter(isDefined);
if (fields.length > 0) {
return [
{
id: `${view.id}-group-general`,
name: t`General`,
position: 0,
isVisible: true,
fields,
},
];
}
}
const fieldsToDisplay = [
...inlineFieldMetadataItems,
...legacyActivityTargetFieldMetadataItems,
];
if (fieldsToDisplay.length === 0) {
return [];
}
const generalFields: Array<{
fieldMetadataItem: (typeof fieldsToDisplay)[0];
position: number;
}> = [];
const otherFields: Array<{
fieldMetadataItem: (typeof fieldsToDisplay)[0];
position: number;
}> = [];
let generalPosition = 0;
let otherPosition = 0;
fieldsToDisplay.forEach((field) => {
if (field.isCustom === true) {
otherFields.push({
fieldMetadataItem: field,
position: otherPosition++,
});
} else {
generalFields.push({
fieldMetadataItem: field,
position: generalPosition++,
});
}
});
const groups: FieldsWidgetGroup[] = [];
let globalIndex = 0;
if (generalFields.length > 0) {
groups.push({
id: `${objectNameSingular}-group-general`,
name: t`General`,
position: 0,
isVisible: true,
fields: generalFields.map((field) => ({
fieldMetadataItem: field.fieldMetadataItem,
position: field.position,
isVisible: true,
globalIndex: globalIndex++,
})),
});
}
if (otherFields.length > 0) {
groups.push({
id: `${objectNameSingular}-group-other`,
name: t`Other`,
position: 1,
isVisible: true,
fields: otherFields.map((field) => ({
fieldMetadataItem: field.fieldMetadataItem,
position: field.position,
isVisible: true,
globalIndex: globalIndex++,
})),
});
}
return groups;
return filterDraftGroupsForDisplay(
buildDefaultFieldsWidgetGroups({
fields: objectMetadataItem.fields,
objectNameSingular,
labelIdentifierFieldMetadataItemId:
labelIdentifierFieldMetadataItem?.id,
}),
);
}, [
objectMetadataItem,
objectNameSingular,
t,
labelIdentifierFieldMetadataItem,
view,
inlineFieldMetadataItems,
legacyActivityTargetFieldMetadataItems,
]);
const displayMode: FieldsWidgetDisplayMode =
isDefined(view) &&
!isNonEmptyArray(view.viewFieldGroups) &&
view.viewFields.length > 0
? 'inline'
: 'grouped';
const displayMode: FieldsWidgetDisplayMode = 'grouped';
return {
groups,
displayMode,
isFromView:
isDefined(view) &&
(isNonEmptyArray(view.viewFieldGroups) || view.viewFields.length > 0),
isFromView: isDefined(view) && isNonEmptyArray(view.viewFieldGroups),
};
};
@@ -0,0 +1,65 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { FieldMetadataType } from 'twenty-shared/types';
import { isFieldMetadataEligibleForFieldsWidget } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
export const buildDefaultFieldsWidgetGroups = ({
fields,
objectNameSingular,
labelIdentifierFieldMetadataItemId,
}: {
fields: FieldMetadataItem[];
objectNameSingular: string;
labelIdentifierFieldMetadataItemId: string | undefined;
}): FieldsWidgetGroup[] => {
const eligibleFields = fields.filter((field) =>
isFieldMetadataEligibleForFieldsWidget({
fieldName: field.name,
fieldType: field.type,
isLabelIdentifierField: field.id === labelIdentifierFieldMetadataItemId,
}),
);
const standardFields = eligibleFields.filter((field) => !field.isCustom);
const customFields = eligibleFields.filter((field) => field.isCustom);
const isFieldVisible = (fieldType: FieldMetadataType) =>
fieldType !== FieldMetadataType.RELATION &&
fieldType !== FieldMetadataType.MORPH_RELATION;
const groups: FieldsWidgetGroup[] = [];
let globalIndex = 0;
if (standardFields.length > 0) {
groups.push({
id: uuidv4(),
name: 'General',
position: 0,
isVisible: true,
fields: standardFields.map((field, index) => ({
fieldMetadataItem: field,
position: index,
isVisible: isFieldVisible(field.type),
globalIndex: globalIndex++,
})),
});
}
if (customFields.length > 0) {
groups.push({
id: uuidv4(),
name: 'Other',
position: 1,
isVisible: true,
fields: customFields.map((field, index) => ({
fieldMetadataItem: field,
position: index,
isVisible: isFieldVisible(field.type),
globalIndex: globalIndex++,
})),
});
}
return groups;
};
@@ -15,7 +15,6 @@ import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPositi
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields';
import { getFieldWidgetDefaultDisplayMode } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig';
import { useCreateViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget';
import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget';
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
@@ -92,8 +91,6 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
const { insertCreatedWidgetAtContext } =
useInsertCreatedWidgetAtContext(pageLayoutId);
const { createViewForFieldsWidget } = useCreateViewForFieldsWidget();
const { deleteViewForFieldsWidget } = useDeleteViewForFieldsWidget();
const { deleteViewForRecordTableWidget } =
@@ -187,21 +184,14 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
}),
);
const handleCreateFieldsWidget = useCallback(async () => {
const handleCreateFieldsWidget = useCallback(() => {
if (!isDefined(tabId)) {
return;
}
const replacePositionIndex = getExistingWidgetPositionIndex();
const viewId = await createViewForFieldsWidget({
objectMetadataId: objectMetadataItem.id,
viewName: `${objectMetadataItem.labelSingular} Fields`,
});
if (viewId === null) {
return;
}
const viewId = uuidv4();
removeExistingWidgetIfReplacing();
@@ -233,12 +223,10 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
resetNavigationStack: true,
});
}, [
createViewForFieldsWidget,
getExistingWidgetPositionIndex,
insertCreatedWidgetAtContext,
navigatePageLayoutSidePanel,
objectMetadataItem.id,
objectMetadataItem.labelSingular,
pageLayoutDraftState,
removeExistingWidgetIfReplacing,
setPageLayoutEditingWidgetId,
@@ -108,7 +108,7 @@ export const SidePanelRecordPageFieldsSettings = () => {
Icon={IconLayoutSidebarRight}
hasSubMenu
onClick={handleNavigateToLayout}
description={t`${totalFieldsCount} fields`}
description={t`${totalFieldsCount} visible fields`}
contextualTextPosition="right"
/>
</SelectableListItem>