Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78c4f9c73c | ||
|
|
a710c105cf |
+15
-1
@@ -1,5 +1,4 @@
|
||||
import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
|
||||
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
|
||||
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
|
||||
@@ -10,6 +9,9 @@ import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layou
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -90,6 +92,18 @@ export const useCancelLayoutCustomization = () => {
|
||||
}),
|
||||
fieldsWidgetEditorModePersisted,
|
||||
);
|
||||
|
||||
const recordTableWidgetViewPersisted = store.get(
|
||||
recordTableWidgetViewPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
store.set(
|
||||
recordTableWidgetViewDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
recordTableWidgetViewPersisted,
|
||||
);
|
||||
}
|
||||
|
||||
exitLayoutCustomizationMode();
|
||||
|
||||
+23
-1
@@ -1,6 +1,5 @@
|
||||
import { useCommandMenuItemsDraftState } from '@/command-menu-item/hooks/useCommandMenuItemsDraftState';
|
||||
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
|
||||
@@ -8,6 +7,9 @@ import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/st
|
||||
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -86,6 +88,26 @@ export const useIsLayoutCustomizationDirty = () => {
|
||||
if (!isDeeplyEqual(ungroupedFieldsDraft, ungroupedFieldsPersisted)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const recordTableWidgetViewDraft = get(
|
||||
recordTableWidgetViewDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const recordTableWidgetViewPersisted = get(
|
||||
recordTableWidgetViewPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
recordTableWidgetViewDraft,
|
||||
recordTableWidgetViewPersisted,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
+5
@@ -7,6 +7,7 @@ import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/state
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
|
||||
import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews';
|
||||
import { useCreatePendingRecordTableWidgetViews } from '@/page-layout/hooks/useCreatePendingRecordTableWidgetViews';
|
||||
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
|
||||
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
@@ -42,6 +43,8 @@ export const useSaveLayoutCustomization = () => {
|
||||
useUpdatePageLayoutWithTabsAndWidgets();
|
||||
const { createPendingFieldsWidgetViews } =
|
||||
useCreatePendingFieldsWidgetViews();
|
||||
const { createPendingRecordTableWidgetViews } =
|
||||
useCreatePendingRecordTableWidgetViews();
|
||||
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
|
||||
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
|
||||
|
||||
@@ -113,6 +116,7 @@ export const useSaveLayoutCustomization = () => {
|
||||
);
|
||||
|
||||
await createPendingFieldsWidgetViews(pageLayoutId);
|
||||
await createPendingRecordTableWidgetViews(pageLayoutId);
|
||||
|
||||
if (isPageLayoutStructureDirty) {
|
||||
const updateInput = convertPageLayoutDraftToUpdateInput(draft, {
|
||||
@@ -185,6 +189,7 @@ export const useSaveLayoutCustomization = () => {
|
||||
saveCommandMenuItemsDraft,
|
||||
isCommandMenuItemsDirty,
|
||||
createPendingFieldsWidgetViews,
|
||||
createPendingRecordTableWidgetViews,
|
||||
updatePageLayoutWithTabsAndWidgets,
|
||||
savePageLayoutWidgetsData,
|
||||
exitLayoutCustomizationMode,
|
||||
|
||||
+1
@@ -98,6 +98,7 @@ export const RecordTableWidgetProvider = ({
|
||||
>
|
||||
<RecordTableWidgetViewLoadEffect
|
||||
viewId={viewId}
|
||||
widgetId={widgetId}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
{children}
|
||||
|
||||
+27
-9
@@ -1,6 +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 { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
|
||||
import { recordTableWidgetViewDraftByWidgetIdComponentFamilySelector } from '@/page-layout/states/selectors/recordTableWidgetViewDraftByWidgetIdComponentFamilySelector';
|
||||
import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot';
|
||||
import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector';
|
||||
@@ -9,11 +13,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordTableWidgetViewLoadEffectProps = {
|
||||
viewId: string;
|
||||
widgetId: string;
|
||||
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const RecordTableWidgetViewLoadEffect = ({
|
||||
viewId,
|
||||
widgetId,
|
||||
objectMetadataItem,
|
||||
}: RecordTableWidgetViewLoadEffectProps) => {
|
||||
const { loadRecordIndexStates } = useLoadRecordIndexStates();
|
||||
@@ -23,18 +29,30 @@ export const RecordTableWidgetViewLoadEffect = ({
|
||||
setLastLoadedRecordTableWidgetViewId,
|
||||
] = useAtomComponentState(lastLoadedRecordTableWidgetViewIdComponentState);
|
||||
|
||||
const viewFromViewId = useAtomFamilySelectorValue(
|
||||
viewFromViewIdFamilySelector,
|
||||
{
|
||||
viewId,
|
||||
},
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
|
||||
const draftSnapshot = useAtomComponentFamilySelectorValue(
|
||||
recordTableWidgetViewDraftByWidgetIdComponentFamilySelector,
|
||||
{ widgetId },
|
||||
);
|
||||
|
||||
const viewFromDraft =
|
||||
isPageLayoutInEditMode && isDefined(draftSnapshot)
|
||||
? constructViewFromRecordTableWidgetViewSnapshot(draftSnapshot)
|
||||
: undefined;
|
||||
|
||||
const viewFromSelector = useAtomFamilySelectorValue(
|
||||
viewFromViewIdFamilySelector,
|
||||
{ viewId },
|
||||
);
|
||||
|
||||
const currentView = viewFromDraft ?? viewFromSelector;
|
||||
|
||||
const viewHasFields =
|
||||
isDefined(viewFromViewId) && viewFromViewId.viewFields.length > 0;
|
||||
isDefined(currentView) && currentView.viewFields.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(viewFromViewId)) {
|
||||
if (!isDefined(currentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +68,7 @@ export const RecordTableWidgetViewLoadEffect = ({
|
||||
return;
|
||||
}
|
||||
|
||||
loadRecordIndexStates(viewFromViewId, objectMetadataItem);
|
||||
loadRecordIndexStates(currentView, objectMetadataItem);
|
||||
|
||||
setLastLoadedRecordTableWidgetViewId({
|
||||
viewId,
|
||||
@@ -60,7 +78,7 @@ export const RecordTableWidgetViewLoadEffect = ({
|
||||
viewId,
|
||||
lastLoadedRecordTableWidgetViewId,
|
||||
setLastLoadedRecordTableWidgetViewId,
|
||||
viewFromViewId,
|
||||
currentView,
|
||||
viewHasFields,
|
||||
objectMetadataItem,
|
||||
loadRecordIndexStates,
|
||||
|
||||
+3
-3
@@ -9,10 +9,10 @@ import {
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
jest.mock(
|
||||
'@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget',
|
||||
'@/page-layout/widgets/record-table/hooks/useRemoveDraftViewForRecordTableWidget',
|
||||
() => ({
|
||||
useDeleteViewForRecordTableWidget: () => ({
|
||||
deleteViewForRecordTableWidget: jest.fn(),
|
||||
useRemoveDraftViewForRecordTableWidget: () => ({
|
||||
removeDraftViewForRecordTableWidget: jest.fn(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCreatePendingRecordTableWidgetViews = () => {
|
||||
const { performViewAPICreate, performViewAPIDestroy } =
|
||||
usePerformViewAPIPersist();
|
||||
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
|
||||
const store = useStore();
|
||||
|
||||
const createPendingRecordTableWidgetViews = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
const draft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const persisted = store.get(
|
||||
pageLayoutPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const recordTableWidgetViewDraft = store.get(
|
||||
recordTableWidgetViewDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const persistedRecordTableWidgets = new Map(
|
||||
(persisted?.tabs ?? [])
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.filter((widget) => widget.type === WidgetType.RECORD_TABLE)
|
||||
.map((widget) => [
|
||||
widget.id,
|
||||
getWidgetConfigurationViewId(widget.configuration),
|
||||
]),
|
||||
);
|
||||
|
||||
const draftRecordTableWidgets = draft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.filter((widget) => widget.type === WidgetType.RECORD_TABLE);
|
||||
|
||||
const draftWidgetIds = new Set(
|
||||
draftRecordTableWidgets.map((widget) => widget.id),
|
||||
);
|
||||
|
||||
for (const widget of draftRecordTableWidgets) {
|
||||
const viewId = getWidgetConfigurationViewId(widget.configuration);
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const persistedViewId = persistedRecordTableWidgets.get(widget.id);
|
||||
|
||||
if (persistedViewId === viewId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDefined(persistedViewId)) {
|
||||
await performViewAPIDestroy({ id: persistedViewId });
|
||||
}
|
||||
|
||||
const widgetViewDraft = recordTableWidgetViewDraft[widget.id];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { view } = widgetViewDraft;
|
||||
|
||||
const result = await performViewAPICreate(
|
||||
{
|
||||
input: {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
icon: view.icon,
|
||||
objectMetadataId: view.objectMetadataId,
|
||||
type: view.type,
|
||||
isCompact: view.isCompact,
|
||||
position: view.position,
|
||||
openRecordIn: view.openRecordIn,
|
||||
visibility: view.visibility,
|
||||
shouldHideEmptyGroups: view.shouldHideEmptyGroups,
|
||||
},
|
||||
},
|
||||
view.objectMetadataId,
|
||||
);
|
||||
|
||||
if (result.status === 'failed') {
|
||||
throw new Error(
|
||||
`Failed to create view for RECORD_TABLE widget ${widget.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const viewFieldInputs = widgetViewDraft.viewFields.map((field) => ({
|
||||
id: field.id,
|
||||
viewId: field.viewId,
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
position: field.position,
|
||||
size: field.size,
|
||||
isVisible: field.isVisible,
|
||||
}));
|
||||
|
||||
if (viewFieldInputs.length > 0) {
|
||||
await performViewFieldAPICreate({ inputs: viewFieldInputs });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [widgetId, viewId] of persistedRecordTableWidgets) {
|
||||
if (!draftWidgetIds.has(widgetId) && isDefined(viewId)) {
|
||||
await performViewAPIDestroy({ id: viewId });
|
||||
}
|
||||
}
|
||||
|
||||
store.set(
|
||||
recordTableWidgetViewPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
recordTableWidgetViewDraft,
|
||||
);
|
||||
},
|
||||
[
|
||||
performViewAPICreate,
|
||||
performViewAPIDestroy,
|
||||
performViewFieldAPICreate,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
return { createPendingRecordTableWidgetViews };
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews';
|
||||
import { useCreatePendingRecordTableWidgetViews } from '@/page-layout/hooks/useCreatePendingRecordTableWidgetViews';
|
||||
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
@@ -45,6 +46,9 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
const { createPendingFieldsWidgetViews } =
|
||||
useCreatePendingFieldsWidgetViews();
|
||||
|
||||
const { createPendingRecordTableWidgetViews } =
|
||||
useCreatePendingRecordTableWidgetViews();
|
||||
|
||||
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 createPendingRecordTableWidgetViews(pageLayoutId);
|
||||
|
||||
const pageLayoutDraft = store.get(pageLayoutDraftCallbackState);
|
||||
const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft, {
|
||||
@@ -91,6 +96,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
return result;
|
||||
}, [
|
||||
createPendingFieldsWidgetViews,
|
||||
createPendingRecordTableWidgetViews,
|
||||
isRecordPageLayoutEditingEnabled,
|
||||
pageLayoutCurrentLayoutsCallbackState,
|
||||
pageLayoutDraftCallbackState,
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export const recordTableWidgetViewDraftComponentState =
|
||||
createAtomComponentState<Record<string, RecordTableWidgetViewSnapshot>>({
|
||||
key: 'recordTableWidgetViewDraftComponentState',
|
||||
defaultValue: {},
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export const recordTableWidgetViewPersistedComponentState =
|
||||
createAtomComponentState<Record<string, RecordTableWidgetViewSnapshot>>({
|
||||
key: 'recordTableWidgetViewPersistedComponentState',
|
||||
defaultValue: {},
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { createAtomComponentFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomComponentFamilySelector';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from '../contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export const recordTableWidgetViewDraftByWidgetIdComponentFamilySelector =
|
||||
createAtomComponentFamilySelector<
|
||||
RecordTableWidgetViewSnapshot | undefined,
|
||||
{ widgetId: string }
|
||||
>({
|
||||
key: 'recordTableWidgetViewDraftByWidgetIdComponentFamilySelector',
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId, familyKey }) =>
|
||||
({ get }) => {
|
||||
const draftMap = get(recordTableWidgetViewDraftComponentState, {
|
||||
instanceId,
|
||||
});
|
||||
|
||||
return draftMap[familyKey.widgetId];
|
||||
},
|
||||
});
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { type FlatViewFieldGroup } from '@/metadata-store/types/FlatViewFieldGroup';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { buildFieldsWidgetGroupsFromFlatViewData } from '@/page-layout/utils/buildFieldsWidgetGroupsFromFlatViewData';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
const createFieldMetadata = (
|
||||
overrides: Partial<FieldMetadataItem> & { id: string },
|
||||
): FieldMetadataItem =>
|
||||
({
|
||||
name: 'field',
|
||||
label: 'Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
...overrides,
|
||||
}) as FieldMetadataItem;
|
||||
|
||||
const createFlatViewField = (
|
||||
overrides: Partial<FlatViewField> & {
|
||||
id: string;
|
||||
fieldMetadataId: string;
|
||||
viewId: string;
|
||||
},
|
||||
): FlatViewField =>
|
||||
({
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
isActive: true,
|
||||
...overrides,
|
||||
}) as FlatViewField;
|
||||
|
||||
const createFlatViewFieldGroup = (
|
||||
overrides: Partial<FlatViewFieldGroup> & { id: string },
|
||||
): FlatViewFieldGroup =>
|
||||
({
|
||||
name: 'Group',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
...overrides,
|
||||
}) as FlatViewFieldGroup;
|
||||
|
||||
describe('buildFieldsWidgetGroupsFromFlatViewData', () => {
|
||||
const fm1 = createFieldMetadata({ id: 'fm-1', name: 'name', label: 'Name' });
|
||||
const fm2 = createFieldMetadata({
|
||||
id: 'fm-2',
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
});
|
||||
const fm3 = createFieldMetadata({
|
||||
id: 'fm-3',
|
||||
name: 'phone',
|
||||
label: 'Phone',
|
||||
});
|
||||
|
||||
describe('ungrouped mode', () => {
|
||||
it('should return ungrouped fields sorted by position when no groups exist', () => {
|
||||
const flatViewFields = [
|
||||
createFlatViewField({
|
||||
id: 'vf-2',
|
||||
fieldMetadataId: 'fm-2',
|
||||
viewId: 'v1',
|
||||
position: 1,
|
||||
}),
|
||||
createFlatViewField({
|
||||
id: 'vf-1',
|
||||
fieldMetadataId: 'fm-1',
|
||||
viewId: 'v1',
|
||||
position: 0,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = buildFieldsWidgetGroupsFromFlatViewData({
|
||||
flatViewFieldGroups: [],
|
||||
flatViewFields,
|
||||
fieldMetadataItems: [fm1, fm2],
|
||||
});
|
||||
|
||||
expect(result.editorMode).toBe('ungrouped');
|
||||
expect(result.groups).toEqual([]);
|
||||
expect(result.ungroupedFields).toHaveLength(2);
|
||||
expect(result.ungroupedFields[0].fieldMetadataItem.id).toBe('fm-1');
|
||||
expect(result.ungroupedFields[1].fieldMetadataItem.id).toBe('fm-2');
|
||||
});
|
||||
|
||||
it('should assign sequential globalIndex based on sorted position', () => {
|
||||
const flatViewFields = [
|
||||
createFlatViewField({
|
||||
id: 'vf-1',
|
||||
fieldMetadataId: 'fm-1',
|
||||
viewId: 'v1',
|
||||
position: 5,
|
||||
}),
|
||||
createFlatViewField({
|
||||
id: 'vf-2',
|
||||
fieldMetadataId: 'fm-2',
|
||||
viewId: 'v1',
|
||||
position: 2,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = buildFieldsWidgetGroupsFromFlatViewData({
|
||||
flatViewFieldGroups: [],
|
||||
flatViewFields,
|
||||
fieldMetadataItems: [fm1, fm2],
|
||||
});
|
||||
|
||||
expect(result.ungroupedFields[0].globalIndex).toBe(0);
|
||||
expect(result.ungroupedFields[0].fieldMetadataItem.id).toBe('fm-2');
|
||||
expect(result.ungroupedFields[1].globalIndex).toBe(1);
|
||||
expect(result.ungroupedFields[1].fieldMetadataItem.id).toBe('fm-1');
|
||||
});
|
||||
|
||||
it('should skip fields whose fieldMetadataId has no matching metadata', () => {
|
||||
const flatViewFields = [
|
||||
createFlatViewField({
|
||||
id: 'vf-1',
|
||||
fieldMetadataId: 'fm-1',
|
||||
viewId: 'v1',
|
||||
position: 0,
|
||||
}),
|
||||
createFlatViewField({
|
||||
id: 'vf-orphan',
|
||||
fieldMetadataId: 'fm-nonexistent',
|
||||
viewId: 'v1',
|
||||
position: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = buildFieldsWidgetGroupsFromFlatViewData({
|
||||
flatViewFieldGroups: [],
|
||||
flatViewFields,
|
||||
fieldMetadataItems: [fm1],
|
||||
});
|
||||
|
||||
expect(result.ungroupedFields).toHaveLength(1);
|
||||
expect(result.ungroupedFields[0].fieldMetadataItem.id).toBe('fm-1');
|
||||
});
|
||||
|
||||
it('should return empty ungroupedFields when there are no view fields', () => {
|
||||
const result = buildFieldsWidgetGroupsFromFlatViewData({
|
||||
flatViewFieldGroups: [],
|
||||
flatViewFields: [],
|
||||
fieldMetadataItems: [fm1],
|
||||
});
|
||||
|
||||
expect(result.editorMode).toBe('ungrouped');
|
||||
expect(result.ungroupedFields).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('grouped mode', () => {
|
||||
it('should return grouped fields when groups exist', () => {
|
||||
const group1 = createFlatViewFieldGroup({
|
||||
id: 'g1',
|
||||
name: 'General',
|
||||
position: 0,
|
||||
});
|
||||
const group2 = createFlatViewFieldGroup({
|
||||
id: 'g2',
|
||||
name: 'Details',
|
||||
position: 1,
|
||||
});
|
||||
|
||||
const flatViewFields = [
|
||||
createFlatViewField({
|
||||
id: 'vf-1',
|
||||
fieldMetadataId: 'fm-1',
|
||||
viewId: 'v1',
|
||||
position: 0,
|
||||
viewFieldGroupId: 'g1',
|
||||
}),
|
||||
createFlatViewField({
|
||||
id: 'vf-2',
|
||||
fieldMetadataId: 'fm-2',
|
||||
viewId: 'v1',
|
||||
position: 0,
|
||||
viewFieldGroupId: 'g2',
|
||||
}),
|
||||
];
|
||||
|
||||
const result = buildFieldsWidgetGroupsFromFlatViewData({
|
||||
flatViewFieldGroups: [group1, group2],
|
||||
flatViewFields,
|
||||
fieldMetadataItems: [fm1, fm2],
|
||||
});
|
||||
|
||||
expect(result.editorMode).toBe('grouped');
|
||||
expect(result.ungroupedFields).toEqual([]);
|
||||
expect(result.groups).toHaveLength(2);
|
||||
expect(result.groups[0].name).toBe('General');
|
||||
expect(result.groups[0].fields).toHaveLength(1);
|
||||
expect(result.groups[0].fields[0].fieldMetadataItem.id).toBe('fm-1');
|
||||
expect(result.groups[1].name).toBe('Details');
|
||||
expect(result.groups[1].fields[0].fieldMetadataItem.id).toBe('fm-2');
|
||||
});
|
||||
|
||||
it('should sort fields within each group by position', () => {
|
||||
const group = createFlatViewFieldGroup({ id: 'g1', name: 'All' });
|
||||
|
||||
const flatViewFields = [
|
||||
createFlatViewField({
|
||||
id: 'vf-3',
|
||||
fieldMetadataId: 'fm-3',
|
||||
viewId: 'v1',
|
||||
position: 2,
|
||||
viewFieldGroupId: 'g1',
|
||||
}),
|
||||
createFlatViewField({
|
||||
id: 'vf-1',
|
||||
fieldMetadataId: 'fm-1',
|
||||
viewId: 'v1',
|
||||
position: 0,
|
||||
viewFieldGroupId: 'g1',
|
||||
}),
|
||||
createFlatViewField({
|
||||
id: 'vf-2',
|
||||
fieldMetadataId: 'fm-2',
|
||||
viewId: 'v1',
|
||||
position: 1,
|
||||
viewFieldGroupId: 'g1',
|
||||
}),
|
||||
];
|
||||
|
||||
const result = buildFieldsWidgetGroupsFromFlatViewData({
|
||||
flatViewFieldGroups: [group],
|
||||
flatViewFields,
|
||||
fieldMetadataItems: [fm1, fm2, fm3],
|
||||
});
|
||||
|
||||
expect(
|
||||
result.groups[0].fields.map((f) => f.fieldMetadataItem.id),
|
||||
).toEqual(['fm-1', 'fm-2', 'fm-3']);
|
||||
});
|
||||
|
||||
it('should skip fields with missing metadata in grouped mode', () => {
|
||||
const group = createFlatViewFieldGroup({ id: 'g1', name: 'All' });
|
||||
|
||||
const flatViewFields = [
|
||||
createFlatViewField({
|
||||
id: 'vf-1',
|
||||
fieldMetadataId: 'fm-1',
|
||||
viewId: 'v1',
|
||||
position: 0,
|
||||
viewFieldGroupId: 'g1',
|
||||
}),
|
||||
createFlatViewField({
|
||||
id: 'vf-orphan',
|
||||
fieldMetadataId: 'fm-nonexistent',
|
||||
viewId: 'v1',
|
||||
position: 1,
|
||||
viewFieldGroupId: 'g1',
|
||||
}),
|
||||
];
|
||||
|
||||
const result = buildFieldsWidgetGroupsFromFlatViewData({
|
||||
flatViewFieldGroups: [group],
|
||||
flatViewFields,
|
||||
fieldMetadataItems: [fm1],
|
||||
});
|
||||
|
||||
expect(result.groups[0].fields).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should preserve group visibility in the output', () => {
|
||||
const group = createFlatViewFieldGroup({
|
||||
id: 'g1',
|
||||
name: 'Hidden Group',
|
||||
isVisible: false,
|
||||
});
|
||||
|
||||
const result = buildFieldsWidgetGroupsFromFlatViewData({
|
||||
flatViewFieldGroups: [group],
|
||||
flatViewFields: [],
|
||||
fieldMetadataItems: [],
|
||||
});
|
||||
|
||||
expect(result.groups[0].isVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+14
-7
@@ -1,6 +1,7 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { RecordTableWidget } from '@/object-record/record-table-widget/components/RecordTableWidget';
|
||||
import { RecordTableWidgetProvider } from '@/object-record/record-table-widget/components/RecordTableWidgetProvider';
|
||||
import { RecordTableWidgetViewDraftInitEffect } from '@/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect';
|
||||
|
||||
type RecordTableWidgetRendererContentProps = {
|
||||
objectMetadataId: string;
|
||||
@@ -18,12 +19,18 @@ export const RecordTableWidgetRendererContent = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<RecordTableWidgetProvider
|
||||
objectNameSingular={objectMetadataItem.nameSingular}
|
||||
viewId={viewId}
|
||||
widgetId={widgetId}
|
||||
>
|
||||
<RecordTableWidget />
|
||||
</RecordTableWidgetProvider>
|
||||
<>
|
||||
<RecordTableWidgetViewDraftInitEffect
|
||||
widgetId={widgetId}
|
||||
viewId={viewId}
|
||||
/>
|
||||
<RecordTableWidgetProvider
|
||||
objectNameSingular={objectMetadataItem.nameSingular}
|
||||
viewId={viewId}
|
||||
widgetId={widgetId}
|
||||
>
|
||||
<RecordTableWidget />
|
||||
</RecordTableWidgetProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
|
||||
import { useInitializeRecordTableWidgetViewDraft } from '@/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDraft';
|
||||
import { useViewById } from '@/views/hooks/useViewById';
|
||||
|
||||
type RecordTableWidgetViewDraftInitEffectProps = {
|
||||
widgetId: string;
|
||||
viewId: string;
|
||||
};
|
||||
|
||||
export const RecordTableWidgetViewDraftInitEffect = ({
|
||||
widgetId,
|
||||
viewId,
|
||||
}: RecordTableWidgetViewDraftInitEffectProps) => {
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
const { view } = useViewById(viewId);
|
||||
|
||||
useInitializeRecordTableWidgetViewDraft({
|
||||
widgetId,
|
||||
view: isPageLayoutInEditMode ? view : undefined,
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { type FlatView } from '@/metadata-store/types/FlatView';
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation';
|
||||
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
ViewOpenRecordIn,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const DEFAULT_VIEW_FIELD_SIZE = 180;
|
||||
const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6;
|
||||
|
||||
export const useAddDraftViewForRecordTableWidget = (pageLayoutId: string) => {
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const addDraftViewForRecordTableWidget = useCallback(
|
||||
(widgetId: string, objectMetadataItem: EnrichedObjectMetadataItem) => {
|
||||
const newViewId = v4();
|
||||
|
||||
const flatView: FlatView = {
|
||||
id: newViewId,
|
||||
name: `${objectMetadataItem.labelPlural} Table`,
|
||||
icon: objectMetadataItem.icon ?? 'IconTable',
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
type: ViewType.TABLE_WIDGET,
|
||||
isCompact: false,
|
||||
position: 0,
|
||||
openRecordIn: ViewOpenRecordIn.RECORD_PAGE,
|
||||
visibility: ViewVisibility.UNLISTED,
|
||||
shouldHideEmptyGroups: false,
|
||||
};
|
||||
|
||||
const eligibleFields = objectMetadataItem.fields.filter(
|
||||
filterFieldsForRecordTableViewCreation,
|
||||
);
|
||||
|
||||
const sortedFields = eligibleFields.toSorted(
|
||||
sortFieldsByRelevanceForRecordTableWidget(
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
const flatViewFields: FlatViewField[] = sortedFields.map(
|
||||
(field, index) => ({
|
||||
id: v4(),
|
||||
viewId: newViewId,
|
||||
fieldMetadataId: field.id,
|
||||
position: index,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
|
||||
isActive: true,
|
||||
}),
|
||||
);
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: { view: flatView, viewFields: flatViewFields },
|
||||
}));
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
updatePageLayoutWidget(widgetId, {
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.RECORD_TABLE,
|
||||
viewId: newViewId,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[store, recordTableWidgetViewDraftState, updatePageLayoutWidget],
|
||||
);
|
||||
|
||||
return { addDraftViewForRecordTableWidget };
|
||||
};
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation';
|
||||
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { useCallback } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
WidgetConfigurationType,
|
||||
ViewType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const DEFAULT_VIEW_FIELD_SIZE = 180;
|
||||
const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6;
|
||||
|
||||
export const useCreateViewForRecordTableWidget = (pageLayoutId: string) => {
|
||||
const { performViewAPICreate } = usePerformViewAPIPersist();
|
||||
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
|
||||
const createViewForRecordTableWidget = useCallback(
|
||||
async (
|
||||
widgetId: string,
|
||||
objectMetadataItem: EnrichedObjectMetadataItem,
|
||||
) => {
|
||||
const newViewId = v4();
|
||||
|
||||
const viewResult = await performViewAPICreate(
|
||||
{
|
||||
input: {
|
||||
id: newViewId,
|
||||
name: `${objectMetadataItem.labelPlural} Table`,
|
||||
icon: objectMetadataItem.icon ?? 'IconTable',
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
type: ViewType.TABLE_WIDGET,
|
||||
},
|
||||
},
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
if (viewResult.status !== 'successful') {
|
||||
return;
|
||||
}
|
||||
|
||||
const eligibleFields = objectMetadataItem.fields.filter(
|
||||
filterFieldsForRecordTableViewCreation,
|
||||
);
|
||||
|
||||
const sortedFields = eligibleFields.toSorted(
|
||||
sortFieldsByRelevanceForRecordTableWidget(
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
const viewFieldInputs = sortedFields.map((field, index) => ({
|
||||
id: v4(),
|
||||
viewId: newViewId,
|
||||
fieldMetadataId: field.id,
|
||||
position: index,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
|
||||
}));
|
||||
|
||||
try {
|
||||
await performViewFieldAPICreate({ inputs: viewFieldInputs });
|
||||
|
||||
updatePageLayoutWidget(widgetId, {
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.RECORD_TABLE,
|
||||
viewId: newViewId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
'Failed to create view fields for record table widget',
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
},
|
||||
[performViewAPICreate, performViewFieldAPICreate, updatePageLayoutWidget],
|
||||
);
|
||||
|
||||
return { createViewForRecordTableWidget };
|
||||
};
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useDeleteViewForRecordTableWidget = () => {
|
||||
const { performViewAPIDestroy } = usePerformViewAPIPersist();
|
||||
|
||||
const deleteViewForRecordTableWidget = useCallback(
|
||||
async (viewId: string) => {
|
||||
await performViewAPIDestroy({ id: viewId });
|
||||
},
|
||||
[performViewAPIDestroy],
|
||||
);
|
||||
|
||||
return { deleteViewForRecordTableWidget };
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
type UseInitializeRecordTableWidgetViewDraftParams = {
|
||||
widgetId: string;
|
||||
view: View | undefined;
|
||||
};
|
||||
|
||||
export const useInitializeRecordTableWidgetViewDraft = ({
|
||||
widgetId,
|
||||
view,
|
||||
}: UseInitializeRecordTableWidgetViewDraftParams) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
);
|
||||
|
||||
const recordTableWidgetViewPersistedState =
|
||||
useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewPersistedComponentState,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const initializeDraft = useCallback(() => {
|
||||
const currentDraft = store.get(recordTableWidgetViewDraftState);
|
||||
|
||||
if (widgetId in currentDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!view || view.viewFields.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewFields, ...viewProps } = view;
|
||||
|
||||
const flatViewFields: FlatViewField[] = viewFields.map((field) => ({
|
||||
...field,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const snapshot: RecordTableWidgetViewSnapshot = {
|
||||
view: viewProps,
|
||||
viewFields: flatViewFields,
|
||||
};
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: snapshot,
|
||||
}));
|
||||
|
||||
store.set(recordTableWidgetViewPersistedState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: snapshot,
|
||||
}));
|
||||
}, [
|
||||
recordTableWidgetViewDraftState,
|
||||
recordTableWidgetViewPersistedState,
|
||||
widgetId,
|
||||
view,
|
||||
store,
|
||||
]);
|
||||
|
||||
useEffect(initializeDraft, [initializeDraft]);
|
||||
};
|
||||
+19
-4
@@ -1,18 +1,33 @@
|
||||
import { useMapViewFieldToRecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/hooks/useMapViewFieldToRecordTableWidgetViewFieldItem';
|
||||
import { useRecordTableWidgetViewForDisplay } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay';
|
||||
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
|
||||
import { useViewById } from '@/views/hooks/useViewById';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { sortByProperty } from '~/utils/array/sortByProperty';
|
||||
|
||||
export const useRecordTableWidgetViewFieldItems = (viewId: string) => {
|
||||
const { view } = useViewById(viewId);
|
||||
type UseRecordTableWidgetViewFieldItemsParams = {
|
||||
viewId: string;
|
||||
widgetId: string;
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
export const useRecordTableWidgetViewFieldItems = ({
|
||||
viewId,
|
||||
widgetId,
|
||||
pageLayoutId,
|
||||
}: UseRecordTableWidgetViewFieldItemsParams) => {
|
||||
const { view } = useRecordTableWidgetViewForDisplay({
|
||||
viewId,
|
||||
widgetId,
|
||||
pageLayoutId,
|
||||
});
|
||||
|
||||
const { mapViewFieldToRecordTableWidgetViewFieldItem } =
|
||||
useMapViewFieldToRecordTableWidgetViewFieldItem();
|
||||
|
||||
const recordTableWidgetViewFieldItems: RecordTableWidgetViewFieldItem[] =
|
||||
useMemo(() => {
|
||||
if (!view) {
|
||||
if (!isDefined(view)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useViewById } from '@/views/hooks/useViewById';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseRecordTableWidgetViewForDisplayParams = {
|
||||
viewId: string;
|
||||
widgetId: string;
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
export const useRecordTableWidgetViewForDisplay = ({
|
||||
viewId,
|
||||
widgetId,
|
||||
pageLayoutId,
|
||||
}: UseRecordTableWidgetViewForDisplayParams): {
|
||||
view: View | undefined;
|
||||
} => {
|
||||
const { view } = useViewById(viewId);
|
||||
|
||||
const recordTableWidgetViewDraft = useAtomComponentStateValue(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const draftSnapshot = recordTableWidgetViewDraft[widgetId];
|
||||
|
||||
const viewFromDraft = isDefined(draftSnapshot)
|
||||
? constructViewFromRecordTableWidgetViewSnapshot(draftSnapshot)
|
||||
: undefined;
|
||||
|
||||
return { view: viewFromDraft ?? view };
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useRemoveDraftViewForRecordTableWidget = (
|
||||
pageLayoutId: string,
|
||||
) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const removeDraftViewForRecordTableWidget = useCallback(
|
||||
(widgetId: string) => {
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const { [widgetId]: _, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
},
|
||||
[store, recordTableWidgetViewDraftState],
|
||||
);
|
||||
|
||||
return { removeDraftViewForRecordTableWidget };
|
||||
};
|
||||
+47
-12
@@ -1,12 +1,28 @@
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useReorderRecordTableWidgetFields = () => {
|
||||
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
|
||||
type UseReorderRecordTableWidgetFieldsParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useReorderRecordTableWidgetFields = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseReorderRecordTableWidgetFieldsParams) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const reorderRecordTableWidgetFields = useCallback(
|
||||
async (
|
||||
(
|
||||
sourceIndex: number,
|
||||
destinationIndex: number,
|
||||
visibleFieldItems: RecordTableWidgetViewFieldItem[],
|
||||
@@ -19,16 +35,35 @@ export const useReorderRecordTableWidgetFields = () => {
|
||||
const [movedField] = reorderedFields.splice(sourceIndex, 1);
|
||||
reorderedFields.splice(destinationIndex, 0, movedField);
|
||||
|
||||
const updates = reorderedFields.map((fieldItem, index) => ({
|
||||
input: {
|
||||
id: fieldItem.viewField.id,
|
||||
update: { position: index },
|
||||
},
|
||||
}));
|
||||
const updatedPositions = new Map(
|
||||
reorderedFields.map((fieldItem, index) => [
|
||||
fieldItem.viewField.id,
|
||||
index,
|
||||
]),
|
||||
);
|
||||
|
||||
await performViewFieldAPIUpdate(updates);
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const widgetViewDraft = prev[widgetId];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFields: widgetViewDraft.viewFields.map((field) => {
|
||||
const newPosition = updatedPositions.get(field.id);
|
||||
return newPosition !== undefined
|
||||
? { ...field, position: newPosition }
|
||||
: field;
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[performViewFieldAPIUpdate],
|
||||
[store, recordTableWidgetViewDraftState, widgetId],
|
||||
);
|
||||
|
||||
return { reorderRecordTableWidgetFields };
|
||||
|
||||
+15
-4
@@ -5,6 +5,7 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { useMapRecordFieldToViewFieldWithCurrentAggregateOperation } from '@/page-layout/widgets/record-table/hooks/useMapRecordFieldToViewFieldWithCurrentAggregateOperation';
|
||||
import { computeViewFieldsToCreateAndUpdate } from '@/page-layout/widgets/record-table/utils/computeViewFieldsToCreateAndUpdate';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
@@ -65,6 +66,11 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
|
||||
);
|
||||
const views = store.get(viewsSelector.atom);
|
||||
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
|
||||
const recordTableWidgetViewDraft = store.get(
|
||||
recordTableWidgetViewDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const recordTableWidgets = pageLayoutDraft.tabs.flatMap((tab) =>
|
||||
tab.widgets.filter(
|
||||
@@ -182,16 +188,21 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const newViewFields = currentRecordFields.map(
|
||||
const recordIndexViewFields = currentRecordFields.map(
|
||||
mapRecordFieldToViewFieldWithCurrentAggregateOperation,
|
||||
);
|
||||
|
||||
const existingViewFields = currentView.viewFields ?? [];
|
||||
const draftSnapshot = recordTableWidgetViewDraft[widget.id];
|
||||
const draftViewFields = draftSnapshot?.viewFields ?? [];
|
||||
const metadataStoreViewFields =
|
||||
draftViewFields.length > 0
|
||||
? draftViewFields
|
||||
: (currentView.viewFields ?? []);
|
||||
|
||||
const { viewFieldsToCreate, viewFieldsToUpdate } =
|
||||
computeViewFieldsToCreateAndUpdate({
|
||||
newViewFields,
|
||||
existingViewFields,
|
||||
newViewFields: metadataStoreViewFields,
|
||||
existingViewFields: recordIndexViewFields,
|
||||
viewId,
|
||||
});
|
||||
|
||||
|
||||
+37
-12
@@ -1,21 +1,46 @@
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useToggleRecordTableWidgetFieldVisibility = () => {
|
||||
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
|
||||
type UseToggleRecordTableWidgetFieldVisibilityParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useToggleRecordTableWidgetFieldVisibility = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseToggleRecordTableWidgetFieldVisibilityParams) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const toggleRecordTableWidgetFieldVisibility = useCallback(
|
||||
async (viewFieldId: string, isVisible: boolean) => {
|
||||
await performViewFieldAPIUpdate([
|
||||
{
|
||||
input: {
|
||||
id: viewFieldId,
|
||||
update: { isVisible },
|
||||
(viewFieldId: string, isVisible: boolean) => {
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const widgetViewDraft = prev[widgetId];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFields: widgetViewDraft.viewFields.map((field) =>
|
||||
field.id === viewFieldId ? { ...field, isVisible } : field,
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
});
|
||||
},
|
||||
[performViewFieldAPIUpdate],
|
||||
[store, recordTableWidgetViewDraftState, widgetId],
|
||||
);
|
||||
|
||||
return { toggleRecordTableWidgetFieldVisibility };
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type FlatView } from '@/metadata-store/types/FlatView';
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
|
||||
export type RecordTableWidgetViewSnapshot = {
|
||||
view: FlatView;
|
||||
viewFields: FlatViewField[];
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
|
||||
const createField = (
|
||||
overrides: Partial<FieldMetadataItem> & { id: string },
|
||||
): FieldMetadataItem =>
|
||||
({
|
||||
name: 'field',
|
||||
label: 'Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
settings: null,
|
||||
...overrides,
|
||||
}) as FieldMetadataItem;
|
||||
|
||||
describe('sortFieldsByRelevanceForRecordTableWidget', () => {
|
||||
const labelIdentifierId = 'label-field-id';
|
||||
const sorter = sortFieldsByRelevanceForRecordTableWidget(labelIdentifierId);
|
||||
|
||||
it('should place the label identifier field first', () => {
|
||||
const labelField = createField({ id: labelIdentifierId });
|
||||
const textField = createField({ id: 'text-1' });
|
||||
|
||||
expect(sorter(labelField, textField)).toBe(-1);
|
||||
expect(sorter(textField, labelField)).toBe(1);
|
||||
});
|
||||
|
||||
it('should place ONE_TO_MANY relation fields after non-relation fields', () => {
|
||||
const reverseSide = createField({
|
||||
id: 'reverse-1',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: { relationType: RelationType.ONE_TO_MANY },
|
||||
});
|
||||
const textField = createField({ id: 'text-1' });
|
||||
|
||||
expect(sorter(reverseSide, textField)).toBe(1);
|
||||
expect(sorter(textField, reverseSide)).toBe(-1);
|
||||
});
|
||||
|
||||
it('should place regular relation fields after non-relation fields', () => {
|
||||
const relation = createField({
|
||||
id: 'rel-1',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: { relationType: RelationType.MANY_TO_ONE },
|
||||
});
|
||||
const textField = createField({ id: 'text-1' });
|
||||
|
||||
expect(sorter(relation, textField)).toBe(1);
|
||||
expect(sorter(textField, relation)).toBe(-1);
|
||||
});
|
||||
|
||||
it('should place ONE_TO_MANY relations after MANY_TO_ONE relations', () => {
|
||||
const reverseSide = createField({
|
||||
id: 'reverse-1',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: { relationType: RelationType.ONE_TO_MANY },
|
||||
});
|
||||
const manyToOne = createField({
|
||||
id: 'rel-1',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: { relationType: RelationType.MANY_TO_ONE },
|
||||
});
|
||||
|
||||
expect(sorter(reverseSide, manyToOne)).toBe(1);
|
||||
expect(sorter(manyToOne, reverseSide)).toBe(-1);
|
||||
});
|
||||
|
||||
it('should return 0 for two non-relation fields of equal priority', () => {
|
||||
const fieldA = createField({ id: 'a' });
|
||||
const fieldB = createField({ id: 'b' });
|
||||
|
||||
expect(sorter(fieldA, fieldB)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 for two ONE_TO_MANY relation fields', () => {
|
||||
const reverseA = createField({
|
||||
id: 'reverse-a',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: { relationType: RelationType.ONE_TO_MANY },
|
||||
});
|
||||
const reverseB = createField({
|
||||
id: 'reverse-b',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: { relationType: RelationType.ONE_TO_MANY },
|
||||
});
|
||||
|
||||
expect(sorter(reverseA, reverseB)).toBe(0);
|
||||
});
|
||||
|
||||
it('should produce a correct full sort order', () => {
|
||||
const labelField = createField({ id: labelIdentifierId });
|
||||
const textField = createField({ id: 'text-1' });
|
||||
const numberField = createField({
|
||||
id: 'number-1',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
});
|
||||
const manyToOneField = createField({
|
||||
id: 'rel-1',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: { relationType: RelationType.MANY_TO_ONE },
|
||||
});
|
||||
const oneToManyField = createField({
|
||||
id: 'reverse-1',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: { relationType: RelationType.ONE_TO_MANY },
|
||||
});
|
||||
|
||||
const fields = [
|
||||
oneToManyField,
|
||||
manyToOneField,
|
||||
textField,
|
||||
numberField,
|
||||
labelField,
|
||||
];
|
||||
|
||||
const sorted = [...fields].sort(sorter);
|
||||
|
||||
expect(sorted[0].id).toBe(labelIdentifierId);
|
||||
expect(sorted[sorted.length - 1].id).toBe('reverse-1');
|
||||
const labelIdx = sorted.findIndex((f) => f.id === labelIdentifierId);
|
||||
const textIdx = sorted.findIndex((f) => f.id === 'text-1');
|
||||
const relIdx = sorted.findIndex((f) => f.id === 'rel-1');
|
||||
const reverseIdx = sorted.findIndex((f) => f.id === 'reverse-1');
|
||||
expect(labelIdx).toBeLessThan(textIdx);
|
||||
expect(textIdx).toBeLessThan(relIdx);
|
||||
expect(relIdx).toBeLessThan(reverseIdx);
|
||||
});
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { type View } from '@/views/types/View';
|
||||
|
||||
export const constructViewFromRecordTableWidgetViewSnapshot = (
|
||||
snapshot: RecordTableWidgetViewSnapshot,
|
||||
): View => ({
|
||||
...snapshot.view,
|
||||
viewFields: snapshot.viewFields,
|
||||
viewFilters: [],
|
||||
viewSorts: [],
|
||||
viewGroups: [],
|
||||
viewFilterGroups: [],
|
||||
});
|
||||
+5
-5
@@ -11,7 +11,7 @@ import { useRemovePageLayoutWidgetAndPreservePosition } from '@/page-layout/hook
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
|
||||
import { useCreateViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget';
|
||||
import { useAddDraftViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useAddDraftViewForRecordTableWidget';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
@@ -83,8 +83,8 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => {
|
||||
const { removePageLayoutWidgetAndPreservePosition } =
|
||||
useRemovePageLayoutWidgetAndPreservePosition(pageLayoutId);
|
||||
|
||||
const { createViewForRecordTableWidget } =
|
||||
useCreateViewForRecordTableWidget(pageLayoutId);
|
||||
const { addDraftViewForRecordTableWidget } =
|
||||
useAddDraftViewForRecordTableWidget(pageLayoutId);
|
||||
const { readableObjectMetadataItems } = useReadableObjectMetadataItems();
|
||||
|
||||
const firstAvailableObjectMetadataItem =
|
||||
@@ -185,7 +185,7 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => {
|
||||
closeSidePanelMenu();
|
||||
};
|
||||
|
||||
const handleNavigateToRecordTableSettings = async () => {
|
||||
const handleNavigateToRecordTableSettings = () => {
|
||||
if (
|
||||
isExistingWidgetMissingOrDifferentType(
|
||||
existingWidget?.type,
|
||||
@@ -202,7 +202,7 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => {
|
||||
|
||||
setPageLayoutEditingWidgetId(newRecordTableWidget.id);
|
||||
|
||||
await createViewForRecordTableWidget(
|
||||
addDraftViewForRecordTableWidget(
|
||||
newRecordTableWidget.id,
|
||||
firstAvailableObjectMetadataItem,
|
||||
);
|
||||
|
||||
+10
-16
@@ -3,8 +3,8 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
||||
import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { useCreateViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget';
|
||||
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
|
||||
import { useAddDraftViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useAddDraftViewForRecordTableWidget';
|
||||
import { useRemoveDraftViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useRemoveDraftViewForRecordTableWidget';
|
||||
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';
|
||||
@@ -47,11 +47,11 @@ export const RecordTableDataSourceDropdownContent = () => {
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { createViewForRecordTableWidget } =
|
||||
useCreateViewForRecordTableWidget(pageLayoutId);
|
||||
const { addDraftViewForRecordTableWidget } =
|
||||
useAddDraftViewForRecordTableWidget(pageLayoutId);
|
||||
|
||||
const { deleteViewForRecordTableWidget } =
|
||||
useDeleteViewForRecordTableWidget();
|
||||
const { removeDraftViewForRecordTableWidget } =
|
||||
useRemoveDraftViewForRecordTableWidget(pageLayoutId);
|
||||
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
|
||||
@@ -77,20 +77,14 @@ export const RecordTableDataSourceDropdownContent = () => {
|
||||
getSearchableValues: (item) => [item.labelPlural, item.namePlural],
|
||||
});
|
||||
|
||||
const currentViewId =
|
||||
widgetInEditMode?.configuration &&
|
||||
'viewId' in widgetInEditMode.configuration
|
||||
? (widgetInEditMode.configuration.viewId as string | undefined)
|
||||
: undefined;
|
||||
|
||||
const handleSelectSource = async (newObjectMetadataItemId: string) => {
|
||||
const handleSelectSource = (newObjectMetadataItemId: string) => {
|
||||
if (currentObjectMetadataItemId === newObjectMetadataItemId) {
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(currentViewId)) {
|
||||
await deleteViewForRecordTableWidget(currentViewId);
|
||||
if (isDefined(widgetInEditMode)) {
|
||||
removeDraftViewForRecordTableWidget(widgetInEditMode.id);
|
||||
}
|
||||
|
||||
updateCurrentWidgetConfig({
|
||||
@@ -105,7 +99,7 @@ export const RecordTableDataSourceDropdownContent = () => {
|
||||
);
|
||||
|
||||
if (isDefined(selectedObjectMetadataItem) && isDefined(widgetInEditMode)) {
|
||||
await createViewForRecordTableWidget(
|
||||
addDraftViewForRecordTableWidget(
|
||||
widgetInEditMode.id,
|
||||
selectedObjectMetadataItem,
|
||||
);
|
||||
|
||||
+10
-4
@@ -27,19 +27,25 @@ const StyledSectionLabel = styled.div`
|
||||
|
||||
type RecordTableSettingsFieldVisibilityProps = {
|
||||
viewId: string;
|
||||
widgetId: string;
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsFieldVisibility = ({
|
||||
viewId,
|
||||
widgetId,
|
||||
pageLayoutId,
|
||||
}: RecordTableSettingsFieldVisibilityProps) => {
|
||||
const { recordTableWidgetViewFieldItems } =
|
||||
useRecordTableWidgetViewFieldItems(viewId);
|
||||
useRecordTableWidgetViewFieldItems({ viewId, widgetId, pageLayoutId });
|
||||
|
||||
const { toggleRecordTableWidgetFieldVisibility } =
|
||||
useToggleRecordTableWidgetFieldVisibility();
|
||||
useToggleRecordTableWidgetFieldVisibility({ pageLayoutId, widgetId });
|
||||
|
||||
const { reorderRecordTableWidgetFields } =
|
||||
useReorderRecordTableWidgetFields();
|
||||
const { reorderRecordTableWidgetFields } = useReorderRecordTableWidgetFields({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
|
||||
+10
-2
@@ -3,9 +3,9 @@ import { AdvancedFilterSidePanelContainer } from '@/object-record/advanced-filte
|
||||
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
|
||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { useRecordTableWidgetViewForDisplay } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay';
|
||||
import { RecordTableSettingsFiltersInitializeStateEffect } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFiltersInitializeStateEffect';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { useViewById } from '@/views/hooks/useViewById';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -20,14 +20,22 @@ const StyledFilterSettingsContainer = styled.div`
|
||||
|
||||
type RecordTableSettingsFiltersProps = {
|
||||
viewId: string;
|
||||
widgetId: string;
|
||||
pageLayoutId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsFilters = ({
|
||||
viewId,
|
||||
widgetId,
|
||||
pageLayoutId,
|
||||
objectMetadataId,
|
||||
}: RecordTableSettingsFiltersProps) => {
|
||||
const { view } = useViewById(viewId);
|
||||
const { view } = useRecordTableWidgetViewForDisplay({
|
||||
viewId,
|
||||
widgetId,
|
||||
pageLayoutId,
|
||||
});
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
+10
-2
@@ -2,10 +2,10 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMeta
|
||||
import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems';
|
||||
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { useRecordTableWidgetViewForDisplay } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay';
|
||||
import { RecordTableSettingsSortsContent } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSortsContent';
|
||||
import { RecordTableSettingsSortsInitializeStateEffect } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSortsInitializeStateEffect';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { useViewById } from '@/views/hooks/useViewById';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -23,14 +23,22 @@ const StyledSortSettingsContainer = styled.div`
|
||||
|
||||
type RecordTableSettingsSortsProps = {
|
||||
viewId: string;
|
||||
widgetId: string;
|
||||
pageLayoutId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsSorts = ({
|
||||
viewId,
|
||||
widgetId,
|
||||
pageLayoutId,
|
||||
objectMetadataId,
|
||||
}: RecordTableSettingsSortsProps) => {
|
||||
const { view } = useViewById(viewId);
|
||||
const { view } = useRecordTableWidgetViewForDisplay({
|
||||
viewId,
|
||||
widgetId,
|
||||
pageLayoutId,
|
||||
});
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
+7
-1
@@ -28,5 +28,11 @@ export const SidePanelRecordTableFieldsSubPage = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <RecordTableSettingsFieldVisibility viewId={viewId} />;
|
||||
return (
|
||||
<RecordTableSettingsFieldVisibility
|
||||
viewId={viewId}
|
||||
widgetId={widgetInEditMode.id}
|
||||
pageLayoutId={pageLayoutId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -31,6 +31,8 @@ export const SidePanelRecordTableFilterSubPage = () => {
|
||||
return (
|
||||
<RecordTableSettingsFilters
|
||||
viewId={viewId}
|
||||
widgetId={widgetInEditMode.id}
|
||||
pageLayoutId={pageLayoutId}
|
||||
objectMetadataId={widgetInEditMode.objectMetadataId}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
@@ -32,6 +32,8 @@ export const SidePanelRecordTableSortSubPage = () => {
|
||||
return (
|
||||
<RecordTableSettingsSorts
|
||||
viewId={viewId}
|
||||
widgetId={widgetInEditMode.id}
|
||||
pageLayoutId={pageLayoutId}
|
||||
objectMetadataId={widgetInEditMode.objectMetadataId}
|
||||
/>
|
||||
);
|
||||
|
||||
+36
-14
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import path, { dirname, join } from 'path';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
@@ -10,6 +10,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import { SEED_DEPENDENCIES_DIRNAME } from 'src/engine/core-modules/application/application-package/constants/seed-dependencies-dirname';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import {
|
||||
getLogicFunctionSeedProjectFiles,
|
||||
@@ -257,24 +258,45 @@ export class LogicFunctionResourceService {
|
||||
workspaceId: string;
|
||||
inMemoryFolderPath: string;
|
||||
}) {
|
||||
const yarnLockExists = await this.fileStorageService.checkFileExists({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
resourcePath: 'yarn.lock',
|
||||
});
|
||||
|
||||
const promises = [];
|
||||
|
||||
promises.push(
|
||||
this.fileStorageService.downloadFile({
|
||||
const [packageJsonExists, yarnLockExists] = await Promise.all([
|
||||
this.fileStorageService.checkFileExists({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
resourcePath: 'package.json',
|
||||
localPath: join(inMemoryFolderPath, 'package.json'),
|
||||
}),
|
||||
);
|
||||
this.fileStorageService.checkFileExists({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
resourcePath: 'yarn.lock',
|
||||
}),
|
||||
]);
|
||||
|
||||
const promises = [];
|
||||
|
||||
if (packageJsonExists) {
|
||||
promises.push(
|
||||
this.fileStorageService.downloadFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
resourcePath: 'package.json',
|
||||
localPath: join(inMemoryFolderPath, 'package.json'),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const packageJsonPath = join(inMemoryFolderPath, 'package.json');
|
||||
|
||||
promises.push(
|
||||
fs.mkdir(dirname(packageJsonPath), { recursive: true }).then(() =>
|
||||
fs.copyFile(
|
||||
path.join(SEED_DEPENDENCIES_DIRNAME, 'package.json'),
|
||||
packageJsonPath,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (yarnLockExists) {
|
||||
promises.push(
|
||||
|
||||
Reference in New Issue
Block a user