Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b364ba1c41 | ||
|
|
0be36105ff | ||
|
|
acf46060b4 | ||
|
|
5dbce2c2c8 | ||
|
|
af27e0e300 | ||
|
|
7434b722e4 | ||
|
|
e5540e22fd | ||
|
|
f344984c20 | ||
|
|
4466c8f6db |
File diff suppressed because one or more lines are too long
+5
@@ -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 { useCreatePendingFieldWidgetTableViews } from '@/page-layout/hooks/useCreatePendingFieldWidgetTableViews';
|
||||
import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews';
|
||||
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
|
||||
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
|
||||
@@ -41,6 +42,8 @@ export const useSaveLayoutCustomization = () => {
|
||||
useUpdatePageLayoutWithTabsAndWidgets();
|
||||
const { createPendingFieldsWidgetViews } =
|
||||
useCreatePendingFieldsWidgetViews();
|
||||
const { createPendingFieldWidgetTableViews } =
|
||||
useCreatePendingFieldWidgetTableViews();
|
||||
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
|
||||
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
|
||||
|
||||
@@ -108,6 +111,7 @@ export const useSaveLayoutCustomization = () => {
|
||||
);
|
||||
|
||||
await createPendingFieldsWidgetViews(pageLayoutId);
|
||||
await createPendingFieldWidgetTableViews(pageLayoutId);
|
||||
|
||||
if (isPageLayoutStructureDirty) {
|
||||
const updateInput = convertPageLayoutDraftToUpdateInput(draft, {
|
||||
@@ -180,6 +184,7 @@ export const useSaveLayoutCustomization = () => {
|
||||
saveCommandMenuItemsDraft,
|
||||
isCommandMenuItemsDirty,
|
||||
createPendingFieldsWidgetViews,
|
||||
createPendingFieldWidgetTableViews,
|
||||
updatePageLayoutWithTabsAndWidgets,
|
||||
savePageLayoutWidgetsData,
|
||||
exitLayoutCustomizationMode,
|
||||
|
||||
+16
-13
@@ -1,9 +1,10 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
|
||||
import { lastLoadedRecordTableWidgetViewIdComponentState } from '@/object-record/record-table-widget/states/lastLoadedRecordTableWidgetViewIdComponentState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector';
|
||||
import { useStore } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -18,10 +19,11 @@ export const RecordTableWidgetViewLoadEffect = ({
|
||||
}: RecordTableWidgetViewLoadEffectProps) => {
|
||||
const { loadRecordIndexStates } = useLoadRecordIndexStates();
|
||||
|
||||
const [
|
||||
lastLoadedRecordTableWidgetViewId,
|
||||
setLastLoadedRecordTableWidgetViewId,
|
||||
] = useAtomComponentState(lastLoadedRecordTableWidgetViewIdComponentState);
|
||||
const store = useStore();
|
||||
|
||||
const lastLoadedAtom = useAtomComponentStateCallbackState(
|
||||
lastLoadedRecordTableWidgetViewIdComponentState,
|
||||
);
|
||||
|
||||
const viewFromViewId = useAtomFamilySelectorValue(
|
||||
viewFromViewIdFamilySelector,
|
||||
@@ -42,27 +44,28 @@ export const RecordTableWidgetViewLoadEffect = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const lastLoaded = store.get(lastLoadedAtom);
|
||||
|
||||
if (
|
||||
viewId === lastLoadedRecordTableWidgetViewId?.viewId &&
|
||||
objectMetadataItem.updatedAt ===
|
||||
lastLoadedRecordTableWidgetViewId?.objectMetadataItemUpdatedAt
|
||||
viewId === lastLoaded?.viewId &&
|
||||
objectMetadataItem.updatedAt === lastLoaded?.objectMetadataItemUpdatedAt
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadRecordIndexStates(viewFromViewId, objectMetadataItem);
|
||||
|
||||
setLastLoadedRecordTableWidgetViewId({
|
||||
store.set(lastLoadedAtom, {
|
||||
viewId,
|
||||
objectMetadataItemUpdatedAt: objectMetadataItem.updatedAt,
|
||||
});
|
||||
|
||||
loadRecordIndexStates(viewFromViewId, objectMetadataItem);
|
||||
}, [
|
||||
viewId,
|
||||
lastLoadedRecordTableWidgetViewId,
|
||||
setLastLoadedRecordTableWidgetViewId,
|
||||
viewFromViewId,
|
||||
viewHasFields,
|
||||
objectMetadataItem,
|
||||
store,
|
||||
lastLoadedAtom,
|
||||
loadRecordIndexStates,
|
||||
]);
|
||||
|
||||
|
||||
+1
@@ -167,6 +167,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
configurationType
|
||||
fieldDisplayMode
|
||||
fieldMetadataId
|
||||
viewId
|
||||
}
|
||||
... on FieldRichTextConfiguration {
|
||||
configurationType
|
||||
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { resolveRelatedObjectForFieldWidget } from '@/page-layout/utils/resolveRelatedObjectForFieldWidget';
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from '@/page-layout/widgets/field/constants/fieldWidgetTableDefaults';
|
||||
import { buildDefaultViewFieldInputs } from '@/page-layout/widgets/field/utils/buildDefaultViewFieldInputs';
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
import {
|
||||
FieldDisplayMode,
|
||||
ViewType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
type FieldWidgetWithConfig = PageLayoutWidget & {
|
||||
fieldConfig: FieldConfiguration;
|
||||
};
|
||||
|
||||
export const useCreatePendingFieldWidgetTableViews = () => {
|
||||
const { performViewAPICreate } = usePerformViewAPIPersist();
|
||||
const { performViewFieldAPICreate, performViewFieldAPIUpdate } =
|
||||
usePerformViewFieldAPIPersist();
|
||||
const store = useStore();
|
||||
|
||||
const persistDraftViewFields = useCallback(
|
||||
async (
|
||||
widget: PageLayoutWidget,
|
||||
viewId: string,
|
||||
ungroupedDraft: Record<
|
||||
string,
|
||||
Array<{
|
||||
viewFieldId?: string | null;
|
||||
fieldMetadataItem: { id: string };
|
||||
position: number;
|
||||
isVisible: boolean;
|
||||
}>
|
||||
>,
|
||||
) => {
|
||||
const draftFields = ungroupedDraft[widget.id] ?? [];
|
||||
|
||||
if (!isNonEmptyArray(draftFields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldsToCreate = draftFields
|
||||
.filter((field) => !isDefined(field.viewFieldId))
|
||||
.map((field) => ({
|
||||
id: v4(),
|
||||
viewId,
|
||||
fieldMetadataId: field.fieldMetadataItem.id,
|
||||
position: field.position,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
isVisible: field.isVisible,
|
||||
}));
|
||||
|
||||
const fieldsToUpdate = draftFields
|
||||
.filter((field): field is typeof field & { viewFieldId: string } =>
|
||||
isDefined(field.viewFieldId),
|
||||
)
|
||||
.map((field) => ({
|
||||
input: {
|
||||
id: field.viewFieldId,
|
||||
update: {
|
||||
isVisible: field.isVisible,
|
||||
position: field.position,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
if (isNonEmptyArray(fieldsToCreate)) {
|
||||
await performViewFieldAPICreate({ inputs: fieldsToCreate });
|
||||
}
|
||||
|
||||
if (isNonEmptyArray(fieldsToUpdate)) {
|
||||
await performViewFieldAPIUpdate(fieldsToUpdate);
|
||||
}
|
||||
},
|
||||
[performViewFieldAPICreate, performViewFieldAPIUpdate],
|
||||
);
|
||||
|
||||
const createPendingFieldWidgetTableViews = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
const draft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const persisted = store.get(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const ungroupedDraft = store.get(
|
||||
fieldsWidgetUngroupedFieldsDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const persistedViewIdsByWidgetId = new Map<string, string | null>();
|
||||
persisted?.tabs.forEach((tab) =>
|
||||
tab.widgets.forEach((widget) => {
|
||||
if (widget.configuration.configurationType === 'FIELD') {
|
||||
const config = widget.configuration as FieldConfiguration;
|
||||
persistedViewIdsByWidgetId.set(widget.id, config.viewId ?? null);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
|
||||
|
||||
const tableFieldWidgets: FieldWidgetWithConfig[] = draft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.filter(
|
||||
(
|
||||
widget,
|
||||
): widget is PageLayoutWidget & { type: typeof WidgetType.FIELD } =>
|
||||
widget.type === WidgetType.FIELD,
|
||||
)
|
||||
.map((widget) => ({
|
||||
...widget,
|
||||
fieldConfig: widget.configuration as FieldConfiguration,
|
||||
}))
|
||||
.filter(
|
||||
(widget) =>
|
||||
widget.fieldConfig.fieldDisplayMode === FieldDisplayMode.VIEW &&
|
||||
isDefined(widget.fieldConfig.viewId),
|
||||
);
|
||||
|
||||
const pendingWidgets = tableFieldWidgets.filter(
|
||||
(widget) =>
|
||||
persistedViewIdsByWidgetId.get(widget.id) !==
|
||||
widget.fieldConfig.viewId,
|
||||
);
|
||||
|
||||
const existingWidgetsWithDraft = tableFieldWidgets.filter((widget) => {
|
||||
if (
|
||||
persistedViewIdsByWidgetId.get(widget.id) !==
|
||||
widget.fieldConfig.viewId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isNonEmptyArray(ungroupedDraft[widget.id]);
|
||||
});
|
||||
|
||||
for (const widget of pendingWidgets) {
|
||||
const viewId = widget.fieldConfig.viewId;
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parentObjectMetadataId =
|
||||
widget.objectMetadataId ?? draft.objectMetadataId;
|
||||
|
||||
if (!isDefined(parentObjectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolved = resolveRelatedObjectForFieldWidget({
|
||||
objectMetadataItems,
|
||||
parentObjectMetadataId,
|
||||
fieldMetadataId: widget.fieldConfig.fieldMetadataId,
|
||||
});
|
||||
|
||||
if (!isDefined(resolved)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { relatedObject } = resolved;
|
||||
|
||||
const viewResult = await performViewAPICreate(
|
||||
{
|
||||
input: {
|
||||
id: viewId,
|
||||
name: `${relatedObject.labelPlural} Table`,
|
||||
icon: relatedObject.icon ?? 'IconTable',
|
||||
objectMetadataId: relatedObject.id,
|
||||
type: ViewType.TABLE_WIDGET,
|
||||
},
|
||||
},
|
||||
relatedObject.id,
|
||||
);
|
||||
|
||||
if (viewResult.status === 'failed') {
|
||||
throw new Error(
|
||||
`Failed to create view for FIELD table widget ${widget.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (isNonEmptyArray(ungroupedDraft[widget.id])) {
|
||||
await persistDraftViewFields(widget, viewId, ungroupedDraft);
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewFieldInputs = buildDefaultViewFieldInputs({
|
||||
relatedObject,
|
||||
viewId,
|
||||
});
|
||||
|
||||
await performViewFieldAPICreate({ inputs: viewFieldInputs });
|
||||
}
|
||||
|
||||
for (const widget of existingWidgetsWithDraft) {
|
||||
const viewId = widget.fieldConfig.viewId;
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await persistDraftViewFields(widget, viewId, ungroupedDraft);
|
||||
}
|
||||
},
|
||||
[
|
||||
performViewAPICreate,
|
||||
performViewFieldAPICreate,
|
||||
persistDraftViewFields,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
return { createPendingFieldWidgetTableViews };
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fiel
|
||||
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
|
||||
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
|
||||
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -13,6 +14,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type UpsertFieldsWidgetInput,
|
||||
type ViewFragmentFragment,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useSaveFieldsWidgetGroups = () => {
|
||||
@@ -52,12 +54,29 @@ export const useSaveFieldsWidgetGroups = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const widgetIds = new Set([
|
||||
const allWidgetIds = new Set([
|
||||
...Object.keys(allDraftGroups),
|
||||
...Object.keys(allPersistedGroups),
|
||||
...Object.keys(allUngroupedFieldsDraft),
|
||||
]);
|
||||
|
||||
const pageLayoutDraft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const fieldWidgetIds = new Set(
|
||||
pageLayoutDraft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.filter((widget) => widget.type === WidgetType.FIELD)
|
||||
.map((widget) => widget.id),
|
||||
);
|
||||
|
||||
const widgetIds = [...allWidgetIds].filter(
|
||||
(widgetId) => !fieldWidgetIds.has(widgetId),
|
||||
);
|
||||
|
||||
for (const widgetId of widgetIds) {
|
||||
const editorMode = allEditorModes[widgetId] ?? 'ungrouped';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCreatePendingFieldWidgetTableViews } from '@/page-layout/hooks/useCreatePendingFieldWidgetTableViews';
|
||||
import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews';
|
||||
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
@@ -45,6 +46,9 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
const { createPendingFieldsWidgetViews } =
|
||||
useCreatePendingFieldsWidgetViews();
|
||||
|
||||
const { createPendingFieldWidgetTableViews } =
|
||||
useCreatePendingFieldWidgetTableViews();
|
||||
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
|
||||
@@ -52,6 +56,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
|
||||
const savePageLayout = useCallback(async () => {
|
||||
await createPendingFieldsWidgetViews(pageLayoutId);
|
||||
await createPendingFieldWidgetTableViews(pageLayoutId);
|
||||
|
||||
const pageLayoutDraft = store.get(pageLayoutDraftCallbackState);
|
||||
const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft, {
|
||||
@@ -91,6 +96,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
return result;
|
||||
}, [
|
||||
createPendingFieldsWidgetViews,
|
||||
createPendingFieldWidgetTableViews,
|
||||
isRecordPageLayoutEditingEnabled,
|
||||
pageLayoutCurrentLayoutsCallbackState,
|
||||
pageLayoutDraftCallbackState,
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type FlatView } from '@/metadata-store/types/FlatView';
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
|
||||
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
|
||||
import { resolveRelatedObjectForFieldWidget } from '@/page-layout/utils/resolveRelatedObjectForFieldWidget';
|
||||
import { buildDefaultViewFieldInputs } from '@/page-layout/widgets/field/utils/buildDefaultViewFieldInputs';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
ViewOpenRecordIn,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useSeedDraftViewForFieldWidgetTable = () => {
|
||||
const store = useStore();
|
||||
|
||||
const seedDraftViewForFieldWidgetTable = useCallback(
|
||||
({
|
||||
viewId,
|
||||
fieldMetadataId,
|
||||
parentObjectMetadataId,
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: {
|
||||
viewId: string;
|
||||
fieldMetadataId: string;
|
||||
parentObjectMetadataId: string;
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
}) => {
|
||||
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
|
||||
|
||||
const resolved = resolveRelatedObjectForFieldWidget({
|
||||
objectMetadataItems,
|
||||
parentObjectMetadataId,
|
||||
fieldMetadataId,
|
||||
});
|
||||
|
||||
if (!isDefined(resolved)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { relatedObject } = resolved;
|
||||
|
||||
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
|
||||
const viewsCurrent = viewsEntry.current as FlatView[];
|
||||
|
||||
if (viewsCurrent.some((view) => view.id === viewId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syntheticView: FlatView = {
|
||||
id: viewId,
|
||||
name: `${relatedObject.labelPlural} Table`,
|
||||
icon: relatedObject.icon ?? 'IconTable',
|
||||
objectMetadataId: relatedObject.id,
|
||||
type: ViewType.TABLE_WIDGET,
|
||||
isCompact: false,
|
||||
shouldHideEmptyGroups: false,
|
||||
position: 0,
|
||||
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
|
||||
visibility: ViewVisibility.UNLISTED,
|
||||
key: null,
|
||||
};
|
||||
|
||||
store.set(metadataStoreState.atomFamily('views'), {
|
||||
...viewsEntry,
|
||||
current: [...viewsCurrent, syntheticView],
|
||||
});
|
||||
|
||||
const syntheticViewFields: FlatViewField[] = buildDefaultViewFieldInputs({
|
||||
relatedObject,
|
||||
viewId,
|
||||
}).map((input) => ({
|
||||
...input,
|
||||
isActive: true,
|
||||
}));
|
||||
|
||||
const viewFieldsEntry = store.get(
|
||||
metadataStoreState.atomFamily('viewFields'),
|
||||
);
|
||||
const viewFieldsCurrent = viewFieldsEntry.current as FlatViewField[];
|
||||
|
||||
store.set(metadataStoreState.atomFamily('viewFields'), {
|
||||
...viewFieldsEntry,
|
||||
current: [...viewFieldsCurrent, ...syntheticViewFields],
|
||||
});
|
||||
|
||||
const ungroupedDraftFields = syntheticViewFields
|
||||
.map((viewField, index) => {
|
||||
const fieldMetadataItem = relatedObject.fields.find(
|
||||
(field) => field.id === viewField.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
fieldMetadataItem,
|
||||
position: viewField.position,
|
||||
isVisible: viewField.isVisible,
|
||||
globalIndex: index,
|
||||
viewFieldId: viewField.id,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
store.set(
|
||||
fieldsWidgetUngroupedFieldsDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
(prev) => ({
|
||||
...prev,
|
||||
[widgetId]: ungroupedDraftFields,
|
||||
}),
|
||||
);
|
||||
|
||||
store.set(
|
||||
fieldsWidgetEditorModeDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
(prev) => ({
|
||||
...prev,
|
||||
[widgetId]: 'ungrouped',
|
||||
}),
|
||||
);
|
||||
|
||||
store.set(
|
||||
fieldsWidgetGroupsDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
(prev) => ({
|
||||
...prev,
|
||||
[widgetId]: [],
|
||||
}),
|
||||
);
|
||||
|
||||
store.set(
|
||||
hasInitializedFieldsWidgetGroupsDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
(prev) => ({
|
||||
...prev,
|
||||
[widgetId]: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { seedDraftViewForFieldWidgetTable };
|
||||
};
|
||||
@@ -8,4 +8,5 @@ export type FieldConfiguration = {
|
||||
configurationType: WidgetConfigurationType.FIELD;
|
||||
fieldMetadataId: string;
|
||||
fieldDisplayMode: FieldDisplayMode;
|
||||
viewId?: string | null;
|
||||
};
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const resolveRelatedObjectForFieldWidget = ({
|
||||
objectMetadataItems,
|
||||
parentObjectMetadataId,
|
||||
fieldMetadataId,
|
||||
}: {
|
||||
objectMetadataItems: EnrichedObjectMetadataItem[];
|
||||
parentObjectMetadataId: string;
|
||||
fieldMetadataId: string;
|
||||
}):
|
||||
| { field: FieldMetadataItem; relatedObject: EnrichedObjectMetadataItem }
|
||||
| undefined => {
|
||||
const parentObject = objectMetadataItems.find(
|
||||
(item) => item.id === parentObjectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(parentObject)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const field = parentObject.fields.find(
|
||||
(fieldItem) => fieldItem.id === fieldMetadataId,
|
||||
);
|
||||
const relatedObjectId = field?.relation?.targetObjectMetadata.id;
|
||||
|
||||
if (!isDefined(field) || !isDefined(relatedObjectId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const relatedObject = objectMetadataItems.find(
|
||||
(item) => item.id === relatedObjectId,
|
||||
);
|
||||
|
||||
if (!isDefined(relatedObject)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { field, relatedObject };
|
||||
};
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
|
||||
import { type WidgetAction } from '@/page-layout/widgets/types/WidgetAction';
|
||||
import { CustomError } from 'twenty-shared/utils';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
|
||||
import { WidgetActionFieldEdit } from './WidgetActionFieldEdit';
|
||||
import { WidgetActionFieldSeeAll } from './WidgetActionFieldSeeAll';
|
||||
|
||||
|
||||
+18
@@ -16,6 +16,7 @@ import { FieldWidgetMorphRelationCard } from '@/page-layout/widgets/field/compon
|
||||
import { FieldWidgetMorphRelationField } from '@/page-layout/widgets/field/components/FieldWidgetMorphRelationField';
|
||||
import { FieldWidgetRelationCard } from '@/page-layout/widgets/field/components/FieldWidgetRelationCard';
|
||||
import { FieldWidgetRelationField } from '@/page-layout/widgets/field/components/FieldWidgetRelationField';
|
||||
import { FieldWidgetRelationTable } from '@/page-layout/widgets/field/components/FieldWidgetRelationTable';
|
||||
import { assertFieldWidgetOrThrow } from '@/page-layout/widgets/field/utils/assertFieldWidgetOrThrow';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
@@ -157,6 +158,23 @@ export const FieldWidget = ({ widget }: FieldWidgetProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldDisplayMode === FieldDisplayMode.VIEW) {
|
||||
const viewId = widget.configuration.viewId;
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldWidgetRelationTable
|
||||
fieldDefinition={fieldDefinition}
|
||||
viewId={viewId}
|
||||
widgetId={widget.id}
|
||||
targetRecordId={targetRecord.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldWidgetRelationField
|
||||
fieldDefinition={fieldDefinition}
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { RecordTableWidgetProvider } from '@/object-record/record-table-widget/components/RecordTableWidgetProvider';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { FieldWidgetRelationTableGatedContent } from '@/page-layout/widgets/field/components/FieldWidgetRelationTableGatedContent';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type FieldWidgetRelationTableProps = {
|
||||
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
|
||||
viewId: string;
|
||||
widgetId: string;
|
||||
targetRecordId: string;
|
||||
};
|
||||
|
||||
export const FieldWidgetRelationTable = ({
|
||||
fieldDefinition,
|
||||
viewId,
|
||||
widgetId,
|
||||
targetRecordId,
|
||||
}: FieldWidgetRelationTableProps) => {
|
||||
const relatedObjectMetadataId =
|
||||
fieldDefinition.metadata.relationObjectMetadataId;
|
||||
|
||||
const { objectMetadataItem: relatedObjectMetadataItem } =
|
||||
useObjectMetadataItemById({ objectId: relatedObjectMetadataId ?? '' });
|
||||
|
||||
if (!isDefined(relatedObjectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<RecordTableWidgetProvider
|
||||
objectNameSingular={relatedObjectMetadataItem.nameSingular}
|
||||
viewId={viewId}
|
||||
widgetId={widgetId}
|
||||
>
|
||||
<FieldWidgetRelationTableGatedContent
|
||||
viewId={viewId}
|
||||
widgetId={widgetId}
|
||||
inverseRelationFieldMetadataId={
|
||||
fieldDefinition.metadata.relationFieldMetadataId
|
||||
}
|
||||
targetRecordId={targetRecordId}
|
||||
/>
|
||||
</RecordTableWidgetProvider>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { RecordTableWidget } from '@/object-record/record-table-widget/components/RecordTableWidget';
|
||||
import { lastLoadedRecordTableWidgetViewIdComponentState } from '@/object-record/record-table-widget/states/lastLoadedRecordTableWidgetViewIdComponentState';
|
||||
import { FieldWidgetRelationTableShimmer } from '@/page-layout/widgets/field/components/FieldWidgetRelationTableShimmer';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ViewFilterOperand } from '~/generated-metadata/graphql';
|
||||
|
||||
type FieldWidgetRelationTableGatedContentProps = {
|
||||
viewId: string;
|
||||
widgetId: string;
|
||||
inverseRelationFieldMetadataId: string;
|
||||
targetRecordId: string;
|
||||
};
|
||||
|
||||
export const FieldWidgetRelationTableGatedContent = ({
|
||||
viewId,
|
||||
widgetId,
|
||||
inverseRelationFieldMetadataId,
|
||||
targetRecordId,
|
||||
}: FieldWidgetRelationTableGatedContentProps) => {
|
||||
const [isScopedFilterReady, setIsScopedFilterReady] = useState(false);
|
||||
|
||||
const lastLoadedRecordTableWidgetViewId = useAtomComponentStateValue(
|
||||
lastLoadedRecordTableWidgetViewIdComponentState,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const currentRecordFiltersAtom = useAtomComponentStateCallbackState(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastLoadedRecordTableWidgetViewId?.viewId !== viewId) {
|
||||
setIsScopedFilterReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const filterId = `field-widget-relation-scope-${widgetId}`;
|
||||
|
||||
const existingFilters = store.get(currentRecordFiltersAtom);
|
||||
const alreadyApplied = existingFilters.some(
|
||||
(filter) =>
|
||||
filter.id === filterId &&
|
||||
filter.fieldMetadataId === inverseRelationFieldMetadataId &&
|
||||
filter.value ===
|
||||
JSON.stringify({
|
||||
isCurrentWorkspaceMemberSelected: false,
|
||||
selectedRecordIds: [targetRecordId],
|
||||
}),
|
||||
);
|
||||
|
||||
if (!alreadyApplied) {
|
||||
const scopedFilter: RecordFilter = {
|
||||
id: filterId,
|
||||
fieldMetadataId: inverseRelationFieldMetadataId,
|
||||
value: JSON.stringify({
|
||||
isCurrentWorkspaceMemberSelected: false,
|
||||
selectedRecordIds: [targetRecordId],
|
||||
}),
|
||||
displayValue: '',
|
||||
label: '',
|
||||
type: 'RELATION',
|
||||
operand: ViewFilterOperand.IS,
|
||||
};
|
||||
|
||||
store.set(currentRecordFiltersAtom, [scopedFilter]);
|
||||
}
|
||||
|
||||
setIsScopedFilterReady(true);
|
||||
}, [
|
||||
lastLoadedRecordTableWidgetViewId,
|
||||
viewId,
|
||||
widgetId,
|
||||
inverseRelationFieldMetadataId,
|
||||
targetRecordId,
|
||||
store,
|
||||
currentRecordFiltersAtom,
|
||||
]);
|
||||
|
||||
if (!isScopedFilterReady) {
|
||||
return <FieldWidgetRelationTableShimmer />;
|
||||
}
|
||||
|
||||
return <RecordTableWidget />;
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[4]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledRow = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const SHIMMER_ROW_COUNT = 5;
|
||||
const SHIMMER_COLUMN_WIDTHS = [140, 100, 120, 80];
|
||||
|
||||
export const FieldWidgetRelationTableShimmer = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={4}
|
||||
>
|
||||
<StyledContainer>
|
||||
{Array.from({ length: SHIMMER_ROW_COUNT }).map((_, rowIndex) => (
|
||||
<StyledRow key={rowIndex}>
|
||||
{SHIMMER_COLUMN_WIDTHS.map((width, columnIndex) => (
|
||||
<Skeleton
|
||||
key={columnIndex}
|
||||
width={width}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
|
||||
/>
|
||||
))}
|
||||
</StyledRow>
|
||||
))}
|
||||
</StyledContainer>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { lastLoadedRecordTableWidgetViewIdComponentState } from '@/object-record/record-table-widget/states/lastLoadedRecordTableWidgetViewIdComponentState';
|
||||
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from '@/page-layout/widgets/field/constants/fieldWidgetTableDefaults';
|
||||
import { type FieldsWidgetGroupField } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
type FieldWidgetTableDraftToViewSyncEffectProps = {
|
||||
viewId: string | null;
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const FieldWidgetTableDraftToViewSyncEffect = ({
|
||||
viewId,
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: FieldWidgetTableDraftToViewSyncEffectProps) => {
|
||||
const store = useStore();
|
||||
|
||||
const ungroupedDraft = useAtomComponentStateValue(
|
||||
fieldsWidgetUngroupedFieldsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const widgetDraftFields: FieldsWidgetGroupField[] =
|
||||
ungroupedDraft[widgetId] ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(viewId) || widgetDraftFields.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewFieldsEntry = store.get(
|
||||
metadataStoreState.atomFamily('viewFields'),
|
||||
);
|
||||
const currentViewFields = viewFieldsEntry.current as FlatViewField[];
|
||||
|
||||
const currentViewFieldsForThisView = currentViewFields.filter(
|
||||
(viewField) => viewField.viewId === viewId,
|
||||
);
|
||||
|
||||
const hasChanges = widgetDraftFields.some((draftField) => {
|
||||
const existing = currentViewFieldsForThisView.find(
|
||||
(viewField) =>
|
||||
viewField.id === draftField.viewFieldId ||
|
||||
viewField.fieldMetadataId === draftField.fieldMetadataItem.id,
|
||||
);
|
||||
|
||||
if (!isDefined(existing)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
existing.isVisible !== draftField.isVisible ||
|
||||
existing.position !== draftField.position
|
||||
);
|
||||
});
|
||||
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
const otherViewFields = currentViewFields.filter(
|
||||
(viewField) => viewField.viewId !== viewId,
|
||||
);
|
||||
|
||||
const draftViewFields: FlatViewField[] = widgetDraftFields.map(
|
||||
(draftField) => {
|
||||
const existingViewField = currentViewFieldsForThisView.find(
|
||||
(viewField) =>
|
||||
viewField.id === draftField.viewFieldId ||
|
||||
viewField.fieldMetadataId === draftField.fieldMetadataItem.id,
|
||||
);
|
||||
|
||||
return {
|
||||
id: existingViewField?.id ?? draftField.viewFieldId ?? v4(),
|
||||
viewId,
|
||||
fieldMetadataId: draftField.fieldMetadataItem.id,
|
||||
position: draftField.position,
|
||||
size: existingViewField?.size ?? DEFAULT_VIEW_FIELD_SIZE,
|
||||
isVisible: draftField.isVisible,
|
||||
isActive: true,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
store.set(metadataStoreState.atomFamily('viewFields'), {
|
||||
...viewFieldsEntry,
|
||||
current: [...otherViewFields, ...draftViewFields],
|
||||
});
|
||||
|
||||
store.set(
|
||||
lastLoadedRecordTableWidgetViewIdComponentState.atomFamily({
|
||||
instanceId: `record-table-widget-${widgetId}`,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
}, [widgetDraftFields, viewId, store, widgetId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+4
-1
@@ -1,11 +1,14 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { FieldDisplayMode } from '~/generated-metadata/graphql';
|
||||
|
||||
type FieldWidgetFieldTypeConfig = {
|
||||
export type FieldWidgetFieldTypeConfig = {
|
||||
availableDisplayModes: FieldDisplayMode[];
|
||||
defaultDisplayMode: FieldDisplayMode;
|
||||
};
|
||||
|
||||
// Static fallback used when only the field type is known (e.g. when the field
|
||||
// has not been selected yet). Relation-type-sensitive modes like VIEW are
|
||||
// resolved at runtime in getFieldWidgetDisplayModeConfig.
|
||||
export const FIELD_WIDGET_CONFIG: Partial<
|
||||
Record<FieldMetadataType, FieldWidgetFieldTypeConfig>
|
||||
> = {
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const DEFAULT_VIEW_FIELD_SIZE = 180;
|
||||
export const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6;
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import {
|
||||
DEFAULT_VIEW_FIELD_SIZE,
|
||||
INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
|
||||
} from '@/page-layout/widgets/field/constants/fieldWidgetTableDefaults';
|
||||
import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation';
|
||||
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const buildDefaultViewFieldInputs = ({
|
||||
relatedObject,
|
||||
viewId,
|
||||
}: {
|
||||
relatedObject: EnrichedObjectMetadataItem;
|
||||
viewId: string;
|
||||
}) => {
|
||||
const eligibleFields = relatedObject.fields.filter(
|
||||
filterFieldsForRecordTableViewCreation,
|
||||
);
|
||||
|
||||
const sortedFields = eligibleFields.toSorted(
|
||||
sortFieldsByRelevanceForRecordTableWidget(
|
||||
relatedObject.labelIdentifierFieldMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
return sortedFields.map((field, index) => ({
|
||||
id: v4(),
|
||||
viewId,
|
||||
fieldMetadataId: field.id,
|
||||
position: index,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
|
||||
}));
|
||||
};
|
||||
+67
-2
@@ -1,11 +1,61 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
import { FieldDisplayMode } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FIELD_WIDGET_CONFIG } from '@/page-layout/widgets/field/constants/fieldWidgetConfig';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
|
||||
import {
|
||||
FIELD_WIDGET_CONFIG,
|
||||
type FieldWidgetFieldTypeConfig,
|
||||
} from '@/page-layout/widgets/field/constants/fieldWidgetConfig';
|
||||
|
||||
type FieldMetadataItemLike = Pick<
|
||||
FieldMetadataItem,
|
||||
'type' | 'settings' | 'relation'
|
||||
>;
|
||||
|
||||
const isNonJunctionOneToManyRelation = (
|
||||
fieldMetadataItem: FieldMetadataItemLike,
|
||||
) => {
|
||||
if (fieldMetadataItem.type !== FieldMetadataType.RELATION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasJunctionConfig(fieldMetadataItem.settings)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relationType =
|
||||
fieldMetadataItem.relation?.type ??
|
||||
fieldMetadataItem.settings?.relationType;
|
||||
|
||||
return relationType === RelationType.ONE_TO_MANY;
|
||||
};
|
||||
|
||||
export const getFieldWidgetConfig = (fieldType: FieldMetadataType) =>
|
||||
FIELD_WIDGET_CONFIG[fieldType];
|
||||
|
||||
export const getFieldWidgetConfigForField = (
|
||||
fieldMetadataItem: FieldMetadataItemLike,
|
||||
): FieldWidgetFieldTypeConfig | undefined => {
|
||||
const baseConfig = FIELD_WIDGET_CONFIG[fieldMetadataItem.type];
|
||||
|
||||
if (baseConfig === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isNonJunctionOneToManyRelation(fieldMetadataItem)) {
|
||||
return {
|
||||
...baseConfig,
|
||||
availableDisplayModes: [
|
||||
...baseConfig.availableDisplayModes,
|
||||
FieldDisplayMode.VIEW,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return baseConfig;
|
||||
};
|
||||
|
||||
export const getFieldWidgetDefaultDisplayMode = (
|
||||
fieldType: FieldMetadataType,
|
||||
) =>
|
||||
@@ -18,7 +68,22 @@ export const getFieldWidgetAvailableDisplayModes = (
|
||||
FieldDisplayMode.FIELD,
|
||||
];
|
||||
|
||||
export const getFieldWidgetAvailableDisplayModesForField = (
|
||||
fieldMetadataItem: FieldMetadataItemLike,
|
||||
) =>
|
||||
getFieldWidgetConfigForField(fieldMetadataItem)?.availableDisplayModes ?? [
|
||||
FieldDisplayMode.FIELD,
|
||||
];
|
||||
|
||||
export const isDisplayModeValidForFieldType = (
|
||||
fieldType: FieldMetadataType,
|
||||
displayMode: FieldDisplayMode,
|
||||
) => getFieldWidgetAvailableDisplayModes(fieldType).includes(displayMode);
|
||||
|
||||
export const isDisplayModeValidForField = (
|
||||
fieldMetadataItem: FieldMetadataItemLike,
|
||||
displayMode: FieldDisplayMode,
|
||||
) =>
|
||||
getFieldWidgetAvailableDisplayModesForField(fieldMetadataItem).includes(
|
||||
displayMode,
|
||||
);
|
||||
|
||||
+3
-1
@@ -41,11 +41,13 @@ const StyledAddGroupButtonContainer = styled.div`
|
||||
type FieldsConfigurationEditorProps = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
hideAddGroup?: boolean;
|
||||
};
|
||||
|
||||
export const FieldsConfigurationEditor = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
hideAddGroup = false,
|
||||
}: FieldsConfigurationEditorProps) => {
|
||||
const { t } = useLingui();
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
@@ -207,7 +209,7 @@ export const FieldsConfigurationEditor = ({
|
||||
ungroupedFields={ungroupedFields}
|
||||
onMoveField={moveUngroupedField}
|
||||
onToggleFieldVisibility={toggleUngroupedFieldVisibility}
|
||||
onAddGroup={() => handleAddGroup({})}
|
||||
onAddGroup={hideAddGroup ? undefined : () => handleAddGroup({})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+11
-8
@@ -6,6 +6,7 @@ import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/com
|
||||
import { type FieldsWidgetGroupField } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconNewSection } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
@@ -19,7 +20,7 @@ type FieldsConfigurationUngroupedEditorProps = {
|
||||
ungroupedFields: FieldsWidgetGroupField[];
|
||||
onMoveField: (sourceIndex: number, destinationIndex: number) => void;
|
||||
onToggleFieldVisibility: (fieldMetadataId: string) => void;
|
||||
onAddGroup: () => void;
|
||||
onAddGroup?: () => void;
|
||||
};
|
||||
|
||||
export const FieldsConfigurationUngroupedEditor = ({
|
||||
@@ -80,13 +81,15 @@ export const FieldsConfigurationUngroupedEditor = ({
|
||||
))}
|
||||
{provided.placeholder}
|
||||
|
||||
<MenuItem
|
||||
LeftIcon={IconNewSection}
|
||||
text={t`Add a Group`}
|
||||
onClick={onAddGroup}
|
||||
withIconContainer
|
||||
withIconContainerBackground={false}
|
||||
/>
|
||||
{isDefined(onAddGroup) && (
|
||||
<MenuItem
|
||||
LeftIcon={IconNewSection}
|
||||
text={t`Add a Group`}
|
||||
onClick={onAddGroup}
|
||||
withIconContainer
|
||||
withIconContainerBackground={false}
|
||||
/>
|
||||
)}
|
||||
</StyledFieldsDroppable>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SidePanelNewSidebarItemViewObjectPickerSubPage } from '@/navigation-men
|
||||
import { SidePanelNewSidebarItemViewPickerSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewPickerSubPage';
|
||||
import { SidePanelNewSidebarItemViewSystemPickerSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewSystemPickerSubPage';
|
||||
import { SidePanelChartFilterSubPage } from '@/side-panel/pages/page-layout/components/SidePanelChartFilterSubPage';
|
||||
import { SidePanelFieldWidgetTableFieldsSubPage } from '@/side-panel/pages/page-layout/components/SidePanelFieldWidgetTableFieldsSubPage';
|
||||
import { SidePanelFieldsLayoutSubPage } from '@/side-panel/pages/page-layout/components/SidePanelFieldsLayoutSubPage';
|
||||
import { SidePanelRecordTableFilterSubPage } from '@/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFilterSubPage';
|
||||
import { SidePanelRecordTableSortSubPage } from '@/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableSortSubPage';
|
||||
@@ -19,6 +20,10 @@ export const SIDE_PANEL_SUB_PAGES_CONFIG = new Map<
|
||||
>([
|
||||
[SidePanelSubPages.PageLayoutGraphFilter, <SidePanelChartFilterSubPage />],
|
||||
[SidePanelSubPages.PageLayoutFieldsLayout, <SidePanelFieldsLayoutSubPage />],
|
||||
[
|
||||
SidePanelSubPages.PageLayoutFieldWidgetTableFields,
|
||||
<SidePanelFieldWidgetTableFieldsSubPage />,
|
||||
],
|
||||
[SidePanelSubPages.NewSidebarItemMainMenu, <SidePanelNewSidebarItemPage />],
|
||||
[
|
||||
SidePanelSubPages.PageLayoutRecordTableFilter,
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
|
||||
import { resolveRelatedObjectForFieldWidget } from '@/page-layout/utils/resolveRelatedObjectForFieldWidget';
|
||||
import { FieldWidgetTableDraftToViewSyncEffect } from '@/page-layout/widgets/field/components/FieldWidgetTableDraftToViewSyncEffect';
|
||||
import { FieldsConfigurationEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationEditor';
|
||||
import { FieldsWidgetGroupsDraftInitializationEffect } from '@/page-layout/widgets/fields/components/FieldsWidgetGroupsDraftInitializationEffect';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { FieldDisplayMode } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledFieldsLayoutContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
overflow-y: auto;
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const FIELD_WIDGET_TABLE_FIELDS_CONTEXT_STORE_INSTANCE_ID =
|
||||
'field-widget-table-fields-editor';
|
||||
|
||||
export const SidePanelFieldWidgetTableFieldsSubPage = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const pageLayoutDraft = useAtomComponentStateValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fieldConfiguration =
|
||||
widgetInEditMode.configuration as FieldConfiguration;
|
||||
|
||||
if (fieldConfiguration.fieldDisplayMode !== FieldDisplayMode.VIEW) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentObjectMetadataId =
|
||||
widgetInEditMode.objectMetadataId ?? pageLayoutDraft.objectMetadataId;
|
||||
|
||||
if (!isDefined(parentObjectMetadataId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolved = resolveRelatedObjectForFieldWidget({
|
||||
objectMetadataItems,
|
||||
parentObjectMetadataId,
|
||||
fieldMetadataId: fieldConfiguration.fieldMetadataId,
|
||||
});
|
||||
|
||||
if (!isDefined(resolved)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: FIELD_WIDGET_TABLE_FIELDS_CONTEXT_STORE_INSTANCE_ID,
|
||||
}}
|
||||
>
|
||||
<SidePanelFieldWidgetTableFieldsSubPageContent
|
||||
relatedObjectMetadataItemId={resolved.relatedObject.id}
|
||||
viewId={fieldConfiguration.viewId ?? null}
|
||||
pageLayoutId={pageLayoutId}
|
||||
widgetId={widgetInEditMode.id}
|
||||
/>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
type SidePanelFieldWidgetTableFieldsSubPageContentProps = {
|
||||
relatedObjectMetadataItemId: string;
|
||||
viewId: string | null;
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
const SidePanelFieldWidgetTableFieldsSubPageContent = ({
|
||||
relatedObjectMetadataItemId,
|
||||
viewId,
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: SidePanelFieldWidgetTableFieldsSubPageContentProps) => {
|
||||
const setContextStoreCurrentObjectMetadataItemId = useSetAtomComponentState(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
);
|
||||
|
||||
const [isContextStoreReady, setIsContextStoreReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setContextStoreCurrentObjectMetadataItemId(relatedObjectMetadataItemId);
|
||||
setIsContextStoreReady(true);
|
||||
}, [relatedObjectMetadataItemId, setContextStoreCurrentObjectMetadataItemId]);
|
||||
|
||||
if (!isContextStoreReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledFieldsLayoutContainer>
|
||||
<FieldsWidgetGroupsDraftInitializationEffect
|
||||
viewId={viewId}
|
||||
pageLayoutId={pageLayoutId}
|
||||
widgetId={widgetId}
|
||||
/>
|
||||
<FieldWidgetTableDraftToViewSyncEffect
|
||||
viewId={viewId}
|
||||
pageLayoutId={pageLayoutId}
|
||||
widgetId={widgetId}
|
||||
/>
|
||||
<FieldsConfigurationEditor
|
||||
pageLayoutId={pageLayoutId}
|
||||
widgetId={widgetId}
|
||||
hideAddGroup
|
||||
/>
|
||||
</StyledFieldsLayoutContainer>
|
||||
);
|
||||
};
|
||||
+38
-3
@@ -1,10 +1,11 @@
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useSeedDraftViewForFieldWidgetTable } from '@/page-layout/hooks/useSeedDraftViewForFieldWidgetTable';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields';
|
||||
import {
|
||||
getFieldWidgetDefaultDisplayMode,
|
||||
isDisplayModeValidForFieldType,
|
||||
isDisplayModeValidForField,
|
||||
} from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
@@ -23,7 +24,11 @@ import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { type FieldConfiguration } from '~/generated-metadata/graphql';
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
FieldDisplayMode,
|
||||
type FieldConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const FieldWidgetFieldDropdownContent = () => {
|
||||
@@ -57,6 +62,11 @@ export const FieldWidgetFieldDropdownContent = () => {
|
||||
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
|
||||
const { seedDraftViewForFieldWidgetTable } =
|
||||
useSeedDraftViewForFieldWidgetTable();
|
||||
|
||||
const parentObjectMetadataId = widgetInEditMode?.objectMetadataId;
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
@@ -80,11 +90,19 @@ export const FieldWidgetFieldDropdownContent = () => {
|
||||
const needsDisplayModeSwitch =
|
||||
isDefined(selectedField) &&
|
||||
isDefined(currentDisplayMode) &&
|
||||
!isDisplayModeValidForFieldType(selectedField.type, currentDisplayMode);
|
||||
!isDisplayModeValidForField(selectedField, currentDisplayMode);
|
||||
|
||||
const staysInTableMode =
|
||||
currentDisplayMode === FieldDisplayMode.VIEW &&
|
||||
isDefined(selectedField) &&
|
||||
isDisplayModeValidForField(selectedField, FieldDisplayMode.VIEW);
|
||||
|
||||
const nextViewId = staysInTableMode ? v4() : null;
|
||||
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
fieldMetadataId,
|
||||
viewId: nextViewId,
|
||||
...(needsDisplayModeSwitch && {
|
||||
fieldDisplayMode: getFieldWidgetDefaultDisplayMode(
|
||||
selectedField.type,
|
||||
@@ -93,6 +111,23 @@ export const FieldWidgetFieldDropdownContent = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const widgetId = widgetInEditMode?.id;
|
||||
|
||||
if (
|
||||
staysInTableMode &&
|
||||
isDefined(nextViewId) &&
|
||||
isDefined(parentObjectMetadataId) &&
|
||||
isDefined(widgetId)
|
||||
) {
|
||||
seedDraftViewForFieldWidgetTable({
|
||||
viewId: nextViewId,
|
||||
fieldMetadataId,
|
||||
parentObjectMetadataId,
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
}
|
||||
|
||||
if (widgetInEditMode && selectedField) {
|
||||
updatePageLayoutWidget(widgetInEditMode.id, {
|
||||
title: selectedField.label,
|
||||
|
||||
+44
-3
@@ -1,5 +1,6 @@
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { getFieldWidgetAvailableDisplayModes } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig';
|
||||
import { useSeedDraftViewForFieldWidgetTable } from '@/page-layout/hooks/useSeedDraftViewForFieldWidgetTable';
|
||||
import { getFieldWidgetAvailableDisplayModesForField } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
@@ -18,8 +19,11 @@ import {
|
||||
IconFileText,
|
||||
IconLayoutKanban,
|
||||
IconListDetails,
|
||||
IconTable,
|
||||
} from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
FieldDisplayMode,
|
||||
type FieldConfiguration,
|
||||
@@ -29,7 +33,7 @@ const DISPLAY_MODE_ICONS: Record<FieldDisplayMode, IconComponent> = {
|
||||
[FieldDisplayMode.FIELD]: IconListDetails,
|
||||
[FieldDisplayMode.CARD]: IconLayoutKanban,
|
||||
[FieldDisplayMode.EDITOR]: IconFileText,
|
||||
[FieldDisplayMode.VIEW]: IconListDetails,
|
||||
[FieldDisplayMode.VIEW]: IconTable,
|
||||
};
|
||||
|
||||
export const FieldWidgetLayoutDropdownContent = () => {
|
||||
@@ -45,6 +49,7 @@ export const FieldWidgetLayoutDropdownContent = () => {
|
||||
|
||||
const currentDisplayMode = fieldConfiguration?.fieldDisplayMode;
|
||||
const currentFieldMetadataId = fieldConfiguration?.fieldMetadataId;
|
||||
const currentViewId = fieldConfiguration?.viewId;
|
||||
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
currentFieldMetadataId ?? '',
|
||||
@@ -53,7 +58,7 @@ export const FieldWidgetLayoutDropdownContent = () => {
|
||||
const layoutOptions = useMemo(
|
||||
() =>
|
||||
fieldMetadataItem
|
||||
? getFieldWidgetAvailableDisplayModes(fieldMetadataItem.type)
|
||||
? getFieldWidgetAvailableDisplayModesForField(fieldMetadataItem)
|
||||
: [FieldDisplayMode.FIELD],
|
||||
[fieldMetadataItem],
|
||||
);
|
||||
@@ -70,14 +75,49 @@ export const FieldWidgetLayoutDropdownContent = () => {
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { seedDraftViewForFieldWidgetTable } =
|
||||
useSeedDraftViewForFieldWidgetTable();
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const parentObjectMetadataId = widgetInEditMode?.objectMetadataId;
|
||||
|
||||
const handleSelectLayout = (fieldDisplayMode: FieldDisplayMode) => {
|
||||
// Generate a viewId up-front for VIEW mode so the draft carries it; the
|
||||
// actual TABLE_WIDGET view is created on Save. Null it on any other mode
|
||||
// so an abandoned selection does not leak a pending view creation.
|
||||
const nextViewId =
|
||||
fieldDisplayMode === FieldDisplayMode.VIEW
|
||||
? (currentViewId ?? v4())
|
||||
: null;
|
||||
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
fieldDisplayMode,
|
||||
viewId: nextViewId,
|
||||
},
|
||||
});
|
||||
|
||||
// Seed a synthetic view into the client metadata store so RecordTableWidget
|
||||
// can render immediately, before the real view is persisted on Save.
|
||||
const widgetId = widgetInEditMode?.id;
|
||||
|
||||
if (
|
||||
fieldDisplayMode === FieldDisplayMode.VIEW &&
|
||||
isDefined(nextViewId) &&
|
||||
isDefined(currentFieldMetadataId) &&
|
||||
isDefined(parentObjectMetadataId) &&
|
||||
isDefined(widgetId)
|
||||
) {
|
||||
seedDraftViewForFieldWidgetTable({
|
||||
viewId: nextViewId,
|
||||
fieldMetadataId: currentFieldMetadataId,
|
||||
parentObjectMetadataId,
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
}
|
||||
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
@@ -85,6 +125,7 @@ export const FieldWidgetLayoutDropdownContent = () => {
|
||||
[FieldDisplayMode.FIELD]: t`Field`,
|
||||
[FieldDisplayMode.CARD]: t`Card`,
|
||||
[FieldDisplayMode.EDITOR]: t`Editor`,
|
||||
[FieldDisplayMode.VIEW]: t`Table`,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+33
-1
@@ -1,7 +1,9 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
|
||||
import { FieldWidgetFieldDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent';
|
||||
import { FieldWidgetLayoutDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent';
|
||||
import { WidgetSettingsManageSection } from '@/side-panel/pages/page-layout/components/WidgetSettingsManageSection';
|
||||
@@ -10,12 +12,17 @@ import { WIDGET_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-lay
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useWidgetSettingsPlacementSelectableItemIds } from '@/side-panel/pages/page-layout/hooks/useWidgetSettingsPlacementSelectableItemIds';
|
||||
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLayoutSidebarRight, IconListDetails } from 'twenty-ui/display';
|
||||
import {
|
||||
IconColumns,
|
||||
IconLayoutSidebarRight,
|
||||
IconListDetails,
|
||||
} from 'twenty-ui/display';
|
||||
import {
|
||||
FieldDisplayMode,
|
||||
type FieldConfiguration,
|
||||
@@ -37,6 +44,7 @@ const StyledSidePanelContainer = styled.div`
|
||||
export const SidePanelRecordPageFieldSettings = () => {
|
||||
const { t } = useLingui();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
|
||||
|
||||
const { placementSelectableItemIds } =
|
||||
useWidgetSettingsPlacementSelectableItemIds(pageLayoutId);
|
||||
@@ -63,15 +71,25 @@ export const SidePanelRecordPageFieldSettings = () => {
|
||||
[FieldDisplayMode.FIELD]: t`Field`,
|
||||
[FieldDisplayMode.CARD]: t`Card`,
|
||||
[FieldDisplayMode.EDITOR]: t`Editor`,
|
||||
[FieldDisplayMode.VIEW]: t`Table`,
|
||||
};
|
||||
|
||||
const layoutLabel = isDefined(currentDisplayMode)
|
||||
? (displayModeLabels[currentDisplayMode] ?? '')
|
||||
: '';
|
||||
|
||||
const isTableDisplayMode = currentDisplayMode === FieldDisplayMode.VIEW;
|
||||
|
||||
const handleNavigateToFields = () => {
|
||||
navigateToSidePanelSubPage(
|
||||
SidePanelSubPages.PageLayoutFieldWidgetTableFields,
|
||||
);
|
||||
};
|
||||
|
||||
const selectableItemIds = [
|
||||
'field',
|
||||
'layout',
|
||||
...(isTableDisplayMode ? ['fields'] : []),
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION,
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT,
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.REPLACE_WIDGET,
|
||||
@@ -116,6 +134,20 @@ export const SidePanelRecordPageFieldSettings = () => {
|
||||
contextualTextPosition="right"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
{isTableDisplayMode && (
|
||||
<SelectableListItem
|
||||
itemId="fields"
|
||||
onEnter={handleNavigateToFields}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id="fields"
|
||||
label={t`Fields`}
|
||||
Icon={IconColumns}
|
||||
hasSubMenu
|
||||
onClick={handleNavigateToFields}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
</SidePanelGroup>
|
||||
<WidgetSettingsManageSection pageLayoutId={pageLayoutId} />
|
||||
<WidgetSettingsPlacementSection pageLayoutId={pageLayoutId} />
|
||||
|
||||
@@ -3,6 +3,7 @@ export enum SidePanelSubPages {
|
||||
PageLayoutFieldsLayout = 'page-layout-fields-layout',
|
||||
PageLayoutRecordTableFilter = 'page-layout-record-table-filter',
|
||||
PageLayoutRecordTableSort = 'page-layout-record-table-sort',
|
||||
PageLayoutFieldWidgetTableFields = 'page-layout-field-widget-table-fields',
|
||||
NewSidebarItemMainMenu = 'new-sidebar-item-main-menu',
|
||||
NewSidebarItemViewObjectPicker = 'new-sidebar-item-view-object-picker',
|
||||
NewSidebarItemViewPicker = 'new-sidebar-item-view-picker',
|
||||
|
||||
@@ -14,6 +14,8 @@ export const getSidePanelSubPageTitle = (
|
||||
return t`Filters`;
|
||||
case SidePanelSubPages.PageLayoutRecordTableSort:
|
||||
return t`Sorts`;
|
||||
case SidePanelSubPages.PageLayoutFieldWidgetTableFields:
|
||||
return t`Fields`;
|
||||
case SidePanelSubPages.NewSidebarItemMainMenu:
|
||||
return t`Add menu item`;
|
||||
case SidePanelSubPages.NewSidebarItemViewObjectPicker:
|
||||
|
||||
+18
-1
@@ -348,7 +348,7 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({
|
||||
}
|
||||
|
||||
case WidgetConfigurationType.FIELD: {
|
||||
const { fieldMetadataId, fieldDisplayMode, configurationType } =
|
||||
const { fieldMetadataId, fieldDisplayMode, configurationType, viewId } =
|
||||
configuration;
|
||||
|
||||
const fieldMetadataUniversalIdentifier =
|
||||
@@ -358,10 +358,27 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({
|
||||
shouldThrowOnMissingIdentifier,
|
||||
});
|
||||
|
||||
let viewUniversalIdentifier: string | null | undefined = undefined;
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
viewUniversalIdentifier = viewUniversalIdentifierById[viewId] ?? null;
|
||||
|
||||
if (
|
||||
!isDefined(viewUniversalIdentifier) &&
|
||||
shouldThrowOnMissingIdentifier
|
||||
) {
|
||||
throw new FlatEntityMapsException(
|
||||
`View universal identifier not found for id: ${viewId}`,
|
||||
FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
configurationType,
|
||||
fieldMetadataId: fieldMetadataUniversalIdentifier ?? fieldMetadataId,
|
||||
fieldDisplayMode,
|
||||
viewId: viewUniversalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -1,6 +1,12 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsEnum, IsIn, IsNotEmpty, IsString } from 'class-validator';
|
||||
import {
|
||||
IsEnum,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
import { type FieldConfiguration } from 'twenty-shared/types';
|
||||
|
||||
@@ -23,4 +29,9 @@ export class FieldConfigurationDTO implements FieldConfiguration {
|
||||
@IsEnum(FieldDisplayMode)
|
||||
@IsNotEmpty()
|
||||
fieldDisplayMode: FieldDisplayMode;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
viewId?: string | null;
|
||||
}
|
||||
|
||||
+25
-4
@@ -22,7 +22,7 @@ const resolveFieldMetadataIdOrThrow = ({
|
||||
}): string => {
|
||||
if (!isDefined(fieldMetadataUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Could not found any field metadata universal identifier while resolving page layout widget`,
|
||||
`Could not find any field metadata universal identifier while resolving page layout widget`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
@@ -337,15 +337,36 @@ export const fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration = ({
|
||||
}
|
||||
|
||||
case WidgetConfigurationType.FIELD: {
|
||||
const { fieldMetadataId: fieldMetadataUniversalIdentifier, ...rest } =
|
||||
universalConfiguration;
|
||||
const {
|
||||
fieldMetadataId: fieldMetadataUniversalIdentifier,
|
||||
viewId: viewUniversalIdentifier,
|
||||
...rest
|
||||
} = universalConfiguration;
|
||||
|
||||
const fieldMetadataId = resolveFieldMetadataIdOrThrow({
|
||||
fieldMetadataUniversalIdentifier,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return { ...rest, fieldMetadataId };
|
||||
let viewId: string | null | undefined = undefined;
|
||||
|
||||
if (isDefined(viewUniversalIdentifier)) {
|
||||
const flatView = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatViewMaps,
|
||||
universalIdentifier: viewUniversalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatView)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`View not found for universal identifier: ${viewUniversalIdentifier}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
viewId = flatView.id;
|
||||
}
|
||||
|
||||
return { ...rest, fieldMetadataId, viewId };
|
||||
}
|
||||
|
||||
case WidgetConfigurationType.VIEW:
|
||||
|
||||
@@ -100,6 +100,7 @@ export type FieldConfiguration = {
|
||||
configurationType: 'FIELD';
|
||||
fieldMetadataId: string;
|
||||
fieldDisplayMode: 'CARD' | 'EDITOR' | 'FIELD' | 'VIEW';
|
||||
viewId?: string | null;
|
||||
};
|
||||
|
||||
export type FieldsConfiguration = {
|
||||
|
||||
Reference in New Issue
Block a user