add UpsertViewWidget resolver (#20053)
This commit is contained in:
@@ -3193,6 +3193,7 @@ type Mutation {
|
||||
updateView(id: String!, input: UpdateViewInput!): View!
|
||||
deleteView(id: String!): Boolean!
|
||||
destroyView(id: String!): Boolean!
|
||||
upsertViewWidget(input: UpsertViewWidgetInput!): View!
|
||||
createViewSort(input: CreateViewSortInput!): ViewSort!
|
||||
updateViewSort(input: UpdateViewSortInput!): ViewSort!
|
||||
deleteViewSort(input: DeleteViewSortInput!): Boolean!
|
||||
@@ -3510,6 +3511,59 @@ input UpdateViewInput {
|
||||
shouldHideEmptyGroups: Boolean
|
||||
}
|
||||
|
||||
input UpsertViewWidgetInput {
|
||||
"""The id of the view widget (page layout widget)."""
|
||||
widgetId: UUID!
|
||||
|
||||
"""The view fields to upsert."""
|
||||
viewFields: [UpsertViewWidgetViewFieldInput!]
|
||||
|
||||
"""The view filters to upsert."""
|
||||
viewFilters: [UpsertViewWidgetViewFilterInput!]
|
||||
|
||||
"""The view filter groups to upsert."""
|
||||
viewFilterGroups: [UpsertViewWidgetViewFilterGroupInput!]
|
||||
|
||||
"""The view sorts to upsert."""
|
||||
viewSorts: [UpsertViewWidgetViewSortInput!]
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFieldInput {
|
||||
"""The id of an existing view field to update."""
|
||||
viewFieldId: UUID
|
||||
|
||||
"""
|
||||
The field metadata id. Used to create a new view field when viewFieldId is not provided.
|
||||
"""
|
||||
fieldMetadataId: UUID
|
||||
isVisible: Boolean!
|
||||
position: Float!
|
||||
size: Float
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFilterInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
operand: ViewFilterOperand = CONTAINS
|
||||
value: JSON!
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFilterGroupInput {
|
||||
id: UUID
|
||||
parentViewFilterGroupId: UUID
|
||||
logicalOperator: ViewFilterGroupLogicalOperator = AND
|
||||
positionInViewFilterGroup: Float
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewSortInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
direction: ViewSortDirection = ASC
|
||||
}
|
||||
|
||||
input CreateViewSortInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
|
||||
@@ -2675,6 +2675,7 @@ export interface Mutation {
|
||||
updateView: View
|
||||
deleteView: Scalars['Boolean']
|
||||
destroyView: Scalars['Boolean']
|
||||
upsertViewWidget: View
|
||||
createViewSort: ViewSort
|
||||
updateViewSort: ViewSort
|
||||
deleteViewSort: Scalars['Boolean']
|
||||
@@ -5725,6 +5726,7 @@ export interface MutationGenqlSelection{
|
||||
updateView?: (ViewGenqlSelection & { __args: {id: Scalars['String'], input: UpdateViewInput} })
|
||||
deleteView?: { __args: {id: Scalars['String']} }
|
||||
destroyView?: { __args: {id: Scalars['String']} }
|
||||
upsertViewWidget?: (ViewGenqlSelection & { __args: {input: UpsertViewWidgetInput} })
|
||||
createViewSort?: (ViewSortGenqlSelection & { __args: {input: CreateViewSortInput} })
|
||||
updateViewSort?: (ViewSortGenqlSelection & { __args: {input: UpdateViewSortInput} })
|
||||
deleteViewSort?: { __args: {input: DeleteViewSortInput} }
|
||||
@@ -5944,6 +5946,30 @@ export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['S
|
||||
|
||||
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetInput {
|
||||
/** The id of the view widget (page layout widget). */
|
||||
widgetId: Scalars['UUID'],
|
||||
/** The view fields to upsert. */
|
||||
viewFields?: (UpsertViewWidgetViewFieldInput[] | null),
|
||||
/** The view filters to upsert. */
|
||||
viewFilters?: (UpsertViewWidgetViewFilterInput[] | null),
|
||||
/** The view filter groups to upsert. */
|
||||
viewFilterGroups?: (UpsertViewWidgetViewFilterGroupInput[] | null),
|
||||
/** The view sorts to upsert. */
|
||||
viewSorts?: (UpsertViewWidgetViewSortInput[] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFieldInput {
|
||||
/** The id of an existing view field to update. */
|
||||
viewFieldId?: (Scalars['UUID'] | null),
|
||||
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
|
||||
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null)}
|
||||
|
||||
export interface CreateViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null),viewId: Scalars['UUID']}
|
||||
|
||||
export interface UpdateViewSortInput {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+11
-3
@@ -1,6 +1,7 @@
|
||||
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 { computeRecordTableWidgetViewLoadContentSignature } from '@/object-record/record-table-widget/utils/computeRecordTableWidgetViewLoadContentSignature';
|
||||
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';
|
||||
@@ -60,11 +61,17 @@ export const RecordTableWidgetViewLoadEffect = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
const contentSignature =
|
||||
computeRecordTableWidgetViewLoadContentSignature(currentView);
|
||||
|
||||
const lastLoadedMatches =
|
||||
viewId === lastLoadedRecordTableWidgetViewId?.viewId &&
|
||||
objectMetadataItem.updatedAt ===
|
||||
lastLoadedRecordTableWidgetViewId?.objectMetadataItemUpdatedAt
|
||||
) {
|
||||
lastLoadedRecordTableWidgetViewId?.objectMetadataItemUpdatedAt &&
|
||||
contentSignature ===
|
||||
lastLoadedRecordTableWidgetViewId?.loadedViewContentSignature;
|
||||
|
||||
if (lastLoadedMatches) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,6 +80,7 @@ export const RecordTableWidgetViewLoadEffect = ({
|
||||
setLastLoadedRecordTableWidgetViewId({
|
||||
viewId,
|
||||
objectMetadataItemUpdatedAt: objectMetadataItem.updatedAt,
|
||||
loadedViewContentSignature: contentSignature,
|
||||
});
|
||||
}, [
|
||||
viewId,
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ export const lastLoadedRecordTableWidgetViewIdComponentState =
|
||||
createAtomComponentState<{
|
||||
viewId: string;
|
||||
objectMetadataItemUpdatedAt: string;
|
||||
loadedViewContentSignature: string;
|
||||
} | null>({
|
||||
key: 'lastLoadedRecordTableWidgetViewIdComponentState',
|
||||
defaultValue: null,
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { type View } from '@/views/types/View';
|
||||
|
||||
export const computeRecordTableWidgetViewLoadContentSignature = (
|
||||
view: View,
|
||||
): string =>
|
||||
JSON.stringify({
|
||||
fields: [...view.viewFields]
|
||||
.sort((fieldA, fieldB) => fieldA.position - fieldB.position)
|
||||
.map((field) => ({
|
||||
id: field.id,
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
isVisible: field.isVisible,
|
||||
position: field.position,
|
||||
})),
|
||||
filters: [...view.viewFilters]
|
||||
.sort((filterA, filterB) => filterA.id.localeCompare(filterB.id))
|
||||
.map((filter) => ({
|
||||
id: filter.id,
|
||||
fieldMetadataId: filter.fieldMetadataId,
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
viewFilterGroupId: filter.viewFilterGroupId,
|
||||
})),
|
||||
filterGroups: (view.viewFilterGroups ?? []).map((group) => ({
|
||||
id: group.id,
|
||||
logicalOperator: group.logicalOperator,
|
||||
parentViewFilterGroupId: group.parentViewFilterGroupId,
|
||||
})),
|
||||
sorts: view.viewSorts.map((sort) => ({
|
||||
id: sort.id,
|
||||
fieldMetadataId: sort.fieldMetadataId,
|
||||
direction: sort.direction,
|
||||
})),
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPSERT_VIEW_WIDGET = gql`
|
||||
${VIEW_FRAGMENT}
|
||||
mutation UpsertViewWidget($input: UpsertViewWidgetInput!) {
|
||||
upsertViewWidget(input: $input) {
|
||||
...ViewFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
+1
-29
@@ -1,10 +1,8 @@
|
||||
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';
|
||||
@@ -13,7 +11,6 @@ import { WidgetType } from '~/generated-metadata/graphql';
|
||||
export const useCreatePendingRecordTableWidgetViews = () => {
|
||||
const { performViewAPICreate, performViewAPIDestroy } =
|
||||
usePerformViewAPIPersist();
|
||||
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
|
||||
const store = useStore();
|
||||
|
||||
const createPendingRecordTableWidgetViews = useCallback(
|
||||
@@ -101,19 +98,6 @@ export const useCreatePendingRecordTableWidgetViews = () => {
|
||||
`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) {
|
||||
@@ -121,20 +105,8 @@ export const useCreatePendingRecordTableWidgetViews = () => {
|
||||
await performViewAPIDestroy({ id: viewId });
|
||||
}
|
||||
}
|
||||
|
||||
store.set(
|
||||
recordTableWidgetViewPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
recordTableWidgetViewDraft,
|
||||
);
|
||||
},
|
||||
[
|
||||
performViewAPICreate,
|
||||
performViewAPIDestroy,
|
||||
performViewFieldAPICreate,
|
||||
store,
|
||||
],
|
||||
[performViewAPICreate, performViewAPIDestroy, store],
|
||||
);
|
||||
|
||||
return { createPendingRecordTableWidgetViews };
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useHasRecordTableWidgetViewChanges = () => {
|
||||
const store = useStore();
|
||||
|
||||
const hasRecordTableWidgetViewChanges = useCallback(
|
||||
(pageLayoutId: string): boolean => {
|
||||
const recordTableWidgetViewDraft = store.get(
|
||||
recordTableWidgetViewDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const recordTableWidgetViewPersisted = store.get(
|
||||
recordTableWidgetViewPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
return !isDeeplyEqual(
|
||||
recordTableWidgetViewDraft,
|
||||
recordTableWidgetViewPersisted,
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { hasRecordTableWidgetViewChanges };
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useHasRecordTableWidgets = () => {
|
||||
const store = useStore();
|
||||
|
||||
const hasRecordTableWidgets = useCallback(
|
||||
(pageLayoutId: string): boolean => {
|
||||
const pageLayoutDraft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
return pageLayoutDraft.tabs.some((tab) =>
|
||||
tab.widgets.some(
|
||||
(widget) =>
|
||||
widget.configuration.configurationType ===
|
||||
WidgetConfigurationType.RECORD_TABLE,
|
||||
),
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { hasRecordTableWidgets };
|
||||
};
|
||||
+23
@@ -9,6 +9,8 @@ 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 { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
@@ -83,6 +85,17 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ({
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const recordTableWidgetViewPersistedState =
|
||||
useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewPersistedComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const resetDraftPageLayoutToPersistedPageLayout = useCallback(() => {
|
||||
const pageLayoutPersisted = store.get(pageLayoutPersistedState);
|
||||
|
||||
@@ -132,6 +145,14 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ({
|
||||
fieldsWidgetEditorModeDraftState,
|
||||
fieldsWidgetEditorModePersisted,
|
||||
);
|
||||
|
||||
const recordTableWidgetViewPersisted = store.get(
|
||||
recordTableWidgetViewPersistedState,
|
||||
);
|
||||
store.set(
|
||||
recordTableWidgetViewDraftState,
|
||||
recordTableWidgetViewPersisted,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
pageLayoutDraftState,
|
||||
@@ -143,6 +164,8 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ({
|
||||
fieldsWidgetUngroupedFieldsPersistedState,
|
||||
fieldsWidgetEditorModeDraftState,
|
||||
fieldsWidgetEditorModePersistedState,
|
||||
recordTableWidgetViewDraftState,
|
||||
recordTableWidgetViewPersistedState,
|
||||
activeTabId,
|
||||
store,
|
||||
]);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useHasFieldsWidgetChanges } from '@/page-layout/hooks/useHasFieldsWidgetChanges';
|
||||
import { useHasRecordTableWidgets } from '@/page-layout/hooks/useHasRecordTableWidgets';
|
||||
import { useHasRecordTableWidgetViewChanges } from '@/page-layout/hooks/useHasRecordTableWidgetViewChanges';
|
||||
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
|
||||
import { useSaveRecordTableWidgetsViewDataOnDashboardSave } from '@/page-layout/widgets/record-table/hooks/useSaveRecordTableWidgetsViewDataOnDashboardSave';
|
||||
import { useSaveRecordTableWidgetViews } from '@/page-layout/hooks/useSaveRecordTableWidgetViews';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useSavePageLayoutWidgetsData = () => {
|
||||
const { hasFieldsWidgetChanges } = useHasFieldsWidgetChanges();
|
||||
|
||||
const { hasRecordTableWidgets } = useHasRecordTableWidgets();
|
||||
const { hasRecordTableWidgetViewChanges } =
|
||||
useHasRecordTableWidgetViewChanges();
|
||||
|
||||
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
|
||||
|
||||
const { saveRecordTableWidgetsViewDataOnDashboardSave } =
|
||||
useSaveRecordTableWidgetsViewDataOnDashboardSave();
|
||||
const { saveRecordTableWidgetViews } = useSaveRecordTableWidgetViews();
|
||||
|
||||
const savePageLayoutWidgetsData = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
@@ -20,15 +20,15 @@ export const useSavePageLayoutWidgetsData = () => {
|
||||
await saveFieldsWidgetGroups(pageLayoutId);
|
||||
}
|
||||
|
||||
if (hasRecordTableWidgets(pageLayoutId)) {
|
||||
await saveRecordTableWidgetsViewDataOnDashboardSave(pageLayoutId);
|
||||
if (hasRecordTableWidgetViewChanges(pageLayoutId)) {
|
||||
await saveRecordTableWidgetViews(pageLayoutId);
|
||||
}
|
||||
},
|
||||
[
|
||||
hasFieldsWidgetChanges,
|
||||
hasRecordTableWidgetViewChanges,
|
||||
saveFieldsWidgetGroups,
|
||||
saveRecordTableWidgetsViewDataOnDashboardSave,
|
||||
hasRecordTableWidgets,
|
||||
saveRecordTableWidgetViews,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { UPSERT_VIEW_WIDGET } from '@/page-layout/graphql/mutations/upsertViewWidget';
|
||||
import { useHasRecordTableWidgetViewChanges } from '@/page-layout/hooks/useHasRecordTableWidgetViewChanges';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type UpsertViewWidgetInput,
|
||||
type ViewFragmentFragment,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useSaveRecordTableWidgetViews = () => {
|
||||
const [upsertViewWidgetMutation] = useMutation<
|
||||
{ upsertViewWidget: ViewFragmentFragment },
|
||||
{ input: UpsertViewWidgetInput }
|
||||
>(UPSERT_VIEW_WIDGET);
|
||||
|
||||
const { hasRecordTableWidgetViewChanges } =
|
||||
useHasRecordTableWidgetViewChanges();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetViews = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
if (!hasRecordTableWidgetViewChanges(pageLayoutId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const recordTableWidgetViewDraft = store.get(
|
||||
recordTableWidgetViewDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const draftRecordTableWidgets = draft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.filter((widget) => widget.type === WidgetType.RECORD_TABLE);
|
||||
|
||||
for (const widget of draftRecordTableWidgets) {
|
||||
const viewId = getWidgetConfigurationViewId(widget.configuration);
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const widgetViewDraft = recordTableWidgetViewDraft[widget.id];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await upsertViewWidgetMutation({
|
||||
variables: {
|
||||
input: {
|
||||
widgetId: widget.id,
|
||||
viewFields: widgetViewDraft.viewFields.map((field) => ({
|
||||
...(isDefined(field.id) ? { viewFieldId: field.id } : {}),
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
isVisible: field.isVisible,
|
||||
position: field.position,
|
||||
size: field.size,
|
||||
})),
|
||||
viewFilters: widgetViewDraft.viewFilters.map((filter) => ({
|
||||
id: filter.id,
|
||||
fieldMetadataId: filter.fieldMetadataId,
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
viewFilterGroupId: filter.viewFilterGroupId ?? undefined,
|
||||
positionInViewFilterGroup:
|
||||
filter.positionInViewFilterGroup ?? undefined,
|
||||
subFieldName: filter.subFieldName ?? undefined,
|
||||
})),
|
||||
viewFilterGroups: widgetViewDraft.viewFilterGroups.map(
|
||||
(group) => ({
|
||||
id: group.id,
|
||||
parentViewFilterGroupId:
|
||||
group.parentViewFilterGroupId ?? undefined,
|
||||
logicalOperator: group.logicalOperator,
|
||||
positionInViewFilterGroup:
|
||||
group.positionInViewFilterGroup ?? undefined,
|
||||
}),
|
||||
),
|
||||
viewSorts: widgetViewDraft.viewSorts.map((sort) => ({
|
||||
id: sort.id,
|
||||
fieldMetadataId: sort.fieldMetadataId,
|
||||
direction: sort.direction,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
store.set(
|
||||
recordTableWidgetViewPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
recordTableWidgetViewDraft,
|
||||
);
|
||||
},
|
||||
[hasRecordTableWidgetViewChanges, store, upsertViewWidgetMutation],
|
||||
);
|
||||
|
||||
return { saveRecordTableWidgetViews };
|
||||
};
|
||||
+7
-1
@@ -70,7 +70,13 @@ export const useAddDraftViewForRecordTableWidget = (pageLayoutId: string) => {
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: { view: flatView, viewFields: flatViewFields },
|
||||
[widgetId]: {
|
||||
view: flatView,
|
||||
viewFields: flatViewFields,
|
||||
viewFilters: [],
|
||||
viewFilterGroups: [],
|
||||
viewSorts: [],
|
||||
},
|
||||
}));
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
+31
-1
@@ -1,4 +1,7 @@
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter';
|
||||
import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup';
|
||||
import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort';
|
||||
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';
|
||||
@@ -38,16 +41,43 @@ export const useInitializeRecordTableWidgetViewDraft = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewFields, ...viewProps } = view;
|
||||
const {
|
||||
viewFields,
|
||||
viewFilters,
|
||||
viewFilterGroups,
|
||||
viewSorts,
|
||||
viewGroups: _viewGroups,
|
||||
...viewProps
|
||||
} = view;
|
||||
|
||||
const flatViewFields: FlatViewField[] = viewFields.map((field) => ({
|
||||
...field,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewFilters: FlatViewFilter[] = viewFilters.map((filter) => ({
|
||||
...filter,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewFilterGroups: FlatViewFilterGroup[] = (
|
||||
viewFilterGroups ?? []
|
||||
).map((group) => ({
|
||||
...group,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewSorts: FlatViewSort[] = viewSorts.map((sort) => ({
|
||||
...sort,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const snapshot: RecordTableWidgetViewSnapshot = {
|
||||
view: viewProps,
|
||||
viewFields: flatViewFields,
|
||||
viewFilters: flatViewFilters,
|
||||
viewFilterGroups: flatViewFilterGroups,
|
||||
viewSorts: flatViewSorts,
|
||||
};
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => ({
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
||||
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useMapRecordFieldToViewFieldWithCurrentAggregateOperation = () => {
|
||||
const store = useStore();
|
||||
|
||||
const mapRecordFieldToViewFieldWithCurrentAggregateOperation = useCallback(
|
||||
(recordField: RecordField): Omit<ViewField, 'definition'> => {
|
||||
const viewField = mapRecordFieldToViewField(recordField);
|
||||
|
||||
const extendedAggregateOperation = store.get(
|
||||
viewFieldAggregateOperationState.atomFamily({
|
||||
viewFieldId: recordField.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...viewField,
|
||||
aggregateOperation: isDefined(extendedAggregateOperation)
|
||||
? convertExtendedAggregateOperationToAggregateOperation(
|
||||
extendedAggregateOperation,
|
||||
)
|
||||
: viewField.aggregateOperation,
|
||||
};
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { mapRecordFieldToViewFieldWithCurrentAggregateOperation };
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseRecordTableWidgetFieldCallbacksParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
viewId: string;
|
||||
};
|
||||
|
||||
export const useRecordTableWidgetFieldCallbacks = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
viewId,
|
||||
}: UseRecordTableWidgetFieldCallbacksParams) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleFieldUpdated = (
|
||||
viewFieldId: string,
|
||||
update: Partial<{ position: number; 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.clientRecordFieldId === viewFieldId
|
||||
? { ...field, ...update }
|
||||
: field,
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleFieldCreated = (recordField: RecordField) => {
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const widgetViewDraft = prev[widgetId];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFields: [
|
||||
...widgetViewDraft.viewFields,
|
||||
{
|
||||
fieldMetadataId: recordField.fieldMetadataItemId,
|
||||
position: recordField.position,
|
||||
isVisible: recordField.isVisible,
|
||||
size: recordField.size,
|
||||
isActive: true,
|
||||
viewId,
|
||||
clientRecordFieldId: recordField.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return { handleFieldUpdated, handleFieldCreated };
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseRecordTableWidgetFilterCallbacksParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
viewId: string;
|
||||
recordIndexId: string;
|
||||
};
|
||||
|
||||
export const useRecordTableWidgetFilterCallbacks = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
viewId,
|
||||
recordIndexId,
|
||||
}: UseRecordTableWidgetFilterCallbacksParams) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const currentRecordFiltersState = useAtomComponentStateCallbackState(
|
||||
currentRecordFiltersComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const currentRecordFilterGroupsState = useAtomComponentStateCallbackState(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleFilterUpdate = () => {
|
||||
const currentRecordFilters = store.get(currentRecordFiltersState);
|
||||
const currentRecordFilterGroups = store.get(currentRecordFilterGroupsState);
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const widgetViewDraft = prev[widgetId];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFilters: currentRecordFilters.map((recordFilter) => ({
|
||||
id: recordFilter.id,
|
||||
fieldMetadataId: recordFilter.fieldMetadataId,
|
||||
operand: recordFilter.operand,
|
||||
value: recordFilter.value,
|
||||
viewId,
|
||||
viewFilterGroupId: recordFilter.recordFilterGroupId ?? null,
|
||||
positionInViewFilterGroup:
|
||||
recordFilter.positionInRecordFilterGroup ?? null,
|
||||
subFieldName: recordFilter.subFieldName ?? null,
|
||||
})),
|
||||
viewFilterGroups: currentRecordFilterGroups.map((recordFilterGroup) =>
|
||||
mapRecordFilterGroupToViewFilterGroup({
|
||||
recordFilterGroup,
|
||||
view: { id: viewId },
|
||||
}),
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return { handleFilterUpdate };
|
||||
};
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseRecordTableWidgetSortCallbacksParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
viewId: string;
|
||||
recordIndexId: string;
|
||||
};
|
||||
|
||||
export const useRecordTableWidgetSortCallbacks = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
viewId,
|
||||
recordIndexId,
|
||||
}: UseRecordTableWidgetSortCallbacksParams) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const currentRecordSortsState = useAtomComponentStateCallbackState(
|
||||
currentRecordSortsComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleSortUpdate = () => {
|
||||
const currentRecordSorts = store.get(currentRecordSortsState);
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const widgetViewDraft = prev[widgetId];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewSorts: currentRecordSorts.map((recordSort) => ({
|
||||
id: recordSort.id,
|
||||
fieldMetadataId: recordSort.fieldMetadataId,
|
||||
direction: recordSort.direction,
|
||||
viewId,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return { handleSortUpdate };
|
||||
};
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
|
||||
import { getRecordTableWidgetDraftViewFieldClientId } from '@/page-layout/widgets/record-table/utils/getRecordTableWidgetDraftViewFieldClientId';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -54,7 +55,10 @@ export const useReorderRecordTableWidgetFields = ({
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFields: widgetViewDraft.viewFields.map((field) => {
|
||||
const newPosition = updatedPositions.get(field.id);
|
||||
const newPosition = updatedPositions.get(
|
||||
getRecordTableWidgetDraftViewFieldClientId(field),
|
||||
);
|
||||
|
||||
return newPosition !== undefined
|
||||
? { ...field, position: newPosition }
|
||||
: field;
|
||||
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
|
||||
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
|
||||
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
|
||||
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
|
||||
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
|
||||
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
|
||||
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
|
||||
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
|
||||
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSaveRecordTableWidgetFiltersToView = (
|
||||
viewId: string,
|
||||
instanceId: string,
|
||||
) => {
|
||||
const {
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
} = usePerformViewFilterAPIPersist();
|
||||
|
||||
const {
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDestroy,
|
||||
} = usePerformViewFilterGroupAPIPersist();
|
||||
|
||||
const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState(
|
||||
currentRecordFiltersComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const currentRecordFilterGroupsCallbackState =
|
||||
useAtomComponentStateCallbackState(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetFiltersToView = useCallback(async () => {
|
||||
const views = store.get(viewsSelector.atom);
|
||||
const currentView = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!isDefined(currentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRecordFilters = store.get(currentRecordFiltersCallbackState);
|
||||
const currentRecordFilterGroups = store.get(
|
||||
currentRecordFilterGroupsCallbackState,
|
||||
);
|
||||
|
||||
const newViewFilterGroups = currentRecordFilterGroups.map(
|
||||
(recordFilterGroup) =>
|
||||
mapRecordFilterGroupToViewFilterGroup({
|
||||
recordFilterGroup,
|
||||
view: currentView,
|
||||
}),
|
||||
);
|
||||
|
||||
const currentViewFilterGroups = currentView.viewFilterGroups ?? [];
|
||||
|
||||
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
|
||||
await performViewFilterGroupAPICreate(
|
||||
viewFilterGroupsToCreate,
|
||||
currentView,
|
||||
);
|
||||
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
|
||||
|
||||
const newViewFilters = currentRecordFilters.map(
|
||||
mapRecordFilterToViewFilter,
|
||||
);
|
||||
|
||||
const currentViewFilters = currentView.viewFilters ?? [];
|
||||
|
||||
const viewFiltersToCreate = getViewFiltersToCreate(
|
||||
currentViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToUpdate = getViewFiltersToUpdate(
|
||||
currentViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToDelete = getViewFiltersToDelete(
|
||||
currentViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
|
||||
await performViewFilterAPIDelete(
|
||||
viewFiltersToDelete.map((viewFilter) => ({
|
||||
input: { id: viewFilter.id },
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewFilterAPIUpdate(
|
||||
viewFiltersToUpdate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
update: {
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewFilterAPICreate(
|
||||
viewFiltersToCreate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewFilterGroupAPIDestroy(
|
||||
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
|
||||
);
|
||||
}, [
|
||||
store,
|
||||
viewId,
|
||||
currentRecordFiltersCallbackState,
|
||||
currentRecordFilterGroupsCallbackState,
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDestroy,
|
||||
]);
|
||||
|
||||
return { saveRecordTableWidgetFiltersToView };
|
||||
};
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate';
|
||||
import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete';
|
||||
import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate';
|
||||
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSaveRecordTableWidgetSortsToView = (viewId: string) => {
|
||||
const {
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
} = usePerformViewSortAPIPersist();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetSortsToView = useCallback(
|
||||
async (recordSorts: RecordSort[]) => {
|
||||
const views = store.get(viewsSelector.atom);
|
||||
const currentView = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!isDefined(currentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentViewSorts = currentView.viewSorts ?? [];
|
||||
const newViewSorts = recordSorts.map(mapRecordSortToViewSort);
|
||||
|
||||
const viewSortsToCreate = getViewSortsToCreate(
|
||||
currentViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToUpdate = getViewSortsToUpdate(
|
||||
currentViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToDelete = getViewSortsToDelete(
|
||||
currentViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
|
||||
await performViewSortAPICreate(
|
||||
viewSortsToCreate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
fieldMetadataId: viewSort.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewSortAPIUpdate(
|
||||
viewSortsToUpdate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
update: {
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewSortAPIDelete(
|
||||
viewSortsToDelete.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
},
|
||||
})),
|
||||
);
|
||||
},
|
||||
[
|
||||
store,
|
||||
viewId,
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
],
|
||||
);
|
||||
|
||||
return { saveRecordTableWidgetSortsToView };
|
||||
};
|
||||
-319
@@ -1,319 +0,0 @@
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
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';
|
||||
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
|
||||
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
|
||||
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
|
||||
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
|
||||
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
|
||||
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
|
||||
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
|
||||
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
|
||||
import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate';
|
||||
import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete';
|
||||
import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate';
|
||||
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
|
||||
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
|
||||
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
|
||||
const {
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
} = usePerformViewFilterAPIPersist();
|
||||
|
||||
const {
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDestroy,
|
||||
} = usePerformViewFilterGroupAPIPersist();
|
||||
|
||||
const {
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
} = usePerformViewSortAPIPersist();
|
||||
|
||||
const { performViewFieldAPICreate, performViewFieldAPIUpdate } =
|
||||
usePerformViewFieldAPIPersist();
|
||||
|
||||
const { mapRecordFieldToViewFieldWithCurrentAggregateOperation } =
|
||||
useMapRecordFieldToViewFieldWithCurrentAggregateOperation();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetsViewDataOnDashboardSave = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
const pageLayoutDraft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
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(
|
||||
(widget) =>
|
||||
widget.configuration.configurationType ===
|
||||
WidgetConfigurationType.RECORD_TABLE,
|
||||
),
|
||||
);
|
||||
|
||||
for (const widget of recordTableWidgets) {
|
||||
const configuration = widget.configuration;
|
||||
|
||||
if (!('viewId' in configuration) || !isDefined(configuration.viewId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewId = configuration.viewId as string;
|
||||
const currentView = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!isDefined(currentView) || !isDefined(widget.objectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widget.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
viewId,
|
||||
);
|
||||
|
||||
const currentRecordFilters = store.get(
|
||||
currentRecordFiltersComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
);
|
||||
|
||||
const currentRecordFilterGroups = store.get(
|
||||
currentRecordFilterGroupsComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
);
|
||||
|
||||
const currentRecordSorts = store.get(
|
||||
currentRecordSortsComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
);
|
||||
|
||||
const newViewFilterGroups = currentRecordFilterGroups.map(
|
||||
(recordFilterGroup) =>
|
||||
mapRecordFilterGroupToViewFilterGroup({
|
||||
recordFilterGroup,
|
||||
view: currentView,
|
||||
}),
|
||||
);
|
||||
|
||||
const existingViewFilterGroups = currentView.viewFilterGroups ?? [];
|
||||
|
||||
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
|
||||
existingViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
|
||||
existingViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
|
||||
existingViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
|
||||
const newViewFilters = currentRecordFilters.map(
|
||||
mapRecordFilterToViewFilter,
|
||||
);
|
||||
const existingViewFilters = currentView.viewFilters ?? [];
|
||||
|
||||
const viewFiltersToCreate = getViewFiltersToCreate(
|
||||
existingViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToUpdate = getViewFiltersToUpdate(
|
||||
existingViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToDelete = getViewFiltersToDelete(
|
||||
existingViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
|
||||
const newViewSorts = currentRecordSorts.map(mapRecordSortToViewSort);
|
||||
const existingViewSorts = currentView.viewSorts ?? [];
|
||||
|
||||
const viewSortsToCreate = getViewSortsToCreate(
|
||||
existingViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToUpdate = getViewSortsToUpdate(
|
||||
existingViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToDelete = getViewSortsToDelete(
|
||||
existingViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
|
||||
const currentRecordFields = store.get(
|
||||
currentRecordFieldsComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
);
|
||||
|
||||
const recordIndexViewFields = currentRecordFields.map(
|
||||
mapRecordFieldToViewFieldWithCurrentAggregateOperation,
|
||||
);
|
||||
|
||||
const draftSnapshot = recordTableWidgetViewDraft[widget.id];
|
||||
const draftViewFields = draftSnapshot?.viewFields ?? [];
|
||||
const metadataStoreViewFields =
|
||||
draftViewFields.length > 0
|
||||
? draftViewFields
|
||||
: (currentView.viewFields ?? []);
|
||||
|
||||
const { viewFieldsToCreate, viewFieldsToUpdate } =
|
||||
computeViewFieldsToCreateAndUpdate({
|
||||
newViewFields: metadataStoreViewFields,
|
||||
existingViewFields: recordIndexViewFields,
|
||||
viewId,
|
||||
});
|
||||
|
||||
const hasNoRecordTableWidgetChanges =
|
||||
viewFilterGroupsToCreate.length === 0 &&
|
||||
viewFilterGroupsToUpdate.length === 0 &&
|
||||
viewFilterGroupsToDelete.length === 0 &&
|
||||
viewFiltersToCreate.length === 0 &&
|
||||
viewFiltersToUpdate.length === 0 &&
|
||||
viewFiltersToDelete.length === 0 &&
|
||||
viewSortsToCreate.length === 0 &&
|
||||
viewSortsToUpdate.length === 0 &&
|
||||
viewSortsToDelete.length === 0 &&
|
||||
viewFieldsToCreate.length === 0 &&
|
||||
viewFieldsToUpdate.length === 0;
|
||||
|
||||
if (hasNoRecordTableWidgetChanges) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await performViewFilterGroupAPICreate(
|
||||
viewFilterGroupsToCreate,
|
||||
currentView,
|
||||
);
|
||||
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
|
||||
|
||||
await performViewFilterAPIDelete(
|
||||
viewFiltersToDelete.map((viewFilter) => ({
|
||||
input: { id: viewFilter.id },
|
||||
})),
|
||||
);
|
||||
await performViewFilterAPIUpdate(
|
||||
viewFiltersToUpdate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
update: {
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
await performViewFilterAPICreate(
|
||||
viewFiltersToCreate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
})),
|
||||
);
|
||||
await performViewFilterGroupAPIDestroy(
|
||||
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
|
||||
);
|
||||
|
||||
await performViewSortAPICreate(
|
||||
viewSortsToCreate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
fieldMetadataId: viewSort.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
})),
|
||||
);
|
||||
await performViewSortAPIUpdate(
|
||||
viewSortsToUpdate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
update: {
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
await performViewSortAPIDelete(
|
||||
viewSortsToDelete.map((viewSort) => ({
|
||||
input: { id: viewSort.id },
|
||||
})),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
performViewFieldAPICreate({ inputs: viewFieldsToCreate }),
|
||||
performViewFieldAPIUpdate(viewFieldsToUpdate),
|
||||
]);
|
||||
}
|
||||
},
|
||||
[
|
||||
store,
|
||||
mapRecordFieldToViewFieldWithCurrentAggregateOperation,
|
||||
performViewFieldAPICreate,
|
||||
performViewFieldAPIUpdate,
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDestroy,
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
],
|
||||
);
|
||||
|
||||
return { saveRecordTableWidgetsViewDataOnDashboardSave };
|
||||
};
|
||||
+4
-1
@@ -34,7 +34,10 @@ export const useToggleRecordTableWidgetFieldVisibility = ({
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFields: widgetViewDraft.viewFields.map((field) =>
|
||||
field.id === viewFieldId ? { ...field, isVisible } : field,
|
||||
field.id === viewFieldId ||
|
||||
field.clientRecordFieldId === viewFieldId
|
||||
? { ...field, isVisible }
|
||||
: field,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
+12
-1
@@ -1,7 +1,18 @@
|
||||
import { type FlatView } from '@/metadata-store/types/FlatView';
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter';
|
||||
import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup';
|
||||
import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort';
|
||||
|
||||
export type RecordTableWidgetDraftViewField = Omit<FlatViewField, 'id'> & {
|
||||
id?: string;
|
||||
clientRecordFieldId?: string;
|
||||
};
|
||||
|
||||
export type RecordTableWidgetViewSnapshot = {
|
||||
view: FlatView;
|
||||
viewFields: FlatViewField[];
|
||||
viewFields: RecordTableWidgetDraftViewField[];
|
||||
viewFilters: FlatViewFilter[];
|
||||
viewFilterGroups: FlatViewFilterGroup[];
|
||||
viewSorts: FlatViewSort[];
|
||||
};
|
||||
|
||||
-208
@@ -1,208 +0,0 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { computeViewFieldsToCreateAndUpdate } from '@/page-layout/widgets/record-table/utils/computeViewFieldsToCreateAndUpdate';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
|
||||
const buildViewField = (
|
||||
overrides: Partial<Omit<ViewField, 'definition'>>,
|
||||
): Omit<ViewField, 'definition'> => ({
|
||||
id: v4(),
|
||||
fieldMetadataId: v4(),
|
||||
position: 0,
|
||||
isActive: true,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
aggregateOperation: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const buildExistingViewField = (overrides: Partial<ViewField>): ViewField => ({
|
||||
id: v4(),
|
||||
fieldMetadataId: v4(),
|
||||
position: 0,
|
||||
isActive: true,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
aggregateOperation: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('computeViewFieldsToCreateAndUpdate', () => {
|
||||
it('adds new fields with no matching fieldMetadataId to viewFieldsToCreate', () => {
|
||||
const viewId = v4();
|
||||
const newViewField = buildViewField({
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
});
|
||||
|
||||
const result = computeViewFieldsToCreateAndUpdate({
|
||||
newViewFields: [newViewField],
|
||||
existingViewFields: [],
|
||||
viewId,
|
||||
});
|
||||
|
||||
expect(result.viewFieldsToCreate).toHaveLength(1);
|
||||
expect(result.viewFieldsToCreate[0]).toEqual({
|
||||
id: newViewField.id,
|
||||
fieldMetadataId: newViewField.fieldMetadataId,
|
||||
position: newViewField.position,
|
||||
isVisible: newViewField.isVisible,
|
||||
size: newViewField.size,
|
||||
aggregateOperation: newViewField.aggregateOperation,
|
||||
viewId,
|
||||
});
|
||||
expect(result.viewFieldsToUpdate).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('adds existing fields with changed properties to viewFieldsToUpdate', () => {
|
||||
const viewId = v4();
|
||||
const fieldMetadataId = v4();
|
||||
const existingFieldId = v4();
|
||||
|
||||
const existingViewField = buildExistingViewField({
|
||||
id: existingFieldId,
|
||||
fieldMetadataId,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
aggregateOperation: null,
|
||||
});
|
||||
|
||||
const newViewField = buildViewField({
|
||||
fieldMetadataId,
|
||||
position: 2,
|
||||
isVisible: false,
|
||||
size: 200,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
});
|
||||
|
||||
const result = computeViewFieldsToCreateAndUpdate({
|
||||
newViewFields: [newViewField],
|
||||
existingViewFields: [existingViewField],
|
||||
viewId,
|
||||
});
|
||||
|
||||
expect(result.viewFieldsToCreate).toHaveLength(0);
|
||||
expect(result.viewFieldsToUpdate).toHaveLength(1);
|
||||
expect(result.viewFieldsToUpdate[0]).toEqual({
|
||||
input: {
|
||||
id: existingFieldId,
|
||||
update: {
|
||||
isVisible: false,
|
||||
position: 2,
|
||||
size: 200,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('skips existing fields with identical properties', () => {
|
||||
const viewId = v4();
|
||||
const fieldMetadataId = v4();
|
||||
|
||||
const existingViewField = buildExistingViewField({
|
||||
fieldMetadataId,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
});
|
||||
|
||||
const newViewField = buildViewField({
|
||||
fieldMetadataId,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
});
|
||||
|
||||
const result = computeViewFieldsToCreateAndUpdate({
|
||||
newViewFields: [newViewField],
|
||||
existingViewFields: [existingViewField],
|
||||
viewId,
|
||||
});
|
||||
|
||||
expect(result.viewFieldsToCreate).toHaveLength(0);
|
||||
expect(result.viewFieldsToUpdate).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns empty arrays when newViewFields is empty', () => {
|
||||
const viewId = v4();
|
||||
|
||||
const existingViewField = buildExistingViewField({});
|
||||
|
||||
const result = computeViewFieldsToCreateAndUpdate({
|
||||
newViewFields: [],
|
||||
existingViewFields: [existingViewField],
|
||||
viewId,
|
||||
});
|
||||
|
||||
expect(result.viewFieldsToCreate).toHaveLength(0);
|
||||
expect(result.viewFieldsToUpdate).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles a mix of creates and updates in a single call', () => {
|
||||
const viewId = v4();
|
||||
const existingFieldMetadataId = v4();
|
||||
const newFieldMetadataId = v4();
|
||||
const existingFieldId = v4();
|
||||
|
||||
const existingViewField = buildExistingViewField({
|
||||
id: existingFieldId,
|
||||
fieldMetadataId: existingFieldMetadataId,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
aggregateOperation: null,
|
||||
});
|
||||
|
||||
const updatedNewViewField = buildViewField({
|
||||
fieldMetadataId: existingFieldMetadataId,
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 100,
|
||||
aggregateOperation: null,
|
||||
});
|
||||
|
||||
const brandNewViewField = buildViewField({
|
||||
fieldMetadataId: newFieldMetadataId,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 80,
|
||||
aggregateOperation: AggregateOperations.MIN,
|
||||
});
|
||||
|
||||
const result = computeViewFieldsToCreateAndUpdate({
|
||||
newViewFields: [updatedNewViewField, brandNewViewField],
|
||||
existingViewFields: [existingViewField],
|
||||
viewId,
|
||||
});
|
||||
|
||||
expect(result.viewFieldsToCreate).toHaveLength(1);
|
||||
expect(result.viewFieldsToCreate[0]).toEqual({
|
||||
id: brandNewViewField.id,
|
||||
fieldMetadataId: newFieldMetadataId,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 80,
|
||||
aggregateOperation: AggregateOperations.MIN,
|
||||
viewId,
|
||||
});
|
||||
|
||||
expect(result.viewFieldsToUpdate).toHaveLength(1);
|
||||
expect(result.viewFieldsToUpdate[0]).toEqual({
|
||||
input: {
|
||||
id: existingFieldId,
|
||||
update: {
|
||||
isVisible: false,
|
||||
position: 5,
|
||||
size: 100,
|
||||
aggregateOperation: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type CreateViewFieldInput,
|
||||
type UpdateViewFieldMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type ViewFieldsToCreateAndUpdate = {
|
||||
viewFieldsToCreate: CreateViewFieldInput[];
|
||||
viewFieldsToUpdate: UpdateViewFieldMutationVariables[];
|
||||
};
|
||||
|
||||
export const computeViewFieldsToCreateAndUpdate = ({
|
||||
newViewFields,
|
||||
existingViewFields,
|
||||
viewId,
|
||||
}: {
|
||||
newViewFields: Omit<ViewField, 'definition'>[];
|
||||
existingViewFields: ViewField[];
|
||||
viewId: string;
|
||||
}): ViewFieldsToCreateAndUpdate => {
|
||||
return newViewFields.reduce<ViewFieldsToCreateAndUpdate>(
|
||||
(accumulator, newViewField) => {
|
||||
const existingViewField = existingViewFields.find(
|
||||
(existingField) =>
|
||||
existingField.fieldMetadataId === newViewField.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(existingViewField)) {
|
||||
return {
|
||||
viewFieldsToCreate: [
|
||||
...accumulator.viewFieldsToCreate,
|
||||
{
|
||||
id: newViewField.id,
|
||||
fieldMetadataId: newViewField.fieldMetadataId,
|
||||
position: newViewField.position,
|
||||
isVisible: newViewField.isVisible,
|
||||
size: newViewField.size,
|
||||
aggregateOperation: newViewField.aggregateOperation,
|
||||
viewId,
|
||||
},
|
||||
],
|
||||
viewFieldsToUpdate: accumulator.viewFieldsToUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
isDeeplyEqual(
|
||||
{
|
||||
position: existingViewField.position,
|
||||
size: existingViewField.size,
|
||||
isVisible: existingViewField.isVisible,
|
||||
aggregateOperation: existingViewField.aggregateOperation,
|
||||
},
|
||||
{
|
||||
position: newViewField.position,
|
||||
size: newViewField.size,
|
||||
isVisible: newViewField.isVisible,
|
||||
aggregateOperation: newViewField.aggregateOperation,
|
||||
},
|
||||
)
|
||||
) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
return {
|
||||
viewFieldsToCreate: accumulator.viewFieldsToCreate,
|
||||
viewFieldsToUpdate: [
|
||||
...accumulator.viewFieldsToUpdate,
|
||||
{
|
||||
input: {
|
||||
id: existingViewField.id,
|
||||
update: {
|
||||
isVisible: newViewField.isVisible,
|
||||
position: newViewField.position,
|
||||
size: newViewField.size,
|
||||
aggregateOperation: newViewField.aggregateOperation,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
{ viewFieldsToCreate: [], viewFieldsToUpdate: [] },
|
||||
);
|
||||
};
|
||||
+13
-4
@@ -1,13 +1,22 @@
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { getRecordTableWidgetDraftViewFieldClientId } from '@/page-layout/widgets/record-table/utils/getRecordTableWidgetDraftViewFieldClientId';
|
||||
import { type View } from '@/views/types/View';
|
||||
|
||||
export const constructViewFromRecordTableWidgetViewSnapshot = (
|
||||
snapshot: RecordTableWidgetViewSnapshot,
|
||||
): View => ({
|
||||
...snapshot.view,
|
||||
viewFields: snapshot.viewFields,
|
||||
viewFilters: [],
|
||||
viewSorts: [],
|
||||
viewFields: snapshot.viewFields.map((draftField) => {
|
||||
const { clientRecordFieldId: _clientRecordFieldId, ...fieldRest } =
|
||||
draftField;
|
||||
|
||||
return {
|
||||
...fieldRest,
|
||||
id: getRecordTableWidgetDraftViewFieldClientId(draftField),
|
||||
};
|
||||
}),
|
||||
viewFilters: snapshot.viewFilters,
|
||||
viewFilterGroups: snapshot.viewFilterGroups,
|
||||
viewSorts: snapshot.viewSorts,
|
||||
viewGroups: [],
|
||||
viewFilterGroups: [],
|
||||
});
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { type RecordTableWidgetDraftViewField } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordTableWidgetDraftViewFieldIdentity = Pick<
|
||||
RecordTableWidgetDraftViewField,
|
||||
'id' | 'clientRecordFieldId'
|
||||
>;
|
||||
|
||||
export const getRecordTableWidgetDraftViewFieldClientId = (
|
||||
field: RecordTableWidgetDraftViewFieldIdentity,
|
||||
): string => {
|
||||
if (isDefined(field.id)) {
|
||||
return field.id;
|
||||
}
|
||||
|
||||
if (isDefined(field.clientRecordFieldId)) {
|
||||
return field.clientRecordFieldId;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
+10
@@ -1,5 +1,6 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
|
||||
import { useRecordTableWidgetFieldCallbacks } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetFieldCallbacks';
|
||||
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
@@ -65,6 +66,13 @@ export const SidePanelDashboardRecordTableSettings = () => {
|
||||
viewId,
|
||||
});
|
||||
|
||||
const { handleFieldUpdated, handleFieldCreated } =
|
||||
useRecordTableWidgetFieldCallbacks({
|
||||
pageLayoutId,
|
||||
widgetId: widgetInEditMode?.id ?? '',
|
||||
viewId: viewId ?? '',
|
||||
});
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
@@ -136,6 +144,8 @@ export const SidePanelDashboardRecordTableSettings = () => {
|
||||
<RecordTableFieldsDropdownContent
|
||||
viewId={viewId}
|
||||
objectMetadataId={widgetInEditMode.objectMetadataId!}
|
||||
onFieldUpdated={handleFieldUpdated}
|
||||
onFieldCreated={handleFieldCreated}
|
||||
/>
|
||||
}
|
||||
dropdownPlacement="bottom-end"
|
||||
|
||||
+11
@@ -1,4 +1,5 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { RecordTableFieldsDropdownHiddenFieldsContent } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableFieldsDropdownHiddenFieldsContent';
|
||||
@@ -8,11 +9,18 @@ import { useState } from 'react';
|
||||
type RecordTableFieldsDropdownContentProps = {
|
||||
viewId: string;
|
||||
objectMetadataId: string;
|
||||
onFieldUpdated?: (
|
||||
viewFieldId: string,
|
||||
update: Partial<{ position: number; isVisible: boolean }>,
|
||||
) => void;
|
||||
onFieldCreated?: (recordField: RecordField) => void;
|
||||
};
|
||||
|
||||
export const RecordTableFieldsDropdownContent = ({
|
||||
viewId,
|
||||
objectMetadataId,
|
||||
onFieldUpdated,
|
||||
onFieldCreated,
|
||||
}: RecordTableFieldsDropdownContentProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
@@ -34,12 +42,15 @@ export const RecordTableFieldsDropdownContent = ({
|
||||
objectMetadataId={objectMetadataId}
|
||||
recordIndexId={recordIndexId}
|
||||
onBack={() => setShowHiddenFields(false)}
|
||||
onFieldUpdated={onFieldUpdated}
|
||||
onFieldCreated={onFieldCreated}
|
||||
/>
|
||||
) : (
|
||||
<RecordTableFieldsDropdownVisibleFieldsContent
|
||||
objectMetadataId={objectMetadataId}
|
||||
recordIndexId={recordIndexId}
|
||||
onShowHiddenFields={() => setShowHiddenFields(true)}
|
||||
onFieldUpdated={onFieldUpdated}
|
||||
/>
|
||||
)}
|
||||
</RecordFieldsComponentInstanceContext.Provider>
|
||||
|
||||
+9
@@ -20,12 +20,19 @@ type RecordTableFieldsDropdownHiddenFieldsContentProps = {
|
||||
objectMetadataId: string;
|
||||
recordIndexId: string;
|
||||
onBack: () => void;
|
||||
onFieldUpdated?: (
|
||||
viewFieldId: string,
|
||||
update: Partial<{ isVisible: boolean }>,
|
||||
) => void;
|
||||
onFieldCreated?: (recordField: RecordField) => void;
|
||||
};
|
||||
|
||||
export const RecordTableFieldsDropdownHiddenFieldsContent = ({
|
||||
objectMetadataId,
|
||||
recordIndexId,
|
||||
onBack,
|
||||
onFieldUpdated,
|
||||
onFieldCreated,
|
||||
}: RecordTableFieldsDropdownHiddenFieldsContentProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
@@ -61,6 +68,7 @@ export const RecordTableFieldsDropdownHiddenFieldsContent = ({
|
||||
|
||||
if (isDefined(existingRecordField)) {
|
||||
updateRecordField(fieldMetadataId, { isVisible: true });
|
||||
onFieldUpdated?.(existingRecordField.id, { isVisible: true });
|
||||
} else {
|
||||
const lastPosition =
|
||||
currentRecordFields.toSorted(sortByProperty('position', 'desc'))?.[0]
|
||||
@@ -75,6 +83,7 @@ export const RecordTableFieldsDropdownHiddenFieldsContent = ({
|
||||
};
|
||||
|
||||
upsertRecordField(newRecordField);
|
||||
onFieldCreated?.(newRecordField);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+17
-2
@@ -21,12 +21,17 @@ type RecordTableFieldsDropdownVisibleFieldsContentProps = {
|
||||
objectMetadataId: string;
|
||||
recordIndexId: string;
|
||||
onShowHiddenFields: () => void;
|
||||
onFieldUpdated?: (
|
||||
viewFieldId: string,
|
||||
update: Partial<{ position: number; isVisible: boolean }>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const RecordTableFieldsDropdownVisibleFieldsContent = ({
|
||||
objectMetadataId,
|
||||
recordIndexId,
|
||||
onShowHiddenFields,
|
||||
onFieldUpdated,
|
||||
}: RecordTableFieldsDropdownVisibleFieldsContentProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
@@ -71,14 +76,24 @@ export const RecordTableFieldsDropdownVisibleFieldsContent = ({
|
||||
return;
|
||||
}
|
||||
|
||||
reorderVisibleRecordFields({
|
||||
const updatedField = reorderVisibleRecordFields({
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
|
||||
if (isDefined(updatedField)) {
|
||||
onFieldUpdated?.(updatedField.id, { position: updatedField.position });
|
||||
}
|
||||
};
|
||||
|
||||
const handleHideField = (fieldMetadataId: string) => {
|
||||
updateRecordField(fieldMetadataId, { isVisible: false });
|
||||
const updatedField = updateRecordField(fieldMetadataId, {
|
||||
isVisible: false,
|
||||
});
|
||||
|
||||
if (isDefined(updatedField)) {
|
||||
onFieldUpdated?.(updatedField.id, { isVisible: false });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+9
@@ -3,6 +3,7 @@ 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 { useRecordTableWidgetFilterCallbacks } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetFilterCallbacks';
|
||||
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';
|
||||
@@ -45,6 +46,13 @@ export const RecordTableSettingsFilters = ({
|
||||
viewId,
|
||||
);
|
||||
|
||||
const { handleFilterUpdate } = useRecordTableWidgetFilterCallbacks({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
viewId,
|
||||
recordIndexId,
|
||||
});
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return null;
|
||||
}
|
||||
@@ -61,6 +69,7 @@ export const RecordTableSettingsFilters = ({
|
||||
<AdvancedFilterSidePanelContainer
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
isWorkflowFindRecords={false}
|
||||
onUpdate={handleFilterUpdate}
|
||||
/>
|
||||
<RecordTableSettingsFiltersInitializeStateEffect
|
||||
view={view}
|
||||
|
||||
+9
@@ -3,6 +3,7 @@ import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filter
|
||||
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 { useRecordTableWidgetSortCallbacks } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetSortCallbacks';
|
||||
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';
|
||||
@@ -50,6 +51,13 @@ export const RecordTableSettingsSorts = ({
|
||||
viewId,
|
||||
);
|
||||
|
||||
const { handleSortUpdate } = useRecordTableWidgetSortCallbacks({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
viewId,
|
||||
recordIndexId,
|
||||
});
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return null;
|
||||
}
|
||||
@@ -82,6 +90,7 @@ export const RecordTableSettingsSorts = ({
|
||||
<RecordTableSettingsSortsContent
|
||||
sortableFieldOptions={sortableFieldOptions}
|
||||
directionOptions={directionOptions}
|
||||
onUpdate={handleSortUpdate}
|
||||
/>
|
||||
<RecordTableSettingsSortsInitializeStateEffect view={view} />
|
||||
</RecordSortsComponentInstanceContext.Provider>
|
||||
|
||||
+6
@@ -24,11 +24,13 @@ const StyledAddButtonContainer = styled.div`
|
||||
type RecordTableSettingsSortsContentProps = {
|
||||
sortableFieldOptions: Array<SelectOption<string>>;
|
||||
directionOptions: Array<SelectOption<ViewSortDirection>>;
|
||||
onUpdate?: () => void;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsSortsContent = ({
|
||||
sortableFieldOptions,
|
||||
directionOptions,
|
||||
onUpdate,
|
||||
}: RecordTableSettingsSortsContentProps) => {
|
||||
const [currentRecordSorts, setCurrentRecordSorts] = useAtomComponentState(
|
||||
currentRecordSortsComponentState,
|
||||
@@ -48,12 +50,14 @@ export const RecordTableSettingsSortsContent = ({
|
||||
};
|
||||
|
||||
setCurrentRecordSorts([...currentRecordSorts, newSort]);
|
||||
onUpdate?.();
|
||||
};
|
||||
|
||||
const handleRemoveSort = (sortId: string) => {
|
||||
setCurrentRecordSorts(
|
||||
currentRecordSorts.filter((sort) => sort.id !== sortId),
|
||||
);
|
||||
onUpdate?.();
|
||||
};
|
||||
|
||||
const handleFieldChange = (sortId: string, fieldMetadataId: string) => {
|
||||
@@ -62,6 +66,7 @@ export const RecordTableSettingsSortsContent = ({
|
||||
sort.id === sortId ? { ...sort, fieldMetadataId } : sort,
|
||||
),
|
||||
);
|
||||
onUpdate?.();
|
||||
};
|
||||
|
||||
const handleDirectionChange = (
|
||||
@@ -73,6 +78,7 @@ export const RecordTableSettingsSortsContent = ({
|
||||
sort.id === sortId ? { ...sort, direction } : sort,
|
||||
),
|
||||
);
|
||||
onUpdate?.();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+10
-11
@@ -1,8 +1,9 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useViewById } from '@/views/hooks/useViewById';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -45,22 +46,20 @@ export const useRecordTableSettingsDescriptions = ({
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const currentRecordFields = useAtomComponentStateValue(
|
||||
currentRecordFieldsComponentState,
|
||||
const visibleRecordFields = useAtomComponentSelectorValue(
|
||||
visibleRecordFieldsComponentSelector,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const hasViewId = isDefined(viewId);
|
||||
|
||||
const activeVisibleFieldLabels: Array<string | undefined> = hasViewId
|
||||
? currentRecordFields
|
||||
.filter((field) => field.isVisible)
|
||||
.map(
|
||||
(field) =>
|
||||
sourceObjectMetadataItem?.fields.find(
|
||||
(metaField) => metaField.id === field.fieldMetadataItemId,
|
||||
)?.label,
|
||||
)
|
||||
? visibleRecordFields.map(
|
||||
(field) =>
|
||||
sourceObjectMetadataItem?.fields.find(
|
||||
(metaField) => metaField.id === field.fieldMetadataItemId,
|
||||
)?.label,
|
||||
)
|
||||
: [];
|
||||
|
||||
const activeFilterLabels: Array<string | undefined> =
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { AtLeastOneOf } from 'src/engine/metadata-modules/view-field-group/dtos/validators/at-least-one-of.validator';
|
||||
|
||||
@InputType()
|
||||
@AtLeastOneOf(['viewFieldId', 'fieldMetadataId'])
|
||||
export class UpsertViewWidgetViewFieldInput {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, {
|
||||
nullable: true,
|
||||
description: 'The id of an existing view field to update.',
|
||||
})
|
||||
viewFieldId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, {
|
||||
nullable: true,
|
||||
description:
|
||||
'The field metadata id. Used to create a new view field when viewFieldId is not provided.',
|
||||
})
|
||||
fieldMetadataId?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isVisible: boolean;
|
||||
|
||||
@IsNumber()
|
||||
@Field()
|
||||
position: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Field({ nullable: true })
|
||||
size?: number;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNumber, IsOptional, IsUUID } from 'class-validator';
|
||||
import { ViewFilterGroupLogicalOperator } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpsertViewWidgetViewFilterGroupInput {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
parentViewFilterGroupId?: string;
|
||||
|
||||
@Field(() => ViewFilterGroupLogicalOperator, {
|
||||
nullable: true,
|
||||
defaultValue: ViewFilterGroupLogicalOperator.AND,
|
||||
})
|
||||
logicalOperator?: ViewFilterGroupLogicalOperator;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Field({ nullable: true })
|
||||
positionInViewFilterGroup?: number;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsDefined,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { type ViewFilterValue } from 'src/engine/metadata-modules/view-filter/types/view-filter-value.type';
|
||||
|
||||
@InputType()
|
||||
export class UpsertViewWidgetViewFilterInput {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
fieldMetadataId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ViewFilterOperand)
|
||||
@Field({ nullable: true, defaultValue: ViewFilterOperand.CONTAINS })
|
||||
operand?: ViewFilterOperand;
|
||||
|
||||
@IsDefined()
|
||||
@Field(() => GraphQLJSON, { nullable: false })
|
||||
value: ViewFilterValue;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
viewFilterGroupId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Field({ nullable: true })
|
||||
positionInViewFilterGroup?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
subFieldName?: string;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsEnum, IsOptional, IsUUID } from 'class-validator';
|
||||
import { ViewSortDirection } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpsertViewWidgetViewSortInput {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
fieldMetadataId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ViewSortDirection)
|
||||
@Field(() => ViewSortDirection, {
|
||||
nullable: true,
|
||||
defaultValue: ViewSortDirection.ASC,
|
||||
})
|
||||
direction?: ViewSortDirection;
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { UpsertViewWidgetViewFieldInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget-view-field.input';
|
||||
import { UpsertViewWidgetViewFilterGroupInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget-view-filter-group.input';
|
||||
import { UpsertViewWidgetViewFilterInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget-view-filter.input';
|
||||
import { UpsertViewWidgetViewSortInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget-view-sort.input';
|
||||
|
||||
@InputType()
|
||||
export class UpsertViewWidgetInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType, {
|
||||
description: 'The id of the view widget (page layout widget).',
|
||||
})
|
||||
widgetId: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => UpsertViewWidgetViewFieldInput)
|
||||
@Field(() => [UpsertViewWidgetViewFieldInput], {
|
||||
nullable: true,
|
||||
description: 'The view fields to upsert.',
|
||||
})
|
||||
viewFields?: UpsertViewWidgetViewFieldInput[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => UpsertViewWidgetViewFilterInput)
|
||||
@Field(() => [UpsertViewWidgetViewFilterInput], {
|
||||
nullable: true,
|
||||
description: 'The view filters to upsert.',
|
||||
})
|
||||
viewFilters?: UpsertViewWidgetViewFilterInput[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => UpsertViewWidgetViewFilterGroupInput)
|
||||
@Field(() => [UpsertViewWidgetViewFilterGroupInput], {
|
||||
nullable: true,
|
||||
description: 'The view filter groups to upsert.',
|
||||
})
|
||||
viewFilterGroups?: UpsertViewWidgetViewFilterGroupInput[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => UpsertViewWidgetViewSortInput)
|
||||
@Field(() => [UpsertViewWidgetViewSortInput], {
|
||||
nullable: true,
|
||||
description: 'The view sorts to upsert.',
|
||||
})
|
||||
viewSorts?: UpsertViewWidgetViewSortInput[];
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export enum ViewExceptionCode {
|
||||
INVALID_VIEW_DATA = 'INVALID_VIEW_DATA',
|
||||
VIEW_CREATE_PERMISSION_DENIED = 'VIEW_CREATE_PERMISSION_DENIED',
|
||||
VIEW_MODIFY_PERMISSION_DENIED = 'VIEW_MODIFY_PERMISSION_DENIED',
|
||||
VIEW_WIDGET_NOT_FOUND = 'VIEW_WIDGET_NOT_FOUND',
|
||||
}
|
||||
|
||||
export enum ViewExceptionMessageKey {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
ResolveField,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { ViewType, ViewVisibility } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -20,6 +22,7 @@ import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-worksp
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
|
||||
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
|
||||
@@ -34,7 +37,10 @@ import { UpdateViewPermissionGuard } from 'src/engine/metadata-modules/view-perm
|
||||
import { ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto';
|
||||
import { CreateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/create-view.input';
|
||||
import { UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/update-view.input';
|
||||
import { UpsertViewWidgetInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget.input';
|
||||
import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
|
||||
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewWidgetUpsertService } from 'src/engine/metadata-modules/view/services/view-widget-upsert.service';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter';
|
||||
|
||||
@@ -44,6 +50,7 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/
|
||||
export class ViewResolver {
|
||||
constructor(
|
||||
private readonly viewService: ViewService,
|
||||
private readonly viewWidgetUpsertService: ViewWidgetUpsertService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@@ -197,6 +204,19 @@ export class ViewResolver {
|
||||
return isDefined(deletedView);
|
||||
}
|
||||
|
||||
@Mutation(() => ViewDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
async upsertViewWidget(
|
||||
@Args('input') input: UpsertViewWidgetInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ViewEntity> {
|
||||
return await this.viewWidgetUpsertService.upsertViewWidget({
|
||||
input,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => [ViewFieldDTO])
|
||||
async viewFields(
|
||||
@Parent() view: ViewDTO,
|
||||
|
||||
+859
@@ -0,0 +1,859 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
ViewFilterGroupLogicalOperator,
|
||||
ViewFilterOperand,
|
||||
ViewSortDirection,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
import { splitEntitiesByRemovalStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatPageLayoutWidgetConfigurationOfType } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/is-flat-page-layout-widget-configuration-of-type.util';
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { fromViewFieldOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-view-field/utils/from-view-field-overrides-to-universal-overrides.util';
|
||||
import { type FlatViewFilterGroupMaps } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group-maps.type';
|
||||
import { type FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group.type';
|
||||
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
|
||||
import { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type';
|
||||
import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
|
||||
import { sanitizeOverridableEntityInput } from 'src/engine/metadata-modules/utils/sanitize-overridable-entity-input.util';
|
||||
import { type UpsertViewWidgetViewFieldInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget-view-field.input';
|
||||
import { type UpsertViewWidgetViewFilterGroupInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget-view-filter-group.input';
|
||||
import { type UpsertViewWidgetViewFilterInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget-view-filter.input';
|
||||
import { type UpsertViewWidgetViewSortInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget-view-sort.input';
|
||||
import { type UpsertViewWidgetInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget.input';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import {
|
||||
ViewException,
|
||||
ViewExceptionCode,
|
||||
} from 'src/engine/metadata-modules/view/exceptions/view.exception';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const EMPTY_FIELD_OPS = {
|
||||
fieldsToCreate: [] as FlatViewField[],
|
||||
fieldsToUpdate: [] as FlatViewField[],
|
||||
};
|
||||
|
||||
const EMPTY_FILTER_GROUP_OPS = {
|
||||
filterGroupsToCreate: [] as FlatViewFilterGroup[],
|
||||
filterGroupsToUpdate: [] as FlatViewFilterGroup[],
|
||||
filterGroupsToRemove: [] as FlatViewFilterGroup[],
|
||||
};
|
||||
|
||||
const EMPTY_FILTER_OPS = {
|
||||
filtersToCreate: [] as FlatViewFilter[],
|
||||
filtersToUpdate: [] as FlatViewFilter[],
|
||||
filtersToRemove: [] as FlatViewFilter[],
|
||||
};
|
||||
|
||||
const EMPTY_SORT_OPS = {
|
||||
sortsToCreate: [] as FlatViewSort[],
|
||||
sortsToUpdate: [] as FlatViewSort[],
|
||||
sortsToRemove: [] as FlatViewSort[],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ViewWidgetUpsertService {
|
||||
constructor(
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
) {}
|
||||
|
||||
async upsertViewWidget({
|
||||
input,
|
||||
workspaceId,
|
||||
}: {
|
||||
input: UpsertViewWidgetInput;
|
||||
workspaceId: string;
|
||||
}): Promise<ViewEntity> {
|
||||
const { widgetId } = input;
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const {
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewFieldMaps,
|
||||
flatViewFilterMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
flatViewSortMaps,
|
||||
flatViewMaps,
|
||||
} =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatViewFieldMaps',
|
||||
'flatViewFilterMaps',
|
||||
'flatViewFilterGroupMaps',
|
||||
'flatViewSortMaps',
|
||||
'flatViewMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const widget = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: widgetId,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(widget) ||
|
||||
!isFlatPageLayoutWidgetConfigurationOfType(
|
||||
widget,
|
||||
WidgetConfigurationType.RECORD_TABLE,
|
||||
)
|
||||
) {
|
||||
throw new ViewException(
|
||||
t`Record table widget not found`,
|
||||
ViewExceptionCode.VIEW_WIDGET_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const viewId = widget.configuration.viewId;
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
throw new ViewException(
|
||||
t`Record table widget has no associated view`,
|
||||
ViewExceptionCode.VIEW_WIDGET_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const flatView = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: viewId,
|
||||
flatEntityMaps: flatViewMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatView)) {
|
||||
throw new ViewException(
|
||||
t`View not found for widget`,
|
||||
ViewExceptionCode.VIEW_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const upsertContext = {
|
||||
viewId,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
now: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (
|
||||
!isDefined(input.viewFields) &&
|
||||
!isDefined(input.viewFilterGroups) &&
|
||||
!isDefined(input.viewFilters) &&
|
||||
!isDefined(input.viewSorts)
|
||||
) {
|
||||
const view = await this.viewRepository.findOne({
|
||||
where: {
|
||||
id: upsertContext.viewId,
|
||||
workspaceId: upsertContext.workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(view)) {
|
||||
throw new ViewException(
|
||||
t`View not found`,
|
||||
ViewExceptionCode.VIEW_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
const existingViewFields = Object.values(
|
||||
flatViewFieldMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((field) => field.isActive && field.viewId === viewId);
|
||||
|
||||
const existingViewFilters = Object.values(
|
||||
flatViewFilterMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((filter) => filter.viewId === viewId);
|
||||
|
||||
const existingViewFilterGroups = Object.values(
|
||||
flatViewFilterGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((group) => group.viewId === viewId);
|
||||
|
||||
const existingViewSorts = Object.values(
|
||||
flatViewSortMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((sort) => sort.viewId === viewId);
|
||||
|
||||
const viewFieldOperations = isDefined(input.viewFields)
|
||||
? this.computeViewFieldOperations({
|
||||
inputFields: input.viewFields,
|
||||
existingViewFields,
|
||||
...upsertContext,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
})
|
||||
: EMPTY_FIELD_OPS;
|
||||
|
||||
const viewFilterGroupOperations = isDefined(input.viewFilterGroups)
|
||||
? this.computeViewFilterGroupOperations({
|
||||
inputFilterGroups: input.viewFilterGroups,
|
||||
existingViewFilterGroups,
|
||||
...upsertContext,
|
||||
flatViewMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
})
|
||||
: EMPTY_FILTER_GROUP_OPS;
|
||||
|
||||
const optimisticFilterGroupMaps =
|
||||
viewFilterGroupOperations.filterGroupsToCreate.reduce(
|
||||
(maps, group) =>
|
||||
addFlatEntityToFlatEntityMapsOrThrow({
|
||||
flatEntity: group,
|
||||
flatEntityMaps: maps,
|
||||
}),
|
||||
flatViewFilterGroupMaps,
|
||||
);
|
||||
|
||||
const viewFilterOperations = isDefined(input.viewFilters)
|
||||
? this.computeViewFilterOperations({
|
||||
inputFilters: input.viewFilters,
|
||||
existingViewFilters,
|
||||
...upsertContext,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFilterGroupMaps: optimisticFilterGroupMaps,
|
||||
})
|
||||
: EMPTY_FILTER_OPS;
|
||||
|
||||
const viewSortOperations = isDefined(input.viewSorts)
|
||||
? this.computeViewSortOperations({
|
||||
inputSorts: input.viewSorts,
|
||||
existingViewSorts,
|
||||
...upsertContext,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
})
|
||||
: EMPTY_SORT_OPS;
|
||||
|
||||
const {
|
||||
toHardDelete: filterGroupsToDelete,
|
||||
toDeactivate: filterGroupsToDeactivate,
|
||||
} = splitEntitiesByRemovalStrategy({
|
||||
entitiesToRemove: viewFilterGroupOperations.filterGroupsToRemove,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
upsertContext.applicationUniversalIdentifier,
|
||||
now: upsertContext.now,
|
||||
});
|
||||
|
||||
const { toHardDelete: filtersToDelete, toDeactivate: filtersToDeactivate } =
|
||||
splitEntitiesByRemovalStrategy({
|
||||
entitiesToRemove: viewFilterOperations.filtersToRemove,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
upsertContext.applicationUniversalIdentifier,
|
||||
now: upsertContext.now,
|
||||
});
|
||||
|
||||
const { toHardDelete: sortsToDelete, toDeactivate: sortsToDeactivate } =
|
||||
splitEntitiesByRemovalStrategy({
|
||||
entitiesToRemove: viewSortOperations.sortsToRemove,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
upsertContext.applicationUniversalIdentifier,
|
||||
now: upsertContext.now,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
viewField: {
|
||||
flatEntityToCreate: viewFieldOperations.fieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: viewFieldOperations.fieldsToUpdate,
|
||||
},
|
||||
viewFilterGroup: {
|
||||
flatEntityToCreate:
|
||||
viewFilterGroupOperations.filterGroupsToCreate,
|
||||
flatEntityToDelete: filterGroupsToDelete,
|
||||
flatEntityToUpdate: [
|
||||
...viewFilterGroupOperations.filterGroupsToUpdate,
|
||||
...filterGroupsToDeactivate,
|
||||
],
|
||||
},
|
||||
viewFilter: {
|
||||
flatEntityToCreate: viewFilterOperations.filtersToCreate,
|
||||
flatEntityToDelete: filtersToDelete,
|
||||
flatEntityToUpdate: [
|
||||
...viewFilterOperations.filtersToUpdate,
|
||||
...filtersToDeactivate,
|
||||
],
|
||||
},
|
||||
viewSort: {
|
||||
flatEntityToCreate: viewSortOperations.sortsToCreate,
|
||||
flatEntityToDelete: sortsToDelete,
|
||||
flatEntityToUpdate: [
|
||||
...viewSortOperations.sortsToUpdate,
|
||||
...sortsToDeactivate,
|
||||
],
|
||||
},
|
||||
},
|
||||
workspaceId: upsertContext.workspaceId,
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier:
|
||||
upsertContext.applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while upserting view widget',
|
||||
);
|
||||
}
|
||||
|
||||
const view = await this.viewRepository.findOne({
|
||||
where: {
|
||||
id: upsertContext.viewId,
|
||||
workspaceId: upsertContext.workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(view)) {
|
||||
throw new ViewException(
|
||||
t`View not found after upsert`,
|
||||
ViewExceptionCode.VIEW_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private computeViewFieldOperations({
|
||||
inputFields,
|
||||
existingViewFields,
|
||||
viewId,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
}: {
|
||||
inputFields: UpsertViewWidgetViewFieldInput[];
|
||||
existingViewFields: FlatViewField[];
|
||||
viewId: string;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatViewMaps: FlatViewMaps;
|
||||
}): {
|
||||
fieldsToCreate: FlatViewField[];
|
||||
fieldsToUpdate: FlatViewField[];
|
||||
} {
|
||||
const fieldsToCreate: FlatViewField[] = [];
|
||||
const fieldsToUpdate: FlatViewField[] = [];
|
||||
|
||||
for (const inputField of inputFields) {
|
||||
const existingField = isDefined(inputField.viewFieldId)
|
||||
? existingViewFields.find((f) => f.id === inputField.viewFieldId)
|
||||
: isDefined(inputField.fieldMetadataId)
|
||||
? existingViewFields.find(
|
||||
(f) => f.fieldMetadataId === inputField.fieldMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (isDefined(existingField)) {
|
||||
const resolvedIsVisible = isDefined(existingField.overrides?.isVisible)
|
||||
? existingField.overrides.isVisible
|
||||
: existingField.isVisible;
|
||||
const resolvedPosition = isDefined(existingField.overrides?.position)
|
||||
? existingField.overrides.position
|
||||
: existingField.position;
|
||||
const resolvedSize = isDefined(existingField.overrides?.size)
|
||||
? existingField.overrides.size
|
||||
: existingField.size;
|
||||
|
||||
const hasChanged =
|
||||
resolvedIsVisible !== inputField.isVisible ||
|
||||
resolvedPosition !== inputField.position ||
|
||||
(isDefined(inputField.size) && resolvedSize !== inputField.size);
|
||||
|
||||
if (!hasChanged) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const shouldOverride = isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier: applicationUniversalIdentifier,
|
||||
entityApplicationUniversalIdentifier:
|
||||
existingField.applicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const { overrides, updatedEditableProperties: sanitizedFieldProps } =
|
||||
sanitizeOverridableEntityInput({
|
||||
metadataName: 'viewField',
|
||||
existingFlatEntity: existingField,
|
||||
updatedEditableProperties: {
|
||||
isVisible: inputField.isVisible,
|
||||
position: inputField.position,
|
||||
...(isDefined(inputField.size) ? { size: inputField.size } : {}),
|
||||
},
|
||||
shouldOverride,
|
||||
});
|
||||
|
||||
const updatedField: FlatViewField = {
|
||||
...existingField,
|
||||
...sanitizedFieldProps,
|
||||
overrides,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
if (isDefined(overrides)) {
|
||||
updatedField.universalOverrides =
|
||||
fromViewFieldOverridesToUniversalOverrides({
|
||||
overrides,
|
||||
viewFieldGroupUniversalIdentifierById: {},
|
||||
});
|
||||
} else {
|
||||
updatedField.universalOverrides = null;
|
||||
}
|
||||
|
||||
fieldsToUpdate.push(updatedField);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDefined(inputField.fieldMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: inputField.fieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(fieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { fieldMetadataUniversalIdentifier, viewUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewField',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: inputField.fieldMetadataId,
|
||||
viewId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
},
|
||||
});
|
||||
|
||||
fieldsToCreate.push({
|
||||
id: v4(),
|
||||
workspaceId,
|
||||
applicationId,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
fieldMetadataId: inputField.fieldMetadataId,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewId,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupId: null,
|
||||
viewFieldGroupUniversalIdentifier: null,
|
||||
isVisible: inputField.isVisible,
|
||||
size: inputField.size ?? DEFAULT_VIEW_FIELD_SIZE,
|
||||
position: inputField.position,
|
||||
aggregateOperation: null,
|
||||
overrides: null,
|
||||
universalOverrides: null,
|
||||
isActive: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
return { fieldsToCreate, fieldsToUpdate };
|
||||
}
|
||||
|
||||
private computeViewFilterGroupOperations({
|
||||
inputFilterGroups,
|
||||
existingViewFilterGroups,
|
||||
viewId,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
flatViewMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
}: {
|
||||
inputFilterGroups: UpsertViewWidgetViewFilterGroupInput[];
|
||||
existingViewFilterGroups: FlatViewFilterGroup[];
|
||||
viewId: string;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
flatViewMaps: FlatViewMaps;
|
||||
flatViewFilterGroupMaps: FlatViewFilterGroupMaps;
|
||||
}): {
|
||||
filterGroupsToCreate: FlatViewFilterGroup[];
|
||||
filterGroupsToUpdate: FlatViewFilterGroup[];
|
||||
filterGroupsToRemove: FlatViewFilterGroup[];
|
||||
} {
|
||||
const filterGroupsToCreate: FlatViewFilterGroup[] = [];
|
||||
const filterGroupsToUpdate: FlatViewFilterGroup[] = [];
|
||||
const inputFilterGroupIds = new Set(
|
||||
inputFilterGroups.map((g) => g.id).filter(isDefined),
|
||||
);
|
||||
|
||||
for (const inputGroup of inputFilterGroups) {
|
||||
const existingGroup = isDefined(inputGroup.id)
|
||||
? existingViewFilterGroups.find((g) => g.id === inputGroup.id)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(existingGroup)) {
|
||||
const filterGroupId = inputGroup.id ?? v4();
|
||||
|
||||
const {
|
||||
viewUniversalIdentifier,
|
||||
parentViewFilterGroupUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewFilterGroup',
|
||||
foreignKeyValues: {
|
||||
viewId,
|
||||
parentViewFilterGroupId: inputGroup.parentViewFilterGroupId,
|
||||
},
|
||||
flatEntityMaps: { flatViewMaps, flatViewFilterGroupMaps },
|
||||
});
|
||||
|
||||
filterGroupsToCreate.push({
|
||||
id: filterGroupId,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
universalIdentifier: filterGroupId,
|
||||
applicationUniversalIdentifier,
|
||||
viewId,
|
||||
viewUniversalIdentifier,
|
||||
logicalOperator:
|
||||
inputGroup.logicalOperator ?? ViewFilterGroupLogicalOperator.AND,
|
||||
parentViewFilterGroupId: inputGroup.parentViewFilterGroupId ?? null,
|
||||
parentViewFilterGroupUniversalIdentifier,
|
||||
positionInViewFilterGroup:
|
||||
inputGroup.positionInViewFilterGroup ?? null,
|
||||
viewFilterIds: [],
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
childViewFilterGroupIds: [],
|
||||
childViewFilterGroupUniversalIdentifiers: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
});
|
||||
} else {
|
||||
const hasChanged =
|
||||
existingGroup.logicalOperator !== inputGroup.logicalOperator ||
|
||||
existingGroup.positionInViewFilterGroup !==
|
||||
inputGroup.positionInViewFilterGroup ||
|
||||
existingGroup.parentViewFilterGroupId !==
|
||||
inputGroup.parentViewFilterGroupId;
|
||||
|
||||
if (hasChanged) {
|
||||
const resolvedParentId =
|
||||
inputGroup.parentViewFilterGroupId ??
|
||||
existingGroup.parentViewFilterGroupId;
|
||||
|
||||
const { parentViewFilterGroupUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewFilterGroup',
|
||||
foreignKeyValues: {
|
||||
parentViewFilterGroupId: resolvedParentId,
|
||||
},
|
||||
flatEntityMaps: { flatViewFilterGroupMaps },
|
||||
});
|
||||
|
||||
filterGroupsToUpdate.push({
|
||||
...existingGroup,
|
||||
logicalOperator:
|
||||
inputGroup.logicalOperator ?? existingGroup.logicalOperator,
|
||||
positionInViewFilterGroup:
|
||||
inputGroup.positionInViewFilterGroup ??
|
||||
existingGroup.positionInViewFilterGroup,
|
||||
parentViewFilterGroupId: resolvedParentId,
|
||||
parentViewFilterGroupUniversalIdentifier,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filterGroupsToRemove = existingViewFilterGroups.filter(
|
||||
(g) => !inputFilterGroupIds.has(g.id),
|
||||
);
|
||||
|
||||
return { filterGroupsToCreate, filterGroupsToUpdate, filterGroupsToRemove };
|
||||
}
|
||||
|
||||
private computeViewFilterOperations({
|
||||
inputFilters,
|
||||
existingViewFilters,
|
||||
viewId,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
}: {
|
||||
inputFilters: UpsertViewWidgetViewFilterInput[];
|
||||
existingViewFilters: FlatViewFilter[];
|
||||
viewId: string;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatViewMaps: FlatViewMaps;
|
||||
flatViewFilterGroupMaps: FlatViewFilterGroupMaps;
|
||||
}): {
|
||||
filtersToCreate: FlatViewFilter[];
|
||||
filtersToUpdate: FlatViewFilter[];
|
||||
filtersToRemove: FlatViewFilter[];
|
||||
} {
|
||||
const filtersToCreate: FlatViewFilter[] = [];
|
||||
const filtersToUpdate: FlatViewFilter[] = [];
|
||||
const inputFilterIds = new Set(
|
||||
inputFilters.map((f) => f.id).filter(isDefined),
|
||||
);
|
||||
|
||||
for (const inputFilter of inputFilters) {
|
||||
const existingFilter = isDefined(inputFilter.id)
|
||||
? existingViewFilters.find((f) => f.id === inputFilter.id)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(existingFilter)) {
|
||||
const filterId = inputFilter.id ?? v4();
|
||||
|
||||
const {
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFilterGroupUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewFilter',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: inputFilter.fieldMetadataId,
|
||||
viewId,
|
||||
viewFilterGroupId: inputFilter.viewFilterGroupId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
},
|
||||
});
|
||||
|
||||
filtersToCreate.push({
|
||||
id: filterId,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
fieldMetadataId: inputFilter.fieldMetadataId,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewId,
|
||||
viewUniversalIdentifier,
|
||||
operand: inputFilter.operand ?? ViewFilterOperand.CONTAINS,
|
||||
value: inputFilter.value,
|
||||
viewFilterGroupId: inputFilter.viewFilterGroupId ?? null,
|
||||
viewFilterGroupUniversalIdentifier,
|
||||
positionInViewFilterGroup:
|
||||
inputFilter.positionInViewFilterGroup ?? null,
|
||||
subFieldName: inputFilter.subFieldName ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
});
|
||||
} else {
|
||||
const hasChanged =
|
||||
existingFilter.fieldMetadataId !== inputFilter.fieldMetadataId ||
|
||||
existingFilter.operand !== inputFilter.operand ||
|
||||
JSON.stringify(existingFilter.value) !==
|
||||
JSON.stringify(inputFilter.value) ||
|
||||
existingFilter.viewFilterGroupId !== inputFilter.viewFilterGroupId ||
|
||||
existingFilter.positionInViewFilterGroup !==
|
||||
inputFilter.positionInViewFilterGroup ||
|
||||
existingFilter.subFieldName !== inputFilter.subFieldName;
|
||||
|
||||
if (hasChanged) {
|
||||
const {
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewFilterGroupUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewFilter',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: inputFilter.fieldMetadataId,
|
||||
viewFilterGroupId: inputFilter.viewFilterGroupId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
},
|
||||
});
|
||||
|
||||
filtersToUpdate.push({
|
||||
...existingFilter,
|
||||
fieldMetadataId: inputFilter.fieldMetadataId,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
operand: inputFilter.operand ?? existingFilter.operand,
|
||||
value: inputFilter.value,
|
||||
viewFilterGroupId:
|
||||
inputFilter.viewFilterGroupId ?? existingFilter.viewFilterGroupId,
|
||||
viewFilterGroupUniversalIdentifier,
|
||||
positionInViewFilterGroup:
|
||||
inputFilter.positionInViewFilterGroup ??
|
||||
existingFilter.positionInViewFilterGroup,
|
||||
subFieldName:
|
||||
inputFilter.subFieldName ?? existingFilter.subFieldName,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filtersToRemove = existingViewFilters.filter(
|
||||
(f) => !inputFilterIds.has(f.id),
|
||||
);
|
||||
|
||||
return { filtersToCreate, filtersToUpdate, filtersToRemove };
|
||||
}
|
||||
|
||||
private computeViewSortOperations({
|
||||
inputSorts,
|
||||
existingViewSorts,
|
||||
viewId,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
}: {
|
||||
inputSorts: UpsertViewWidgetViewSortInput[];
|
||||
existingViewSorts: FlatViewSort[];
|
||||
viewId: string;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatViewMaps: FlatViewMaps;
|
||||
}): {
|
||||
sortsToCreate: FlatViewSort[];
|
||||
sortsToUpdate: FlatViewSort[];
|
||||
sortsToRemove: FlatViewSort[];
|
||||
} {
|
||||
const sortsToCreate: FlatViewSort[] = [];
|
||||
const sortsToUpdate: FlatViewSort[] = [];
|
||||
const inputSortIds = new Set(inputSorts.map((s) => s.id).filter(isDefined));
|
||||
|
||||
for (const inputSort of inputSorts) {
|
||||
const existingSort = isDefined(inputSort.id)
|
||||
? existingViewSorts.find((s) => s.id === inputSort.id)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(existingSort)) {
|
||||
const sortId = inputSort.id ?? v4();
|
||||
|
||||
const { fieldMetadataUniversalIdentifier, viewUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewSort',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: inputSort.fieldMetadataId,
|
||||
viewId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
},
|
||||
});
|
||||
|
||||
sortsToCreate.push({
|
||||
id: sortId,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
fieldMetadataId: inputSort.fieldMetadataId,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewId,
|
||||
viewUniversalIdentifier,
|
||||
direction: inputSort.direction ?? ViewSortDirection.ASC,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
});
|
||||
} else {
|
||||
const hasChanged =
|
||||
existingSort.fieldMetadataId !== inputSort.fieldMetadataId ||
|
||||
existingSort.direction !== inputSort.direction;
|
||||
|
||||
if (hasChanged) {
|
||||
const { fieldMetadataUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewSort',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: inputSort.fieldMetadataId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
});
|
||||
|
||||
sortsToUpdate.push({
|
||||
...existingSort,
|
||||
fieldMetadataId: inputSort.fieldMetadataId,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
direction: inputSort.direction ?? existingSort.direction,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sortsToRemove = existingViewSorts.filter(
|
||||
(s) => !inputSortIds.has(s.id),
|
||||
);
|
||||
|
||||
return { sortsToCreate, sortsToUpdate, sortsToRemove };
|
||||
}
|
||||
}
|
||||
+2
@@ -59,6 +59,8 @@ export const viewGraphqlApiExceptionHandler = (error: Error, i18n: I18n) => {
|
||||
throw new ForbiddenError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case ViewExceptionCode.VIEW_WIDGET_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-fie
|
||||
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
|
||||
import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module';
|
||||
import { ViewSortModule } from 'src/engine/metadata-modules/view-sort/view-sort.module';
|
||||
import { ViewWidgetUpsertService } from 'src/engine/metadata-modules/view/services/view-widget-upsert.service';
|
||||
import { ViewController } from 'src/engine/metadata-modules/view/controllers/view.controller';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewResolver } from 'src/engine/metadata-modules/view/resolvers/view.resolver';
|
||||
@@ -44,6 +45,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
ViewResolver,
|
||||
ViewQueryParamsService,
|
||||
ViewToolsFactory,
|
||||
ViewWidgetUpsertService,
|
||||
],
|
||||
exports: [
|
||||
ViewService,
|
||||
|
||||
+1119
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { VIEW_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
|
||||
|
||||
import { type UpsertViewWidgetInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget.input';
|
||||
|
||||
export const upsertViewWidgetQueryFactory = ({
|
||||
gqlFields = VIEW_GQL_FIELDS,
|
||||
input,
|
||||
}: {
|
||||
gqlFields?: string;
|
||||
input: UpsertViewWidgetInput;
|
||||
}) => ({
|
||||
query: gql`
|
||||
mutation UpsertViewWidget($input: UpsertViewWidgetInput!) {
|
||||
upsertViewWidget(input: $input) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { upsertViewWidgetQueryFactory } from 'test/integration/metadata/suites/view/utils/upsert-view-widget-query-factory.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type UpsertViewWidgetInput } from 'src/engine/metadata-modules/view/dtos/inputs/upsert-view-widget.input';
|
||||
import { type ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
|
||||
|
||||
export const upsertViewWidget = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail,
|
||||
}: {
|
||||
input: UpsertViewWidgetInput;
|
||||
gqlFields?: string;
|
||||
expectToFail?: boolean | null;
|
||||
}): CommonResponseBody<{
|
||||
upsertViewWidget: ViewDTO;
|
||||
}> => {
|
||||
const graphqlOperation = upsertViewWidgetQueryFactory({
|
||||
input,
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Upsert view widget should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Upsert view widget has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
Reference in New Issue
Block a user