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:
+1
-1
@@ -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 +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,
|
||||
|
||||
+94
@@ -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 };
|
||||
};
|
||||
+2
-14
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
+17
-1
@@ -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,
|
||||
|
||||
+7
-15
@@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
-42
@@ -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 };
|
||||
};
|
||||
+17
-2
@@ -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,
|
||||
|
||||
-22
@@ -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,
|
||||
};
|
||||
};
|
||||
+16
-124
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
+65
@@ -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;
|
||||
};
|
||||
+2
-14
@@ -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,
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ export const SidePanelRecordPageFieldsSettings = () => {
|
||||
Icon={IconLayoutSidebarRight}
|
||||
hasSubMenu
|
||||
onClick={handleNavigateToLayout}
|
||||
description={t`${totalFieldsCount} fields`}
|
||||
description={t`${totalFieldsCount} visible fields`}
|
||||
contextualTextPosition="right"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
|
||||
Reference in New Issue
Block a user