Wire fields widget to backend + basic edition (#17965)
Closes https://github.com/twentyhq/core-team-issues/issues/2215 Closes https://github.com/twentyhq/core-team-issues/issues/2216 Closes https://github.com/twentyhq/core-team-issues/issues/2218 ## Fields widget edition demo https://github.com/user-attachments/assets/08626d70-8fcb-4ae2-9222-10ef57e75f90 ## Dashboards still work https://github.com/user-attachments/assets/aa9a9c45-a0a2-481e-b132-bc52254778da
This commit is contained in:
committed by
GitHub
parent
b107020ad3
commit
00209f7e2c
File diff suppressed because one or more lines are too long
-4
@@ -1,5 +1,4 @@
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
@@ -7,12 +6,9 @@ import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hook
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
|
||||
export const CancelRecordPageLayoutSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
|
||||
-4
@@ -1,17 +1,13 @@
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useResetLocationHash } from 'twenty-ui/utilities';
|
||||
|
||||
export const EditRecordPageLayoutSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
|
||||
+6
-4
@@ -1,22 +1,22 @@
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
|
||||
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
|
||||
export const SaveRecordPageLayoutSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { savePageLayout } = useSavePageLayout(pageLayoutId);
|
||||
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups({
|
||||
pageLayoutId,
|
||||
});
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
@@ -27,6 +27,8 @@ export const SaveRecordPageLayoutSingleRecordAction = () => {
|
||||
const result = await savePageLayout();
|
||||
|
||||
if (result.status === 'successful') {
|
||||
await saveFieldsWidgetGroups();
|
||||
|
||||
closeCommandMenu();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
|
||||
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
|
||||
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
@@ -69,6 +70,13 @@ export const useExecuteTasksOnAnyLocationChange = () => {
|
||||
false,
|
||||
);
|
||||
|
||||
set(
|
||||
hasInitializedFieldsWidgetGroupsDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
set(currentPageLayoutIdState, null);
|
||||
}
|
||||
|
||||
|
||||
+21
-15
@@ -3,12 +3,15 @@ import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/co
|
||||
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration';
|
||||
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
|
||||
import { FieldsConfigurationEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationEditor';
|
||||
import { FieldsWidgetGroupsDraftInitializationEffect } from '@/page-layout/widgets/fields/components/FieldsWidgetGroupsDraftInitializationEffect';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledOuterContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -28,25 +31,23 @@ const StyledContainer = styled.div`
|
||||
export const CommandMenuPageLayoutFieldsLayout = () => {
|
||||
const { goBackFromCommandMenu } = useCommandMenuHistory();
|
||||
|
||||
const { pageLayoutId, objectNameSingular } =
|
||||
const { pageLayoutId } =
|
||||
usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
|
||||
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
const defaultFieldsConfiguration =
|
||||
useTemporaryFieldsConfiguration(objectNameSingular);
|
||||
const [fieldsConfiguration, setFieldsConfiguration] =
|
||||
useState<FieldsConfiguration>(defaultFieldsConfiguration);
|
||||
const temporaryFieldsConfiguration = useTemporaryFieldsConfiguration();
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleConfigurationChange = (
|
||||
updatedConfiguration: FieldsConfiguration,
|
||||
) => {
|
||||
// TODO: replace with a call to updatePageLayoutWidget
|
||||
setFieldsConfiguration(updatedConfiguration);
|
||||
};
|
||||
const widgetConfiguration = widgetInEditMode.configuration;
|
||||
|
||||
const fieldsConfiguration: FieldsConfiguration =
|
||||
isDefined(widgetConfiguration) &&
|
||||
widgetConfiguration.configurationType === WidgetConfigurationType.FIELDS
|
||||
? (widgetConfiguration as FieldsConfiguration)
|
||||
: temporaryFieldsConfiguration;
|
||||
|
||||
return (
|
||||
<StyledOuterContainer>
|
||||
@@ -55,9 +56,14 @@ export const CommandMenuPageLayoutFieldsLayout = () => {
|
||||
onBackClick={goBackFromCommandMenu}
|
||||
/>
|
||||
<StyledContainer>
|
||||
<FieldsWidgetGroupsDraftInitializationEffect
|
||||
viewId={fieldsConfiguration.viewId ?? null}
|
||||
pageLayoutId={pageLayoutId}
|
||||
widgetId={widgetInEditMode.id}
|
||||
/>
|
||||
<FieldsConfigurationEditor
|
||||
configuration={fieldsConfiguration}
|
||||
onChange={handleConfigurationChange}
|
||||
pageLayoutId={pageLayoutId}
|
||||
widgetId={widgetInEditMode.id}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</StyledOuterContainer>
|
||||
|
||||
+13
-5
@@ -6,12 +6,13 @@ import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layo
|
||||
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration';
|
||||
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLayoutSidebarRight } from 'twenty-ui/display';
|
||||
import { type FieldsConfiguration } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -33,15 +34,22 @@ export const CommandMenuPageLayoutFieldsSettings = () => {
|
||||
usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
|
||||
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
const fieldsConfiguration =
|
||||
useTemporaryFieldsConfiguration(objectNameSingular);
|
||||
|
||||
const fieldsConfiguration = widgetInEditMode?.configuration as
|
||||
| FieldsConfiguration
|
||||
| undefined;
|
||||
|
||||
const { groups } = useFieldsWidgetGroups({
|
||||
viewId: fieldsConfiguration?.viewId ?? null,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalFieldsCount = fieldsConfiguration.sections.reduce(
|
||||
(count, section) => count + section.fields.length,
|
||||
const totalFieldsCount = groups.reduce(
|
||||
(count, group) => count + group.fields.length,
|
||||
0,
|
||||
);
|
||||
|
||||
|
||||
+4
-2
@@ -5,7 +5,6 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/
|
||||
import { getDateGranularityLabel } from '@/command-menu/pages/page-layout/utils/getDateGranularityLabel';
|
||||
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
|
||||
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
@@ -17,7 +16,10 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useR
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { type WidgetConfiguration } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
type WidgetConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type ChartDateGranularitySelectionDropdownContentProps = {
|
||||
axis?: 'primary' | 'secondary';
|
||||
|
||||
-3
@@ -32,10 +32,7 @@ export const usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord =
|
||||
throw new Error('Only one record should be selected');
|
||||
}
|
||||
|
||||
const recordId: string = targetedRecordsRule.selectedRecordIds[0];
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
|
||||
+1
-5
@@ -1,13 +1,9 @@
|
||||
import { type ChartWidget } from '@/command-menu/pages/page-layout/types/ChartWidget';
|
||||
import { isWidgetConfigurationOfTypeGraph } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfTypeGraph';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { type WidgetConfiguration } from '~/generated-metadata/graphql';
|
||||
|
||||
export const isChartWidget = (
|
||||
pageLayoutWidget: PageLayoutWidget,
|
||||
): pageLayoutWidget is ChartWidget => {
|
||||
return isWidgetConfigurationOfTypeGraph(
|
||||
// TODO: Remove this cast when we FieldsConfiguration and FieldConfiguration are in the backend
|
||||
pageLayoutWidget.configuration as WidgetConfiguration,
|
||||
);
|
||||
return isWidgetConfigurationOfTypeGraph(pageLayoutWidget.configuration);
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,11 +1,11 @@
|
||||
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
|
||||
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
|
||||
import {
|
||||
type AggregateChartConfiguration,
|
||||
type BarChartConfiguration,
|
||||
type CalendarConfiguration,
|
||||
type EmailsConfiguration,
|
||||
type FieldRichTextConfiguration,
|
||||
type FieldsConfiguration,
|
||||
type FilesConfiguration,
|
||||
type FrontComponentConfiguration,
|
||||
type GaugeChartConfiguration,
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
|
||||
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
|
||||
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
|
||||
import { type WidgetConfiguration } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
type WidgetConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const isWidgetConfigurationOfTypeGraph = (
|
||||
configuration:
|
||||
|
||||
+3
-1
@@ -50,13 +50,15 @@ const renderHooks = ({
|
||||
isCompact: false,
|
||||
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
|
||||
viewFields: [],
|
||||
viewFieldGroups: [],
|
||||
viewGroups: [],
|
||||
viewSorts: [],
|
||||
viewFilters: [],
|
||||
viewFilterGroups: [],
|
||||
kanbanAggregateOperation: AggregateOperations.COUNT,
|
||||
icon: '',
|
||||
kanbanAggregateOperationFieldMetadataId: '',
|
||||
position: 0,
|
||||
viewFilters: [],
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
createdByUserWorkspaceId: null,
|
||||
shouldHideEmptyGroups: false,
|
||||
|
||||
+3
@@ -149,6 +149,9 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
|
||||
updateCurrentViewParams.mainGroupByFieldMetadataId = null;
|
||||
return await updateCurrentView(updateCurrentViewParams);
|
||||
}
|
||||
case ViewType.FieldsWidget: {
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
return assertUnreachable(viewType);
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-r
|
||||
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
|
||||
import { useRecordPageLayoutId } from '@/page-layout/hooks/useRecordPageLayoutId';
|
||||
import { usePageLayoutIdForRecord } from '@/page-layout/hooks/usePageLayoutIdForRecord';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
@@ -50,7 +50,7 @@ export const PageLayoutRecordPageRenderer = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutId({
|
||||
const { pageLayoutId } = usePageLayoutIdForRecord({
|
||||
id: targetRecordIdentifier.id,
|
||||
targetObjectNameSingular: targetRecordIdentifier.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { getPageLayoutVerticalListViewerVariant } from '@/page-layout/components/utils/getPageLayoutVerticalListViewerVariant';
|
||||
|
||||
describe('getPageLayoutVerticalListViewerVariant', () => {
|
||||
it('should return side-column when isInPinnedTab is true', () => {
|
||||
expect(
|
||||
getPageLayoutVerticalListViewerVariant({
|
||||
isInPinnedTab: true,
|
||||
isMobile: false,
|
||||
isInRightDrawer: false,
|
||||
}),
|
||||
).toBe('side-column');
|
||||
});
|
||||
|
||||
it('should return side-column when isMobile is true', () => {
|
||||
expect(
|
||||
getPageLayoutVerticalListViewerVariant({
|
||||
isInPinnedTab: false,
|
||||
isMobile: true,
|
||||
isInRightDrawer: false,
|
||||
}),
|
||||
).toBe('side-column');
|
||||
});
|
||||
|
||||
it('should return side-column when isInRightDrawer is true', () => {
|
||||
expect(
|
||||
getPageLayoutVerticalListViewerVariant({
|
||||
isInPinnedTab: false,
|
||||
isMobile: false,
|
||||
isInRightDrawer: true,
|
||||
}),
|
||||
).toBe('side-column');
|
||||
});
|
||||
|
||||
it('should return default when none of the conditions are true', () => {
|
||||
expect(
|
||||
getPageLayoutVerticalListViewerVariant({
|
||||
isInPinnedTab: false,
|
||||
isMobile: false,
|
||||
isInRightDrawer: false,
|
||||
}),
|
||||
).toBe('default');
|
||||
});
|
||||
});
|
||||
+7
-7
@@ -47,7 +47,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -85,7 +85,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -123,7 +123,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -161,7 +161,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -199,7 +199,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -237,7 +237,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -275,7 +275,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+11
-10
@@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -48,8 +49,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
// a configuration with actual field metadata IDs from the backend.
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -71,8 +72,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -116,8 +117,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -161,8 +162,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -199,8 +200,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+7
-7
@@ -47,7 +47,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -85,7 +85,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -123,7 +123,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -161,7 +161,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -199,7 +199,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -237,7 +237,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -275,7 +275,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+15
-14
@@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -45,8 +46,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -83,8 +84,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -121,8 +122,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -159,8 +160,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -197,8 +198,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -235,8 +236,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -273,8 +274,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+11
-10
@@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -45,8 +46,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -83,8 +84,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -121,8 +122,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -159,8 +160,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -197,8 +198,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+11
-10
@@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -45,8 +46,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -68,8 +69,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -113,8 +114,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -158,8 +159,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -196,8 +197,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+3
-2
@@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -50,8 +51,8 @@ export const DEFAULT_WORKFLOW_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+5
-4
@@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -51,8 +52,8 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -90,8 +91,8 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+5
-4
@@ -3,6 +3,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -51,8 +52,8 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -90,8 +91,8 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+1
@@ -165,6 +165,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
}
|
||||
... on FieldsConfiguration {
|
||||
configurationType
|
||||
viewId
|
||||
}
|
||||
... on FilesConfiguration {
|
||||
configurationType
|
||||
|
||||
+4
-13
@@ -1,9 +1,8 @@
|
||||
import { PAGE_LAYOUT_TAB_FRAGMENT } from '@/dashboards/graphql/fragments/pageLayoutTabFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { PAGE_LAYOUT_WIDGET_FRAGMENT } from '@/page-layout/graphql/fragments/pageLayoutWidgetFragment';
|
||||
|
||||
export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql`
|
||||
${PAGE_LAYOUT_WIDGET_FRAGMENT}
|
||||
${PAGE_LAYOUT_TAB_FRAGMENT}
|
||||
mutation UpdatePageLayoutWithTabsAndWidgets(
|
||||
$id: String!
|
||||
$input: UpdatePageLayoutWithTabsInput!
|
||||
@@ -13,20 +12,12 @@ export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql`
|
||||
name
|
||||
type
|
||||
objectMetadataId
|
||||
defaultTabToFocusOnMobileAndSidePanelId
|
||||
createdAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
tabs {
|
||||
id
|
||||
applicationId
|
||||
title
|
||||
position
|
||||
pageLayoutId
|
||||
widgets {
|
||||
...PageLayoutWidgetFragment
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
...PageLayoutTabFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ describe('usePageLayoutWithRelationWidgets', () => {
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
sections: [],
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
|
||||
import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId';
|
||||
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const usePageLayoutIdForRecord = ({
|
||||
id,
|
||||
targetObjectNameSingular,
|
||||
}: TargetRecordIdentifier) => {
|
||||
const isDashboard =
|
||||
targetObjectNameSingular === CoreObjectNameSingular.Dashboard;
|
||||
|
||||
const { record } = useFindOneRecord<ObjectRecord & { pageLayoutId?: string }>(
|
||||
{
|
||||
objectNameSingular: targetObjectNameSingular,
|
||||
objectRecordId: id,
|
||||
withSoftDeleted: true,
|
||||
skip: !isDashboard,
|
||||
},
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const recordPageLayout = useRecoilValue(
|
||||
recordPageLayoutByObjectMetadataIdFamilySelector({
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isDashboard) {
|
||||
return {
|
||||
pageLayoutId: record?.pageLayoutId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const pageLayoutId = isDefined(recordPageLayout)
|
||||
? recordPageLayout.id
|
||||
: getDefaultRecordPageLayoutId({ targetObjectNameSingular });
|
||||
|
||||
return {
|
||||
pageLayoutId,
|
||||
};
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getRecordPageLayoutId } from '@/page-layout/utils/getRecordPageLayoutId';
|
||||
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
|
||||
|
||||
export const useRecordPageLayoutId = ({
|
||||
id,
|
||||
targetObjectNameSingular,
|
||||
}: TargetRecordIdentifier) => {
|
||||
const { record } = useFindOneRecord<ObjectRecord & { pageLayoutId?: string }>(
|
||||
{
|
||||
objectNameSingular: targetObjectNameSingular,
|
||||
objectRecordId: id,
|
||||
withSoftDeleted: true,
|
||||
},
|
||||
);
|
||||
|
||||
const pageLayoutId = getRecordPageLayoutId({
|
||||
record,
|
||||
targetObjectNameSingular,
|
||||
});
|
||||
|
||||
return {
|
||||
pageLayoutId,
|
||||
};
|
||||
};
|
||||
+15
-16
@@ -1,27 +1,26 @@
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { getRecordPageLayoutId } from '@/page-layout/utils/getRecordPageLayoutId';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
|
||||
import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId';
|
||||
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useRecordPageLayoutIdFromRecordStoreOrThrow = ({
|
||||
id,
|
||||
targetObjectNameSingular,
|
||||
}: TargetRecordIdentifier) => {
|
||||
const record = useRecoilValue(recordStoreFamilyState(id));
|
||||
|
||||
if (!isDefined(record)) {
|
||||
throw new Error(`Record with id ${id} not found in record store`);
|
||||
}
|
||||
|
||||
const pageLayoutId = getRecordPageLayoutId({
|
||||
record,
|
||||
targetObjectNameSingular,
|
||||
}: Omit<TargetRecordIdentifier, 'id'>) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: targetObjectNameSingular,
|
||||
});
|
||||
|
||||
if (!isDefined(pageLayoutId)) {
|
||||
throw new Error(`Page layout id not found for record with id ${id}`);
|
||||
}
|
||||
const recordPageLayout = useRecoilValue(
|
||||
recordPageLayoutByObjectMetadataIdFamilySelector({
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const pageLayoutId = isDefined(recordPageLayout)
|
||||
? recordPageLayout.id
|
||||
: getDefaultRecordPageLayoutId({ targetObjectNameSingular });
|
||||
|
||||
return {
|
||||
pageLayoutId,
|
||||
|
||||
+19
@@ -1,4 +1,6 @@
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
@@ -42,6 +44,16 @@ export const useResetDraftPageLayoutToPersistedPageLayout = (
|
||||
instanceId: tabListComponentInstanceId,
|
||||
});
|
||||
|
||||
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const fieldsWidgetGroupsPersistedState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsPersistedComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const resetDraftPageLayoutToPersistedPageLayout = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
() => {
|
||||
@@ -73,12 +85,19 @@ export const useResetDraftPageLayoutToPersistedPageLayout = (
|
||||
|
||||
const tabLayouts = convertPageLayoutToTabLayouts(pageLayoutPersisted);
|
||||
set(pageLayoutCurrentLayoutsState, tabLayouts);
|
||||
|
||||
const fieldsWidgetGroupsPersisted = snapshot
|
||||
.getLoadable(fieldsWidgetGroupsPersistedState)
|
||||
.getValue();
|
||||
set(fieldsWidgetGroupsDraftState, fieldsWidgetGroupsPersisted);
|
||||
}
|
||||
},
|
||||
[
|
||||
pageLayoutDraftState,
|
||||
pageLayoutPersistedState,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
fieldsWidgetGroupsDraftState,
|
||||
fieldsWidgetGroupsPersistedState,
|
||||
activeTabIdAtom,
|
||||
store,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { computeFieldsWidgetFieldDiff } from '@/page-layout/utils/computeFieldsWidgetFieldDiff';
|
||||
import { computeFieldsWidgetGroupDiff } from '@/page-layout/utils/computeFieldsWidgetGroupDiff';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { usePerformViewFieldGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFieldGroupAPIPersist';
|
||||
import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseSaveFieldsWidgetGroupsParams = {
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
export const useSaveFieldsWidgetGroups = ({
|
||||
pageLayoutId,
|
||||
}: UseSaveFieldsWidgetGroupsParams) => {
|
||||
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const fieldsWidgetGroupsPersistedState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsPersistedComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutPersistedState = useRecoilComponentCallbackState(
|
||||
pageLayoutPersistedComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const {
|
||||
performViewFieldGroupAPICreate,
|
||||
performViewFieldGroupAPIUpdate,
|
||||
performViewFieldGroupAPIDelete,
|
||||
} = usePerformViewFieldGroupAPIPersist();
|
||||
|
||||
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
|
||||
|
||||
const { refreshAllCoreViews } = useRefreshAllCoreViews();
|
||||
|
||||
const getViewIdForWidget = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(widgetId: string): string | null => {
|
||||
const pageLayoutPersisted = snapshot
|
||||
.getLoadable(pageLayoutPersistedState)
|
||||
.getValue();
|
||||
|
||||
if (!isDefined(pageLayoutPersisted)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const tab of pageLayoutPersisted.tabs) {
|
||||
for (const widget of tab.widgets) {
|
||||
if (
|
||||
widget.id === widgetId &&
|
||||
isDefined(widget.configuration) &&
|
||||
widget.configuration.configurationType ===
|
||||
WidgetConfigurationType.FIELDS
|
||||
) {
|
||||
return (
|
||||
(widget.configuration as FieldsConfiguration).viewId ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[pageLayoutPersistedState],
|
||||
);
|
||||
|
||||
const saveFieldsWidgetGroups = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async () => {
|
||||
const allDraftGroups = snapshot
|
||||
.getLoadable(fieldsWidgetGroupsDraftState)
|
||||
.getValue();
|
||||
const allPersistedGroups = snapshot
|
||||
.getLoadable(fieldsWidgetGroupsPersistedState)
|
||||
.getValue();
|
||||
|
||||
const widgetIds = new Set([
|
||||
...Object.keys(allDraftGroups),
|
||||
...Object.keys(allPersistedGroups),
|
||||
]);
|
||||
|
||||
for (const widgetId of widgetIds) {
|
||||
const draftGroups = allDraftGroups[widgetId] ?? [];
|
||||
const persistedGroups = allPersistedGroups[widgetId] ?? [];
|
||||
|
||||
if (draftGroups.length === 0 && persistedGroups.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewId = getViewIdForWidget(widgetId);
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { createdGroups, deletedGroups, updatedGroups } =
|
||||
computeFieldsWidgetGroupDiff(persistedGroups, draftGroups);
|
||||
|
||||
if (createdGroups.length > 0) {
|
||||
await performViewFieldGroupAPICreate({
|
||||
inputs: createdGroups.map((group) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
position: group.position,
|
||||
isVisible: group.isVisible,
|
||||
viewId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (deletedGroups.length > 0) {
|
||||
for (const group of deletedGroups) {
|
||||
await performViewFieldGroupAPIDelete([
|
||||
{ input: { id: group.id } },
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedGroups.length > 0) {
|
||||
const updates = updatedGroups.map((group) => ({
|
||||
input: {
|
||||
id: group.id,
|
||||
update: {
|
||||
name: group.name,
|
||||
position: group.position,
|
||||
isVisible: group.isVisible,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await performViewFieldGroupAPIUpdate(updates);
|
||||
}
|
||||
|
||||
const fieldUpdates = computeFieldsWidgetFieldDiff(
|
||||
persistedGroups,
|
||||
draftGroups,
|
||||
);
|
||||
|
||||
if (fieldUpdates.length > 0) {
|
||||
const viewFieldUpdateInputs = fieldUpdates.map(
|
||||
({ viewFieldId, ...updates }) => ({
|
||||
input: {
|
||||
id: viewFieldId,
|
||||
update: updates,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await performViewFieldAPIUpdate(viewFieldUpdateInputs);
|
||||
}
|
||||
}
|
||||
|
||||
set(fieldsWidgetGroupsPersistedState, allDraftGroups);
|
||||
|
||||
await refreshAllCoreViews();
|
||||
|
||||
return { status: 'successful' as const };
|
||||
},
|
||||
[
|
||||
fieldsWidgetGroupsDraftState,
|
||||
fieldsWidgetGroupsPersistedState,
|
||||
getViewIdForWidget,
|
||||
performViewFieldGroupAPICreate,
|
||||
performViewFieldGroupAPIDelete,
|
||||
performViewFieldGroupAPIUpdate,
|
||||
performViewFieldAPIUpdate,
|
||||
refreshAllCoreViews,
|
||||
],
|
||||
);
|
||||
|
||||
return { saveFieldsWidgetGroups };
|
||||
};
|
||||
@@ -6,11 +6,13 @@ import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayo
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
|
||||
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
|
||||
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
@@ -55,9 +57,17 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
result.response.data?.updatePageLayoutWithTabsAndWidgets;
|
||||
|
||||
if (isDefined(updatedPageLayout)) {
|
||||
const pageLayoutToPersist: PageLayout =
|
||||
const serverLayout: PageLayout =
|
||||
transformPageLayout(updatedPageLayout);
|
||||
|
||||
const pageLayoutToPersist =
|
||||
serverLayout.type === PageLayoutType.RECORD_PAGE
|
||||
? reInjectDynamicRelationWidgetsFromDraft(
|
||||
serverLayout,
|
||||
pageLayoutDraft,
|
||||
)
|
||||
: serverLayout;
|
||||
|
||||
set(pageLayoutPersistedCallbackState, pageLayoutToPersist);
|
||||
set(
|
||||
pageLayoutCurrentLayoutsCallbackState,
|
||||
|
||||
+10
-78
@@ -1,80 +1,12 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useTemporaryFieldsConfiguration = (
|
||||
objectNameSingular: string,
|
||||
): FieldsConfiguration => {
|
||||
const { t } = useLingui();
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const configuration = useMemo<FieldsConfiguration>(() => {
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error('Object metadata item is not defined');
|
||||
}
|
||||
|
||||
const fieldsToDisplay = objectMetadataItem.fields;
|
||||
|
||||
if (fieldsToDisplay.length === 0) {
|
||||
throw new Error('No fields to display');
|
||||
}
|
||||
|
||||
const generalFields: Array<{ fieldMetadataId: string; position: number }> =
|
||||
[];
|
||||
const otherFields: Array<{ fieldMetadataId: string; position: number }> =
|
||||
[];
|
||||
|
||||
let generalPosition = 0;
|
||||
let otherPosition = 0;
|
||||
|
||||
fieldsToDisplay.forEach((field) => {
|
||||
if (field.isCustom === true) {
|
||||
otherFields.push({
|
||||
fieldMetadataId: field.id,
|
||||
position: otherPosition++,
|
||||
});
|
||||
} else {
|
||||
generalFields.push({
|
||||
fieldMetadataId: field.id,
|
||||
position: generalPosition++,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sections = [];
|
||||
|
||||
if (generalFields.length > 0) {
|
||||
sections.push({
|
||||
id: `${objectNameSingular}-section-general`,
|
||||
title: t`General`,
|
||||
position: 0,
|
||||
fields: generalFields,
|
||||
});
|
||||
}
|
||||
|
||||
if (otherFields.length > 0) {
|
||||
sections.push({
|
||||
id: `${objectNameSingular}-section-other`,
|
||||
title: t`Other`,
|
||||
position: 1,
|
||||
fields: otherFields,
|
||||
});
|
||||
}
|
||||
|
||||
if (sections.length === 0) {
|
||||
throw new Error('No sections to display');
|
||||
}
|
||||
|
||||
return {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections,
|
||||
};
|
||||
}, [objectMetadataItem, objectNameSingular, t]);
|
||||
|
||||
return configuration;
|
||||
export const useTemporaryFieldsConfiguration = (): FieldsConfiguration => {
|
||||
return {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
};
|
||||
};
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export const fieldsWidgetGroupsDraftComponentState = createComponentState<
|
||||
Record<string, FieldsWidgetGroup[]>
|
||||
>({
|
||||
key: 'fieldsWidgetGroupsDraftComponentState',
|
||||
defaultValue: {},
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export const fieldsWidgetGroupsPersistedComponentState = createComponentState<
|
||||
Record<string, FieldsWidgetGroup[]>
|
||||
>({
|
||||
key: 'fieldsWidgetGroupsPersistedComponentState',
|
||||
defaultValue: {},
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export const hasInitializedFieldsWidgetGroupsDraftComponentState =
|
||||
createComponentState<Record<string, boolean>>({
|
||||
key: 'hasInitializedFieldsWidgetGroupsDraftComponentState',
|
||||
defaultValue: {},
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { recordPageLayoutsState } from '@/page-layout/states/recordPageLayoutsState';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
export const recordPageLayoutByObjectMetadataIdFamilySelector = selectorFamily<
|
||||
PageLayout | undefined,
|
||||
{ objectMetadataId: string }
|
||||
>({
|
||||
key: 'recordPageLayoutByObjectMetadataIdFamilySelector',
|
||||
get:
|
||||
({ objectMetadataId }) =>
|
||||
({ get }) => {
|
||||
const recordPageLayouts = get(recordPageLayoutsState);
|
||||
|
||||
return recordPageLayouts.find(
|
||||
(pageLayout) => pageLayout.objectMetadataId === objectMetadataId,
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type RulesLogic } from 'json-logic-js';
|
||||
|
||||
// Legacy types - kept for temporary configuration fallback
|
||||
export type FieldsConfigurationFieldItem = {
|
||||
fieldMetadataId: string;
|
||||
position: number;
|
||||
@@ -8,15 +9,9 @@ export type FieldsConfigurationFieldItem = {
|
||||
isVisible?: boolean;
|
||||
};
|
||||
|
||||
export type FieldsConfigurationSection = {
|
||||
export type FieldsConfigurationGroup = {
|
||||
id: string;
|
||||
title: string;
|
||||
position: number;
|
||||
fields: FieldsConfigurationFieldItem[];
|
||||
};
|
||||
|
||||
export type FieldsConfiguration = {
|
||||
__typename: 'FieldsConfiguration';
|
||||
configurationType: 'FIELDS';
|
||||
sections: FieldsConfigurationSection[];
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
|
||||
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
type PageLayoutWidget as PageLayoutWidgetGenerated,
|
||||
type WidgetConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { computeFieldsWidgetFieldDiff } from '@/page-layout/utils/computeFieldsWidgetFieldDiff';
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
|
||||
const mockFieldMetadataItem = {
|
||||
id: 'field-meta-1',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'TEXT',
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const makeGroup = (
|
||||
overrides: Partial<FieldsWidgetGroup> & { id: string },
|
||||
): FieldsWidgetGroup => ({
|
||||
name: 'Group',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fields: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('computeFieldsWidgetFieldDiff', () => {
|
||||
it('should return empty updates when both arrays are empty', () => {
|
||||
const result = computeFieldsWidgetFieldDiff([], []);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty updates when fields are identical', () => {
|
||||
const groups: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetFieldDiff(groups, groups);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect visibility changes', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: false,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetFieldDiff(persisted, draft);
|
||||
|
||||
expect(result).toEqual([{ viewFieldId: 'vf1', isVisible: false }]);
|
||||
});
|
||||
|
||||
it('should detect position changes', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetFieldDiff(persisted, draft);
|
||||
|
||||
expect(result).toEqual([{ viewFieldId: 'vf1', position: 3 }]);
|
||||
});
|
||||
|
||||
it('should detect group changes (field moved to different group)', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
makeGroup({ id: 'g2' }),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1' }),
|
||||
makeGroup({
|
||||
id: 'g2',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetFieldDiff(persisted, draft);
|
||||
|
||||
expect(result).toEqual([{ viewFieldId: 'vf1', viewFieldGroupId: 'g2' }]);
|
||||
});
|
||||
|
||||
it('should detect multiple simultaneous changes on a field', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
makeGroup({ id: 'g2' }),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1' }),
|
||||
makeGroup({
|
||||
id: 'g2',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetFieldDiff(persisted, draft);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
viewFieldId: 'vf1',
|
||||
isVisible: false,
|
||||
position: 5,
|
||||
viewFieldGroupId: 'g2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip fields without viewFieldId', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
globalIndex: 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetFieldDiff(persisted, draft);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should skip fields not found in persisted state', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [makeGroup({ id: 'g1' })];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf-new',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetFieldDiff(persisted, draft);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle multiple fields across multiple groups', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
globalIndex: 1,
|
||||
viewFieldId: 'vf2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
makeGroup({
|
||||
id: 'g2',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: false,
|
||||
globalIndex: 2,
|
||||
viewFieldId: 'vf3',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 1,
|
||||
isVisible: false,
|
||||
globalIndex: 1,
|
||||
viewFieldId: 'vf2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
makeGroup({
|
||||
id: 'g2',
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 2,
|
||||
viewFieldId: 'vf3',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetFieldDiff(persisted, draft);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContainEqual({ viewFieldId: 'vf2', isVisible: false });
|
||||
expect(result).toContainEqual({ viewFieldId: 'vf3', isVisible: true });
|
||||
});
|
||||
});
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { computeFieldsWidgetGroupDiff } from '@/page-layout/utils/computeFieldsWidgetGroupDiff';
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
|
||||
const mockFieldMetadataItem = {
|
||||
id: 'field-meta-1',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'TEXT',
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const makeGroup = (
|
||||
overrides: Partial<FieldsWidgetGroup> & { id: string },
|
||||
): FieldsWidgetGroup => ({
|
||||
name: 'Group',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fields: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('computeFieldsWidgetGroupDiff', () => {
|
||||
it('should return empty diffs when both arrays are empty', () => {
|
||||
const result = computeFieldsWidgetGroupDiff([], []);
|
||||
|
||||
expect(result).toEqual({
|
||||
createdGroups: [],
|
||||
deletedGroups: [],
|
||||
updatedGroups: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty diffs when groups are identical', () => {
|
||||
const groups: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Group 1', position: 0 }),
|
||||
makeGroup({ id: 'g2', name: 'Group 2', position: 1 }),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetGroupDiff(groups, groups);
|
||||
|
||||
expect(result).toEqual({
|
||||
createdGroups: [],
|
||||
deletedGroups: [],
|
||||
updatedGroups: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect created groups', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Group 1', position: 0 }),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Group 1', position: 0 }),
|
||||
makeGroup({ id: 'g2', name: 'New Group', position: 1 }),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetGroupDiff(persisted, draft);
|
||||
|
||||
expect(result.createdGroups).toHaveLength(1);
|
||||
expect(result.createdGroups[0].id).toBe('g2');
|
||||
expect(result.deletedGroups).toHaveLength(0);
|
||||
expect(result.updatedGroups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect deleted groups', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Group 1', position: 0 }),
|
||||
makeGroup({ id: 'g2', name: 'Group 2', position: 1 }),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Group 1', position: 0 }),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetGroupDiff(persisted, draft);
|
||||
|
||||
expect(result.createdGroups).toHaveLength(0);
|
||||
expect(result.deletedGroups).toHaveLength(1);
|
||||
expect(result.deletedGroups[0].id).toBe('g2');
|
||||
expect(result.updatedGroups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect updated groups when name changes', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Old Name', position: 0 }),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'New Name', position: 0 }),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetGroupDiff(persisted, draft);
|
||||
|
||||
expect(result.updatedGroups).toHaveLength(1);
|
||||
expect(result.updatedGroups[0].name).toBe('New Name');
|
||||
});
|
||||
|
||||
it('should detect updated groups when position changes', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Group 1', position: 0 }),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Group 1', position: 5 }),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetGroupDiff(persisted, draft);
|
||||
|
||||
expect(result.updatedGroups).toHaveLength(1);
|
||||
expect(result.updatedGroups[0].position).toBe(5);
|
||||
});
|
||||
|
||||
it('should detect updated groups when visibility changes', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', isVisible: true }),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', isVisible: false }),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetGroupDiff(persisted, draft);
|
||||
|
||||
expect(result.updatedGroups).toHaveLength(1);
|
||||
expect(result.updatedGroups[0].isVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle simultaneous creates, deletes, and updates', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Group 1', position: 0 }),
|
||||
makeGroup({ id: 'g2', name: 'To Delete', position: 1 }),
|
||||
makeGroup({ id: 'g3', name: 'To Update', position: 2 }),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({ id: 'g1', name: 'Group 1', position: 0 }),
|
||||
makeGroup({ id: 'g3', name: 'Updated Name', position: 2 }),
|
||||
makeGroup({ id: 'g4', name: 'New Group', position: 3 }),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetGroupDiff(persisted, draft);
|
||||
|
||||
expect(result.createdGroups).toHaveLength(1);
|
||||
expect(result.createdGroups[0].id).toBe('g4');
|
||||
expect(result.deletedGroups).toHaveLength(1);
|
||||
expect(result.deletedGroups[0].id).toBe('g2');
|
||||
expect(result.updatedGroups).toHaveLength(1);
|
||||
expect(result.updatedGroups[0].id).toBe('g3');
|
||||
});
|
||||
|
||||
it('should not mark group as updated when fields differ but group props are same', () => {
|
||||
const persisted: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
name: 'Group 1',
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const draft: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
name: 'Group 1',
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: false,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetGroupDiff(persisted, draft);
|
||||
|
||||
expect(result.updatedGroups).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
|
||||
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { PageLayoutType, WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
const makeWidget = (
|
||||
overrides: Partial<PageLayoutWidget> & { id: string },
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
title: 'Widget',
|
||||
type: WidgetType.FIELDS,
|
||||
pageLayoutTabId: 'tab-1',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
|
||||
configuration: null,
|
||||
objectMetadataId: null,
|
||||
...overrides,
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
const makeDraft = (tabs: DraftPageLayout['tabs']): DraftPageLayout =>
|
||||
({
|
||||
id: 'layout-1',
|
||||
name: 'Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: 'obj-1',
|
||||
tabs,
|
||||
}) as DraftPageLayout;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
): DraftPageLayout['tabs'][number] =>
|
||||
({
|
||||
id,
|
||||
title: 'Tab',
|
||||
position: 0,
|
||||
pageLayoutId: 'layout-1',
|
||||
applicationId: 'app-1',
|
||||
widgets,
|
||||
}) as DraftPageLayout['tabs'][number];
|
||||
|
||||
describe('convertPageLayoutDraftToUpdateInput', () => {
|
||||
it('should convert a draft layout to update input', () => {
|
||||
const widget = makeWidget({ id: 'w1' });
|
||||
const draft = makeDraft([makeTab('tab-1', [widget])]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft);
|
||||
|
||||
expect(result.name).toBe('Layout');
|
||||
expect(result.type).toBe(PageLayoutType.RECORD_PAGE);
|
||||
expect(result.objectMetadataId).toBe('obj-1');
|
||||
expect(result.tabs).toHaveLength(1);
|
||||
expect(result.tabs[0].widgets).toHaveLength(1);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('w1');
|
||||
});
|
||||
|
||||
it('should filter out dynamic relation widgets', () => {
|
||||
const regularWidget = makeWidget({ id: 'w1' });
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
type: WidgetType.VIEW,
|
||||
});
|
||||
|
||||
const draft = makeDraft([makeTab('tab-1', [regularWidget, dynamicWidget])]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(1);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('w1');
|
||||
});
|
||||
|
||||
it('should map gridPosition correctly', () => {
|
||||
const widget = makeWidget({
|
||||
id: 'w1',
|
||||
gridPosition: { row: 2, column: 3, rowSpan: 4, columnSpan: 5 },
|
||||
});
|
||||
const draft = makeDraft([makeTab('tab-1', [widget])]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft);
|
||||
|
||||
expect(result.tabs[0].widgets[0].gridPosition).toEqual({
|
||||
row: 2,
|
||||
column: 3,
|
||||
rowSpan: 4,
|
||||
columnSpan: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle objectMetadataId as null when not provided', () => {
|
||||
const draft = makeDraft([makeTab('tab-1', [])]);
|
||||
draft.objectMetadataId = undefined;
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft);
|
||||
|
||||
expect(result.objectMetadataId).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle multiple tabs', () => {
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [makeWidget({ id: 'w1' })]),
|
||||
makeTab('tab-2', [makeWidget({ id: 'w2' })]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft);
|
||||
|
||||
expect(result.tabs).toHaveLength(2);
|
||||
expect(result.tabs[0].id).toBe('tab-1');
|
||||
expect(result.tabs[1].id).toBe('tab-2');
|
||||
});
|
||||
|
||||
it('should keep all non-dynamic widgets when multiple widget types exist', () => {
|
||||
const fieldsWidget = makeWidget({ id: 'w1', type: WidgetType.FIELDS });
|
||||
const timelineWidget = makeWidget({
|
||||
id: 'w2',
|
||||
type: WidgetType.TIMELINE,
|
||||
});
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel`,
|
||||
});
|
||||
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
|
||||
});
|
||||
});
|
||||
+6
-3
@@ -1,6 +1,9 @@
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
import { filterVisibleWidgets } from '@/page-layout/utils/filterVisibleWidgets';
|
||||
import {
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('filterVisibleWidgets', () => {
|
||||
const createMockWidget = (
|
||||
@@ -22,8 +25,8 @@ describe('filterVisibleWidgets', () => {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId';
|
||||
import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId';
|
||||
import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId';
|
||||
import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId';
|
||||
import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId';
|
||||
import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId';
|
||||
import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId';
|
||||
|
||||
describe('getDefaultRecordPageLayoutId', () => {
|
||||
it('should return Company layout id for Company object', () => {
|
||||
expect(
|
||||
getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: CoreObjectNameSingular.Company,
|
||||
}),
|
||||
).toBe(DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID);
|
||||
});
|
||||
|
||||
it('should return Person layout id for Person object', () => {
|
||||
expect(
|
||||
getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: CoreObjectNameSingular.Person,
|
||||
}),
|
||||
).toBe(DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID);
|
||||
});
|
||||
|
||||
it('should return Opportunity layout id for Opportunity object', () => {
|
||||
expect(
|
||||
getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: CoreObjectNameSingular.Opportunity,
|
||||
}),
|
||||
).toBe(DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID);
|
||||
});
|
||||
|
||||
it('should return Note layout id for Note object', () => {
|
||||
expect(
|
||||
getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: CoreObjectNameSingular.Note,
|
||||
}),
|
||||
).toBe(DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID);
|
||||
});
|
||||
|
||||
it('should return Task layout id for Task object', () => {
|
||||
expect(
|
||||
getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: CoreObjectNameSingular.Task,
|
||||
}),
|
||||
).toBe(DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID);
|
||||
});
|
||||
|
||||
it('should return Workflow layout id for Workflow object', () => {
|
||||
expect(
|
||||
getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
}),
|
||||
).toBe(DEFAULT_WORKFLOW_PAGE_LAYOUT_ID);
|
||||
});
|
||||
|
||||
it('should return WorkflowVersion layout id for WorkflowVersion object', () => {
|
||||
expect(
|
||||
getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
}),
|
||||
).toBe(DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID);
|
||||
});
|
||||
|
||||
it('should return WorkflowRun layout id for WorkflowRun object', () => {
|
||||
expect(
|
||||
getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: CoreObjectNameSingular.WorkflowRun,
|
||||
}),
|
||||
).toBe(DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID);
|
||||
});
|
||||
|
||||
it('should return default record page layout id for unknown object', () => {
|
||||
expect(
|
||||
getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: 'customObject',
|
||||
}),
|
||||
).toBe(DEFAULT_RECORD_PAGE_LAYOUT_ID);
|
||||
});
|
||||
});
|
||||
+6
-3
@@ -1,6 +1,9 @@
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
import { getTabsWithVisibleWidgets } from '@/page-layout/utils/getTabsWithVisibleWidgets';
|
||||
import {
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('getTabsWithVisibleWidgets', () => {
|
||||
const createMockWidget = (
|
||||
@@ -22,8 +25,8 @@ describe('getTabsWithVisibleWidgets', () => {
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import {
|
||||
DYNAMIC_RELATION_WIDGET_ID_PREFIX,
|
||||
isDynamicRelationWidget,
|
||||
} from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
|
||||
const makeWidget = (id: string): PageLayoutWidget =>
|
||||
({
|
||||
id,
|
||||
title: 'Widget',
|
||||
type: 'FIELDS',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
|
||||
configuration: {},
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
describe('isDynamicRelationWidget', () => {
|
||||
it('should return true for a widget with the dynamic relation prefix', () => {
|
||||
const widget = makeWidget(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}some-relation`,
|
||||
);
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a widget without the dynamic relation prefix', () => {
|
||||
const widget = makeWidget('regular-widget-id');
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a widget with a similar but different prefix', () => {
|
||||
const widget = makeWidget('dynamic-relation-other-widget-123');
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for the prefix alone as id', () => {
|
||||
const widget = makeWidget(DYNAMIC_RELATION_WIDGET_ID_PREFIX);
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(true);
|
||||
});
|
||||
});
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
const makeWidget = (
|
||||
overrides: Partial<PageLayoutWidget> & { id: string },
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
title: 'Widget',
|
||||
type: WidgetType.FIELDS,
|
||||
pageLayoutTabId: 'tab-1',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
|
||||
configuration: {},
|
||||
...overrides,
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
const makeDynamicRelationWidget = (suffix: string): PageLayoutWidget =>
|
||||
makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}${suffix}`,
|
||||
type: WidgetType.VIEW,
|
||||
title: `Dynamic Relation ${suffix}`,
|
||||
});
|
||||
|
||||
const makeServerLayout = (tabs: PageLayout['tabs']): PageLayout =>
|
||||
({
|
||||
id: 'layout-1',
|
||||
name: 'Layout',
|
||||
type: 'RECORD_PAGE',
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
tabs,
|
||||
}) as PageLayout;
|
||||
|
||||
const makeDraftLayout = (tabs: DraftPageLayout['tabs']): DraftPageLayout =>
|
||||
({
|
||||
id: 'layout-1',
|
||||
name: 'Layout',
|
||||
type: 'RECORD_PAGE',
|
||||
tabs,
|
||||
}) as DraftPageLayout;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
): PageLayout['tabs'][number] =>
|
||||
({
|
||||
id,
|
||||
title: 'Tab',
|
||||
position: 0,
|
||||
pageLayoutId: 'layout-1',
|
||||
applicationId: 'app-1',
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
widgets,
|
||||
}) as PageLayout['tabs'][number];
|
||||
|
||||
describe('reInjectDynamicRelationWidgetsFromDraft', () => {
|
||||
it('should return server layout unchanged when draft has no dynamic widgets', () => {
|
||||
const serverLayout = makeServerLayout([
|
||||
makeTab('tab-1', [makeWidget({ id: 'w1' })]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [makeWidget({ id: 'w1' })]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
|
||||
|
||||
expect(result).toBe(serverLayout);
|
||||
});
|
||||
|
||||
it('should inject dynamic widgets after the first FIELDS widget', () => {
|
||||
const fieldsWidget = makeWidget({
|
||||
id: 'fields-1',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const timelineWidget = makeWidget({
|
||||
id: 'timeline-1',
|
||||
type: WidgetType.TIMELINE,
|
||||
});
|
||||
const dynamicWidget = makeDynamicRelationWidget('relation-1');
|
||||
|
||||
const serverLayout = makeServerLayout([
|
||||
makeTab('tab-1', [fieldsWidget, timelineWidget]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
);
|
||||
expect(result.tabs[0].widgets[2].id).toBe('timeline-1');
|
||||
});
|
||||
|
||||
it('should append dynamic widgets if no FIELDS widget exists in the tab', () => {
|
||||
const timelineWidget = makeWidget({
|
||||
id: 'timeline-1',
|
||||
type: WidgetType.TIMELINE,
|
||||
});
|
||||
const dynamicWidget = makeDynamicRelationWidget('relation-1');
|
||||
|
||||
const serverLayout = makeServerLayout([makeTab('tab-1', [timelineWidget])]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [timelineWidget, dynamicWidget]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('timeline-1');
|
||||
expect(result.tabs[0].widgets[1].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple tabs independently', () => {
|
||||
const fieldsWidget1 = makeWidget({
|
||||
id: 'fields-1',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const fieldsWidget2 = makeWidget({
|
||||
id: 'fields-2',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const dynamicWidget = makeDynamicRelationWidget('rel-1');
|
||||
|
||||
const serverLayout = makeServerLayout([
|
||||
makeTab('tab-1', [fieldsWidget1]),
|
||||
makeTab('tab-2', [fieldsWidget2]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [fieldsWidget1, dynamicWidget]),
|
||||
makeTab('tab-2', [fieldsWidget2]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
|
||||
|
||||
// Tab 1 should have dynamic widget injected
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
// Tab 2 should remain unchanged
|
||||
expect(result.tabs[1].widgets).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should inject multiple dynamic widgets', () => {
|
||||
const fieldsWidget = makeWidget({
|
||||
id: 'fields-1',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const dynamicWidget1 = makeDynamicRelationWidget('rel-1');
|
||||
const dynamicWidget2 = makeDynamicRelationWidget('rel-2');
|
||||
|
||||
const serverLayout = makeServerLayout([makeTab('tab-1', [fieldsWidget])]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [fieldsWidget, dynamicWidget1, dynamicWidget2]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel-1`,
|
||||
);
|
||||
expect(result.tabs[0].widgets[2].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel-2`,
|
||||
);
|
||||
});
|
||||
});
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
|
||||
|
||||
describe('sortTabsByPosition', () => {
|
||||
it('should sort tabs by position ascending', () => {
|
||||
const tabs = [
|
||||
{ id: 'c', position: 3 },
|
||||
{ id: 'a', position: 1 },
|
||||
{ id: 'b', position: 2 },
|
||||
];
|
||||
|
||||
const result = sortTabsByPosition(tabs);
|
||||
|
||||
expect(result.map((t) => t.id)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should return empty array for empty input', () => {
|
||||
expect(sortTabsByPosition([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not mutate the original array', () => {
|
||||
const tabs = [
|
||||
{ id: 'b', position: 2 },
|
||||
{ id: 'a', position: 1 },
|
||||
];
|
||||
|
||||
sortTabsByPosition(tabs);
|
||||
|
||||
expect(tabs[0].id).toBe('b');
|
||||
});
|
||||
|
||||
it('should handle single element', () => {
|
||||
const tabs = [{ id: 'a', position: 0 }];
|
||||
|
||||
expect(sortTabsByPosition(tabs)).toEqual([{ id: 'a', position: 0 }]);
|
||||
});
|
||||
});
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
|
||||
import {
|
||||
type PageLayout,
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const makePageLayout = (overrides: Partial<PageLayout> = {}): PageLayout =>
|
||||
({
|
||||
__typename: 'PageLayout',
|
||||
id: 'layout-1',
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
tabs: [],
|
||||
...overrides,
|
||||
}) as PageLayout;
|
||||
|
||||
describe('transformPageLayout', () => {
|
||||
it('should default tabs to empty array when null', () => {
|
||||
const layout = makePageLayout({ tabs: null as any });
|
||||
|
||||
const result = transformPageLayout(layout);
|
||||
|
||||
expect(result.tabs).toEqual([]);
|
||||
});
|
||||
|
||||
it('should sort tabs by position', () => {
|
||||
const layout = makePageLayout({
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-2',
|
||||
position: 2,
|
||||
title: 'Second',
|
||||
pageLayoutId: 'layout-1',
|
||||
applicationId: 'app-1',
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
},
|
||||
{
|
||||
id: 'tab-1',
|
||||
position: 1,
|
||||
title: 'First',
|
||||
pageLayoutId: 'layout-1',
|
||||
applicationId: 'app-1',
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
},
|
||||
] as any,
|
||||
});
|
||||
|
||||
const result = transformPageLayout(layout);
|
||||
|
||||
expect(result.tabs[0].id).toBe('tab-1');
|
||||
expect(result.tabs[1].id).toBe('tab-2');
|
||||
});
|
||||
|
||||
it('should default widgets to empty array when null', () => {
|
||||
const layout = makePageLayout({
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
position: 0,
|
||||
title: 'Tab',
|
||||
pageLayoutId: 'layout-1',
|
||||
applicationId: 'app-1',
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
widgets: null,
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
},
|
||||
] as any,
|
||||
});
|
||||
|
||||
const result = transformPageLayout(layout);
|
||||
|
||||
expect(result.tabs[0].widgets).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve existing widgets', () => {
|
||||
const widget = {
|
||||
id: 'w1',
|
||||
title: 'Widget',
|
||||
type: 'FIELDS',
|
||||
};
|
||||
|
||||
const layout = makePageLayout({
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
position: 0,
|
||||
title: 'Tab',
|
||||
pageLayoutId: 'layout-1',
|
||||
applicationId: 'app-1',
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
widgets: [widget],
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
},
|
||||
] as any,
|
||||
});
|
||||
|
||||
const result = transformPageLayout(layout);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(1);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('w1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const computeFieldsWidgetFieldDiff = (
|
||||
persistedGroups: FieldsWidgetGroup[],
|
||||
draftGroups: FieldsWidgetGroup[],
|
||||
) => {
|
||||
const fieldUpdates: Array<{
|
||||
viewFieldId: string;
|
||||
isVisible?: boolean;
|
||||
position?: number;
|
||||
viewFieldGroupId?: string;
|
||||
}> = [];
|
||||
|
||||
const persistedFieldMap = new Map<
|
||||
string,
|
||||
{
|
||||
isVisible: boolean;
|
||||
position: number;
|
||||
groupId: string;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const group of persistedGroups) {
|
||||
for (const field of group.fields) {
|
||||
if (isDefined(field.viewFieldId)) {
|
||||
persistedFieldMap.set(field.viewFieldId, {
|
||||
isVisible: field.isVisible,
|
||||
position: field.position,
|
||||
groupId: group.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const draftGroup of draftGroups) {
|
||||
for (const draftField of draftGroup.fields) {
|
||||
if (!isDefined(draftField.viewFieldId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const persistedField = persistedFieldMap.get(draftField.viewFieldId);
|
||||
|
||||
if (!isDefined(persistedField)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasVisibilityChange =
|
||||
persistedField.isVisible !== draftField.isVisible;
|
||||
const hasPositionChange = persistedField.position !== draftField.position;
|
||||
const hasGroupChange = persistedField.groupId !== draftGroup.id;
|
||||
|
||||
if (hasVisibilityChange || hasPositionChange || hasGroupChange) {
|
||||
fieldUpdates.push({
|
||||
viewFieldId: draftField.viewFieldId,
|
||||
...(hasVisibilityChange ? { isVisible: draftField.isVisible } : {}),
|
||||
...(hasPositionChange ? { position: draftField.position } : {}),
|
||||
...(hasGroupChange ? { viewFieldGroupId: draftGroup.id } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fieldUpdates;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const computeFieldsWidgetGroupDiff = (
|
||||
persistedGroups: FieldsWidgetGroup[],
|
||||
draftGroups: FieldsWidgetGroup[],
|
||||
) => {
|
||||
const persistedGroupIds = new Set(persistedGroups.map((g) => g.id));
|
||||
const draftGroupIds = new Set(draftGroups.map((g) => g.id));
|
||||
|
||||
const createdGroups = draftGroups.filter((g) => !persistedGroupIds.has(g.id));
|
||||
|
||||
const deletedGroups = persistedGroups.filter((g) => !draftGroupIds.has(g.id));
|
||||
|
||||
const updatedGroups = draftGroups.filter((draftGroup) => {
|
||||
if (!persistedGroupIds.has(draftGroup.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const persistedGroup = persistedGroups.find((g) => g.id === draftGroup.id);
|
||||
|
||||
if (!isDefined(persistedGroup)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
persistedGroup.name !== draftGroup.name ||
|
||||
persistedGroup.position !== draftGroup.position ||
|
||||
persistedGroup.isVisible !== draftGroup.isVisible
|
||||
);
|
||||
});
|
||||
|
||||
return { createdGroups, deletedGroups, updatedGroups };
|
||||
};
|
||||
+17
-14
@@ -1,4 +1,5 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { type UpdatePageLayoutWithTabsInput } from '~/generated-metadata/graphql';
|
||||
|
||||
export const convertPageLayoutDraftToUpdateInput = (
|
||||
@@ -12,20 +13,22 @@ export const convertPageLayoutDraftToUpdateInput = (
|
||||
id: tab.id,
|
||||
title: tab.title,
|
||||
position: tab.position,
|
||||
widgets: tab.widgets.map((widget) => ({
|
||||
id: widget.id,
|
||||
pageLayoutTabId: widget.pageLayoutTabId,
|
||||
title: widget.title,
|
||||
type: widget.type,
|
||||
objectMetadataId: widget.objectMetadataId ?? null,
|
||||
gridPosition: {
|
||||
row: widget.gridPosition.row,
|
||||
column: widget.gridPosition.column,
|
||||
rowSpan: widget.gridPosition.rowSpan,
|
||||
columnSpan: widget.gridPosition.columnSpan,
|
||||
},
|
||||
configuration: widget.configuration ?? null,
|
||||
})),
|
||||
widgets: tab.widgets
|
||||
.filter((widget) => !isDynamicRelationWidget(widget))
|
||||
.map((widget) => ({
|
||||
id: widget.id,
|
||||
pageLayoutTabId: widget.pageLayoutTabId,
|
||||
title: widget.title,
|
||||
type: widget.type,
|
||||
objectMetadataId: widget.objectMetadataId ?? null,
|
||||
gridPosition: {
|
||||
row: widget.gridPosition.row,
|
||||
column: widget.gridPosition.column,
|
||||
rowSpan: widget.gridPosition.rowSpan,
|
||||
columnSpan: widget.gridPosition.columnSpan,
|
||||
},
|
||||
configuration: widget.configuration ?? null,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId';
|
||||
import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId';
|
||||
import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId';
|
||||
import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId';
|
||||
import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId';
|
||||
import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId';
|
||||
|
||||
const OBJECT_NAME_TO_DEFAULT_LAYOUT_ID: Record<string, string> = {
|
||||
[CoreObjectNameSingular.Company]: DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Person]: DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Opportunity]:
|
||||
DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Note]: DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Task]: DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Workflow]: DEFAULT_WORKFLOW_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.WorkflowVersion]:
|
||||
DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.WorkflowRun]: DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID,
|
||||
};
|
||||
|
||||
export const getDefaultRecordPageLayoutId = ({
|
||||
targetObjectNameSingular,
|
||||
}: {
|
||||
targetObjectNameSingular: string;
|
||||
}): string => {
|
||||
return (
|
||||
OBJECT_NAME_TO_DEFAULT_LAYOUT_ID[targetObjectNameSingular] ??
|
||||
DEFAULT_RECORD_PAGE_LAYOUT_ID
|
||||
);
|
||||
};
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
WidgetConfigurationType,
|
||||
@@ -12,7 +13,7 @@ const getRelationFieldWidgetToInsert = (
|
||||
tabId: string,
|
||||
): PageLayoutWidget => ({
|
||||
__typename: 'PageLayoutWidget' as const,
|
||||
id: `dynamic-relation-widget-${field.id}-${field.label}`,
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}${field.id}-${field.label}`,
|
||||
pageLayoutTabId: tabId,
|
||||
title: field.label,
|
||||
type: WidgetType.FIELD,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
|
||||
export const DYNAMIC_RELATION_WIDGET_ID_PREFIX = 'dynamic-relation-widget-';
|
||||
|
||||
export const isDynamicRelationWidget = (widget: PageLayoutWidget): boolean => {
|
||||
return widget.id.startsWith(DYNAMIC_RELATION_WIDGET_ID_PREFIX);
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const reInjectDynamicRelationWidgetsFromDraft = (
|
||||
serverLayout: PageLayout,
|
||||
previousDraft: DraftPageLayout,
|
||||
): PageLayout => {
|
||||
const dynamicWidgetsByTabId = new Map(
|
||||
previousDraft.tabs.map((tab) => [
|
||||
tab.id,
|
||||
tab.widgets.filter(isDynamicRelationWidget),
|
||||
]),
|
||||
);
|
||||
|
||||
const hasDynamicWidgets = [...dynamicWidgetsByTabId.values()].some(
|
||||
(widgets) => widgets.length > 0,
|
||||
);
|
||||
|
||||
if (!hasDynamicWidgets) {
|
||||
return serverLayout;
|
||||
}
|
||||
|
||||
return {
|
||||
...serverLayout,
|
||||
tabs: serverLayout.tabs.map((tab) => {
|
||||
const relationWidgets = dynamicWidgetsByTabId.get(tab.id);
|
||||
|
||||
if (!relationWidgets || relationWidgets.length === 0) {
|
||||
return tab;
|
||||
}
|
||||
|
||||
const firstFieldsWidgetIndex = tab.widgets.findIndex(
|
||||
(widget) => widget.type === WidgetType.FIELDS,
|
||||
);
|
||||
|
||||
if (firstFieldsWidgetIndex === -1) {
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...tab.widgets, ...relationWidgets],
|
||||
};
|
||||
}
|
||||
|
||||
const widgetsBefore = tab.widgets.slice(0, firstFieldsWidgetIndex + 1);
|
||||
const widgetsAfter = tab.widgets.slice(firstFieldsWidgetIndex + 1);
|
||||
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...widgetsBefore, ...relationWidgets, ...widgetsAfter],
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -10,11 +10,8 @@ export const transformPageLayout = (
|
||||
tabs: (pageLayout.tabs ?? [])
|
||||
.toSorted((a, b) => a.position - b.position)
|
||||
.map((tab): PageLayoutTab => {
|
||||
// TODO: remove this once the frontend consumes the new type
|
||||
const { layoutMode: _layoutMode, ...tabWithoutLayoutMode } = tab;
|
||||
|
||||
return {
|
||||
...tabWithoutLayoutMode,
|
||||
...tab,
|
||||
widgets: tab.widgets ?? [],
|
||||
};
|
||||
}),
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { assertFieldWidgetOrThrow } from '@/page-layout/widgets/field/utils/assertFieldWidgetOrThrow';
|
||||
import {
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('assertFieldWidgetOrThrow', () => {
|
||||
it('should not throw for a valid field widget', () => {
|
||||
const widget = {
|
||||
id: 'w1',
|
||||
type: WidgetType.FIELD,
|
||||
configuration: {
|
||||
__typename: 'FieldConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: 'fm-1',
|
||||
layout: 'FIELD',
|
||||
},
|
||||
} as PageLayoutWidget;
|
||||
|
||||
expect(() => assertFieldWidgetOrThrow(widget)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw when configuration is undefined', () => {
|
||||
const widget = {
|
||||
id: 'w1',
|
||||
type: WidgetType.FIELD,
|
||||
configuration: undefined,
|
||||
} as unknown as PageLayoutWidget;
|
||||
|
||||
expect(() => assertFieldWidgetOrThrow(widget)).toThrow(
|
||||
'Widget configuration is required',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when configuration typename does not match', () => {
|
||||
const widget = {
|
||||
id: 'w1',
|
||||
type: WidgetType.FIELD,
|
||||
configuration: {
|
||||
__typename: 'WidgetConfiguration',
|
||||
},
|
||||
} as unknown as PageLayoutWidget;
|
||||
|
||||
expect(() => assertFieldWidgetOrThrow(widget)).toThrow(
|
||||
'Expected FieldConfiguration but got WidgetConfiguration',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when configuration typename is empty string', () => {
|
||||
const widget = {
|
||||
id: 'w1',
|
||||
type: WidgetType.FIELD,
|
||||
configuration: {
|
||||
__typename: '',
|
||||
},
|
||||
} as unknown as PageLayoutWidget;
|
||||
|
||||
expect(() => assertFieldWidgetOrThrow(widget)).toThrow();
|
||||
});
|
||||
});
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { generateFieldWidgetInstanceId } from '@/page-layout/widgets/field/utils/generateFieldWidgetInstanceId';
|
||||
|
||||
describe('generateFieldWidgetInstanceId', () => {
|
||||
it('should generate an instance id without right drawer suffix', () => {
|
||||
const result = generateFieldWidgetInstanceId({
|
||||
widgetId: 'widget-1',
|
||||
recordId: 'record-1',
|
||||
fieldName: 'name',
|
||||
isInRightDrawer: false,
|
||||
});
|
||||
|
||||
expect(result).toBe('widget-1-field-widget-record-1-name');
|
||||
});
|
||||
|
||||
it('should append right drawer suffix when isInRightDrawer is true', () => {
|
||||
const result = generateFieldWidgetInstanceId({
|
||||
widgetId: 'widget-1',
|
||||
recordId: 'record-1',
|
||||
fieldName: 'name',
|
||||
isInRightDrawer: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('widget-1-field-widget-record-1-name-right-drawer');
|
||||
});
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
|
||||
import {
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('isFieldWidget', () => {
|
||||
it('should return true for a FIELD widget with FieldConfiguration', () => {
|
||||
const widget = {
|
||||
id: 'w1',
|
||||
type: WidgetType.FIELD,
|
||||
configuration: {
|
||||
__typename: 'FieldConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: 'fm-1',
|
||||
layout: 'FIELD',
|
||||
},
|
||||
} as PageLayoutWidget;
|
||||
|
||||
expect(isFieldWidget(widget)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a non-FIELD widget type', () => {
|
||||
const widget = {
|
||||
id: 'w1',
|
||||
type: WidgetType.FIELDS,
|
||||
configuration: {
|
||||
__typename: 'FieldConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: 'fm-1',
|
||||
layout: 'FIELD',
|
||||
},
|
||||
} as PageLayoutWidget;
|
||||
|
||||
expect(isFieldWidget(widget)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when configuration is null', () => {
|
||||
const widget = {
|
||||
id: 'w1',
|
||||
type: WidgetType.FIELD,
|
||||
configuration: null,
|
||||
} as unknown as PageLayoutWidget;
|
||||
|
||||
expect(isFieldWidget(widget)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when configuration typename does not match', () => {
|
||||
const widget = {
|
||||
id: 'w1',
|
||||
type: WidgetType.FIELD,
|
||||
configuration: {
|
||||
__typename: 'WidgetConfiguration',
|
||||
},
|
||||
} as unknown as PageLayoutWidget;
|
||||
|
||||
expect(isFieldWidget(widget)).toBe(false);
|
||||
});
|
||||
});
|
||||
+73
-123
@@ -7,43 +7,56 @@ import {
|
||||
} from '@hello-pangea/dnd';
|
||||
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
type FieldsConfigurationFieldItem,
|
||||
type FieldsConfigurationSection,
|
||||
} from '@/page-layout/types/FieldsConfiguration';
|
||||
import { FieldsConfigurationSectionEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor';
|
||||
import { FieldsConfigurationGroupEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor';
|
||||
import { useCreateFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useCreateFieldsWidgetEditorGroup';
|
||||
import { useFieldsWidgetGroupsDraft } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsDraft';
|
||||
import { useMoveFieldInDraft } from '@/page-layout/widgets/fields/hooks/useMoveFieldInDraft';
|
||||
import { useReorderFieldsWidgetEditorGroups } from '@/page-layout/widgets/fields/hooks/useReorderFieldsWidgetEditorGroups';
|
||||
import { useToggleFieldVisibilityInDraft } from '@/page-layout/widgets/fields/hooks/useToggleFieldVisibilityInDraft';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
const StyledSectionsDroppable = styled.div`
|
||||
const StyledGroupsDroppable = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type FieldsConfigurationEditorProps = {
|
||||
configuration: FieldsConfiguration;
|
||||
onChange: (configuration: FieldsConfiguration) => void;
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const FieldsConfigurationEditor = ({
|
||||
configuration,
|
||||
onChange,
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: FieldsConfigurationEditorProps) => {
|
||||
const { t } = useLingui();
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const handleSectionChange = (
|
||||
sectionId: string,
|
||||
updatedSection: FieldsConfigurationSection,
|
||||
) => {
|
||||
const updatedSections = configuration.sections.map((section) =>
|
||||
section.id === sectionId ? updatedSection : section,
|
||||
);
|
||||
const { draftGroups } = useFieldsWidgetGroupsDraft({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
onChange({
|
||||
...configuration,
|
||||
sections: updatedSections,
|
||||
});
|
||||
};
|
||||
const { createGroup } = useCreateFieldsWidgetEditorGroup({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { reorderGroups } = useReorderFieldsWidgetEditorGroups({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { moveField } = useMoveFieldInDraft({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { toggleFieldVisibility } = useToggleFieldVisibilityInDraft({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
const { source, destination, type } = result;
|
||||
@@ -59,8 +72,8 @@ export const FieldsConfigurationEditor = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'SECTION') {
|
||||
handleSectionReorder(source.index, destination.index);
|
||||
if (type === 'GROUP') {
|
||||
handleGroupReorder(source.index, destination.index);
|
||||
} else if (type === 'FIELD') {
|
||||
handleFieldMove(
|
||||
source.droppableId,
|
||||
@@ -71,144 +84,81 @@ export const FieldsConfigurationEditor = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSectionReorder = (
|
||||
const handleGroupReorder = (
|
||||
sourceIndex: number,
|
||||
destinationIndex: number,
|
||||
) => {
|
||||
const sortedSections = [...configuration.sections].sort(
|
||||
const sortedGroups = [...draftGroups].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
const [movedSection] = sortedSections.splice(sourceIndex, 1);
|
||||
sortedSections.splice(destinationIndex, 0, movedSection);
|
||||
const reorderedGroupIds = sortedGroups.map((g) => g.id);
|
||||
const [movedGroupId] = reorderedGroupIds.splice(sourceIndex, 1);
|
||||
reorderedGroupIds.splice(destinationIndex, 0, movedGroupId);
|
||||
|
||||
const updatedSections = sortedSections.map((section, index) => ({
|
||||
...section,
|
||||
position: index,
|
||||
}));
|
||||
|
||||
onChange({
|
||||
...configuration,
|
||||
sections: updatedSections,
|
||||
});
|
||||
reorderGroups(reorderedGroupIds);
|
||||
};
|
||||
|
||||
const handleFieldMove = (
|
||||
sourceSectionId: string,
|
||||
destinationSectionId: string,
|
||||
sourceGroupId: string,
|
||||
destinationGroupId: string,
|
||||
sourceIndex: number,
|
||||
destinationIndex: number,
|
||||
) => {
|
||||
const sourceSection = configuration.sections.find(
|
||||
(s) => `section-${s.id}` === sourceSectionId,
|
||||
const cleanSourceGroupId = sourceGroupId.replace('group-', '');
|
||||
const cleanDestinationGroupId = destinationGroupId.replace('group-', '');
|
||||
|
||||
moveField(
|
||||
cleanSourceGroupId,
|
||||
cleanDestinationGroupId,
|
||||
sourceIndex,
|
||||
destinationIndex,
|
||||
);
|
||||
const destinationSection = configuration.sections.find(
|
||||
(s) => `section-${s.id}` === destinationSectionId,
|
||||
);
|
||||
|
||||
if (!sourceSection || !destinationSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceSortedFields = [...sourceSection.fields].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
if (sourceSectionId === destinationSectionId) {
|
||||
// Reorder within the same section
|
||||
const [movedField] = sourceSortedFields.splice(sourceIndex, 1);
|
||||
sourceSortedFields.splice(destinationIndex, 0, movedField);
|
||||
|
||||
const updatedFields = sourceSortedFields.map((field, index) => ({
|
||||
...field,
|
||||
position: index,
|
||||
}));
|
||||
|
||||
handleSectionChange(sourceSection.id, {
|
||||
...sourceSection,
|
||||
fields: updatedFields,
|
||||
});
|
||||
} else {
|
||||
// Move field between sections
|
||||
const [movedField] = sourceSortedFields.splice(sourceIndex, 1);
|
||||
|
||||
const destinationSortedFields = [...destinationSection.fields].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
destinationSortedFields.splice(destinationIndex, 0, movedField);
|
||||
|
||||
const updatedSourceFields = sourceSortedFields.map(
|
||||
(field, index) =>
|
||||
({
|
||||
...field,
|
||||
position: index,
|
||||
}) satisfies FieldsConfigurationFieldItem,
|
||||
);
|
||||
|
||||
const updatedDestinationFields = destinationSortedFields.map(
|
||||
(field, index) =>
|
||||
({
|
||||
...field,
|
||||
position: index,
|
||||
}) satisfies FieldsConfigurationFieldItem,
|
||||
);
|
||||
|
||||
const updatedSections = configuration.sections.map((section) => {
|
||||
if (section.id === sourceSection.id) {
|
||||
return { ...section, fields: updatedSourceFields };
|
||||
}
|
||||
if (section.id === destinationSection.id) {
|
||||
return { ...section, fields: updatedDestinationFields };
|
||||
}
|
||||
return section;
|
||||
});
|
||||
|
||||
onChange({
|
||||
...configuration,
|
||||
sections: updatedSections,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const sortedSections = [...configuration.sections].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
const handleAddGroup = () => {
|
||||
const newGroupName = t`New Group`;
|
||||
createGroup(newGroupName);
|
||||
};
|
||||
|
||||
if (sortedSections.length === 0) {
|
||||
const sortedGroups = [...draftGroups].sort((a, b) => a.position - b.position);
|
||||
|
||||
if (sortedGroups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="sections" type="SECTION">
|
||||
<Droppable droppableId="groups" type="GROUP">
|
||||
{(provided) => (
|
||||
<StyledSectionsDroppable
|
||||
<StyledGroupsDroppable
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{sortedSections.map((section, index) => (
|
||||
{sortedGroups.map((group, index) => (
|
||||
<Draggable
|
||||
key={section.id}
|
||||
draggableId={`section-draggable-${section.id}`}
|
||||
key={group.id}
|
||||
draggableId={`group-draggable-${group.id}`}
|
||||
index={index}
|
||||
>
|
||||
{(draggableProvided, snapshot) => (
|
||||
<FieldsConfigurationSectionEditor
|
||||
section={section}
|
||||
<FieldsConfigurationGroupEditor
|
||||
group={group}
|
||||
index={index}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
onSectionChange={(updatedSection) =>
|
||||
handleSectionChange(section.id, updatedSection)
|
||||
}
|
||||
draggableProvided={draggableProvided}
|
||||
isDragging={snapshot.isDragging}
|
||||
onAddGroup={handleAddGroup}
|
||||
onToggleFieldVisibility={(fieldMetadataId) =>
|
||||
toggleFieldVisibility(group.id, fieldMetadataId)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</StyledSectionsDroppable>
|
||||
</StyledGroupsDroppable>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export const FieldsConfigurationFieldEditor = ({
|
||||
onToggleVisibility,
|
||||
}: FieldsConfigurationFieldEditorProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const isVisible = field.conditionalDisplay !== false;
|
||||
const isVisible = field.isVisible ?? true;
|
||||
const FieldIcon = getIcon(fieldMetadata.icon);
|
||||
|
||||
return (
|
||||
|
||||
+30
-69
@@ -5,11 +5,8 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
type FieldsConfigurationFieldItem,
|
||||
type FieldsConfigurationSection,
|
||||
} from '@/page-layout/types/FieldsConfiguration';
|
||||
import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor';
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { IconDotsVertical, IconNewSection } from 'twenty-ui/display';
|
||||
import { MenuItem, MenuItemDraggable } from 'twenty-ui/navigation';
|
||||
|
||||
@@ -18,7 +15,7 @@ const StyledFieldsDroppable = styled.div`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledSectionContainer = styled.div<{ isDragging: boolean }>`
|
||||
const StyledGroupContainer = styled.div<{ isDragging: boolean }>`
|
||||
background: ${({ isDragging, theme }) =>
|
||||
isDragging ? theme.background.primary : 'transparent'};
|
||||
border: 1px solid
|
||||
@@ -30,61 +27,31 @@ const StyledSectionContainer = styled.div<{ isDragging: boolean }>`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type FieldsConfigurationSectionEditorProps = {
|
||||
section: FieldsConfigurationSection;
|
||||
type FieldsConfigurationGroupEditorProps = {
|
||||
group: FieldsWidgetGroup;
|
||||
index: number;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
onSectionChange: (section: FieldsConfigurationSection) => void;
|
||||
draggableProvided: DraggableProvided;
|
||||
isDragging: boolean;
|
||||
onAddGroup?: () => void;
|
||||
onToggleFieldVisibility: (fieldMetadataId: string) => void;
|
||||
};
|
||||
|
||||
export const FieldsConfigurationSectionEditor = ({
|
||||
section,
|
||||
objectMetadataItem,
|
||||
onSectionChange,
|
||||
export const FieldsConfigurationGroupEditor = ({
|
||||
group,
|
||||
draggableProvided,
|
||||
isDragging,
|
||||
}: FieldsConfigurationSectionEditorProps) => {
|
||||
onAddGroup,
|
||||
onToggleFieldVisibility,
|
||||
}: FieldsConfigurationGroupEditorProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const handleFieldChange = (
|
||||
fieldMetadataId: string,
|
||||
updatedField: FieldsConfigurationFieldItem,
|
||||
) => {
|
||||
const updatedFields = section.fields.map((field) =>
|
||||
field.fieldMetadataId === fieldMetadataId ? updatedField : field,
|
||||
);
|
||||
|
||||
onSectionChange({
|
||||
...section,
|
||||
fields: updatedFields,
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleFieldVisibility = (fieldMetadataId: string) => {
|
||||
const field = section.fields.find(
|
||||
(f) => f.fieldMetadataId === fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedField: FieldsConfigurationFieldItem = {
|
||||
...field,
|
||||
conditionalDisplay: false,
|
||||
};
|
||||
|
||||
handleFieldChange(fieldMetadataId, updatedField);
|
||||
};
|
||||
|
||||
const sortedFields = [...section.fields].sort(
|
||||
const sortedFields = [...group.fields].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledSectionContainer
|
||||
<StyledGroupContainer
|
||||
ref={draggableProvided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided.draggableProps}
|
||||
@@ -93,7 +60,7 @@ export const FieldsConfigurationSectionEditor = ({
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<div {...draggableProvided.dragHandleProps}>
|
||||
<MenuItemDraggable
|
||||
text={section.title}
|
||||
text={group.name}
|
||||
gripMode="always"
|
||||
isIconDisplayedOnHoverOnly={false}
|
||||
withIconContainer
|
||||
@@ -102,14 +69,14 @@ export const FieldsConfigurationSectionEditor = ({
|
||||
Icon: IconDotsVertical,
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
// TODO: Add section menu
|
||||
// TODO: Add group menu
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Droppable droppableId={`section-${section.id}`} type="FIELD">
|
||||
<Droppable droppableId={`group-${group.id}`} type="FIELD">
|
||||
{(droppableProvided) => (
|
||||
<StyledFieldsDroppable
|
||||
ref={droppableProvided.innerRef}
|
||||
@@ -117,27 +84,23 @@ export const FieldsConfigurationSectionEditor = ({
|
||||
{...droppableProvided.droppableProps}
|
||||
>
|
||||
{sortedFields.map((field, fieldIndex) => {
|
||||
const fieldMetadata = objectMetadataItem.fields.find(
|
||||
(f) => f.id === field.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DraggableItem
|
||||
key={field.fieldMetadataId}
|
||||
draggableId={`field-${field.fieldMetadataId}`}
|
||||
key={field.fieldMetadataItem.id}
|
||||
draggableId={`field-${field.fieldMetadataItem.id}`}
|
||||
index={fieldIndex}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<FieldsConfigurationFieldEditor
|
||||
field={field}
|
||||
fieldMetadata={fieldMetadata}
|
||||
onToggleVisibility={() =>
|
||||
handleToggleFieldVisibility(field.fieldMetadataId)
|
||||
}
|
||||
field={{
|
||||
fieldMetadataId: field.fieldMetadataItem.id,
|
||||
position: field.position,
|
||||
isVisible: field.isVisible,
|
||||
}}
|
||||
fieldMetadata={field.fieldMetadataItem}
|
||||
onToggleVisibility={() => {
|
||||
onToggleFieldVisibility(field.fieldMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -151,11 +114,9 @@ export const FieldsConfigurationSectionEditor = ({
|
||||
<MenuItem
|
||||
LeftIcon={IconNewSection}
|
||||
withIconContainer
|
||||
text={t`Add a Section`}
|
||||
onClick={() => {
|
||||
// TODO: Implement add section
|
||||
}}
|
||||
text={t`Add a Group`}
|
||||
onClick={onAddGroup}
|
||||
/>
|
||||
</StyledSectionContainer>
|
||||
</StyledGroupContainer>
|
||||
);
|
||||
};
|
||||
+102
-98
@@ -21,8 +21,8 @@ import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFi
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { FieldsWidgetCellEditModePortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal';
|
||||
import { FieldsWidgetCellHoveredPortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal';
|
||||
import { FieldsWidgetSectionContainer } from '@/page-layout/widgets/fields/components/FieldsWidgetSectionContainer';
|
||||
import { useFieldsWidgetSectionsWithIndices } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices';
|
||||
import { FieldsWidgetGroupContainer } from '@/page-layout/widgets/fields/components/FieldsWidgetGroupContainer';
|
||||
import { useFieldsWidgetGroupsForDisplay } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
||||
} from 'twenty-ui/layout';
|
||||
import { type FieldsConfiguration } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
@@ -64,7 +65,7 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
|
||||
const targetRecord = useTargetRecord();
|
||||
const { isInRightDrawer } = useLayoutRenderingContext();
|
||||
|
||||
const instanceId = `${widget.id}-${targetRecord.id}${isInRightDrawer ? '-right-drawer' : ''}`;
|
||||
const instanceId = `fields-${widget.id}-${targetRecord.id}${isInRightDrawer ? '-right-drawer' : ''}`;
|
||||
|
||||
const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({
|
||||
objectRecordId: targetRecord.id,
|
||||
@@ -91,11 +92,19 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { sectionsWithFieldIndices } = useFieldsWidgetSectionsWithIndices(
|
||||
targetRecord.targetObjectNameSingular,
|
||||
const fieldsConfiguration = widget.configuration as FieldsConfiguration;
|
||||
|
||||
const { groups } = useFieldsWidgetGroupsForDisplay({
|
||||
widgetId: widget.id,
|
||||
viewId: fieldsConfiguration.viewId ?? null,
|
||||
objectNameSingular: targetRecord.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const flattenedFieldMetadataItems = groups.flatMap((group) =>
|
||||
group.fields.map((field) => field.fieldMetadataItem),
|
||||
);
|
||||
|
||||
const hasFieldsToDisplay = sectionsWithFieldIndices.length > 0;
|
||||
const hasFieldsToDisplay = groups.length > 0;
|
||||
|
||||
if (!hasFieldsToDisplay) {
|
||||
return (
|
||||
@@ -128,118 +137,113 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
|
||||
instanceId,
|
||||
}}
|
||||
>
|
||||
{sectionsWithFieldIndices.map((section) => (
|
||||
<FieldsWidgetSectionContainer
|
||||
key={section.id}
|
||||
title={section.title}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
<FieldsWidgetGroupContainer key={group.id} title={group.name}>
|
||||
<StyledPropertyBox>
|
||||
{isPrefetchLoading ? (
|
||||
<PropertyBoxSkeletonLoader />
|
||||
) : (
|
||||
<>
|
||||
{section.fields.map(
|
||||
({ field: fieldMetadataItem, globalIndex }) => {
|
||||
const isActivityTarget = isActivityTargetField(
|
||||
fieldMetadataItem.name,
|
||||
targetRecord.targetObjectNameSingular,
|
||||
);
|
||||
{group.fields.map(({ fieldMetadataItem, globalIndex }) => {
|
||||
const isActivityTarget = isActivityTargetField(
|
||||
fieldMetadataItem.name,
|
||||
targetRecord.targetObjectNameSingular,
|
||||
);
|
||||
|
||||
return (
|
||||
<FieldContext.Provider
|
||||
key={targetRecord.id + fieldMetadataItem.id}
|
||||
value={{
|
||||
recordId: targetRecord.id,
|
||||
maxWidth: 200,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition:
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: globalIndex,
|
||||
objectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 90,
|
||||
return (
|
||||
<FieldContext.Provider
|
||||
key={targetRecord.id + fieldMetadataItem.id}
|
||||
value={{
|
||||
recordId: targetRecord.id,
|
||||
maxWidth: 200,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition:
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: globalIndex,
|
||||
objectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 90,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isDisplayModeFixHeight: true,
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
isRecordReadOnly,
|
||||
objectPermissions:
|
||||
getObjectPermissionsFromMapByObjectMetadataId({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isDisplayModeFixHeight: true,
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
isRecordReadOnly,
|
||||
objectPermissions:
|
||||
getObjectPermissionsFromMapByObjectMetadataId(
|
||||
{
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
},
|
||||
),
|
||||
fieldMetadataItem: {
|
||||
id: fieldMetadataItem.id,
|
||||
isUIReadOnly:
|
||||
fieldMetadataItem.isUIReadOnly ?? false,
|
||||
fieldMetadataItem: {
|
||||
id: fieldMetadataItem.id,
|
||||
isUIReadOnly:
|
||||
fieldMetadataItem.isUIReadOnly ?? false,
|
||||
},
|
||||
}),
|
||||
onMouseEnter: () =>
|
||||
setRecordFieldListHoverPosition(globalIndex),
|
||||
anchorId: `${getRecordFieldInputInstanceId({
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
})}`,
|
||||
isForbidden: isJunctionRelationForbidden({
|
||||
fieldMetadataItem,
|
||||
sourceObjectMetadataId: objectMetadataItem.id,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{isActivityTarget ? (
|
||||
<ActivityTargetsInlineCell
|
||||
componentInstanceId={getRecordFieldInputInstanceId(
|
||||
{
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
},
|
||||
}),
|
||||
onMouseEnter: () =>
|
||||
setRecordFieldListHoverPosition(globalIndex),
|
||||
anchorId: `${getRecordFieldInputInstanceId({
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
})}`,
|
||||
isForbidden: isJunctionRelationForbidden({
|
||||
fieldMetadataItem,
|
||||
sourceObjectMetadataId: objectMetadataItem.id,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{isActivityTarget ? (
|
||||
<ActivityTargetsInlineCell
|
||||
componentInstanceId={getRecordFieldInputInstanceId(
|
||||
{
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
},
|
||||
)}
|
||||
activityObjectNameSingular={
|
||||
targetRecord.targetObjectNameSingular as
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task
|
||||
}
|
||||
activityRecordId={targetRecord.id}
|
||||
showLabel={true}
|
||||
maxWidth={200}
|
||||
/>
|
||||
) : (
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: getRecordFieldInputInstanceId({
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell loading={recordLoading} />
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
)}
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
},
|
||||
)}
|
||||
)}
|
||||
activityObjectNameSingular={
|
||||
targetRecord.targetObjectNameSingular as
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task
|
||||
}
|
||||
activityRecordId={targetRecord.id}
|
||||
showLabel={true}
|
||||
maxWidth={200}
|
||||
/>
|
||||
) : (
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: getRecordFieldInputInstanceId({
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell loading={recordLoading} />
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
)}
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</StyledPropertyBox>
|
||||
</FieldsWidgetSectionContainer>
|
||||
</FieldsWidgetGroupContainer>
|
||||
))}
|
||||
|
||||
<FieldsWidgetCellHoveredPortal
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
recordId={targetRecord.id}
|
||||
flattenedFieldMetadataItems={flattenedFieldMetadataItems}
|
||||
/>
|
||||
<FieldsWidgetCellEditModePortal
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
recordId={targetRecord.id}
|
||||
flattenedFieldMetadataItems={flattenedFieldMetadataItems}
|
||||
/>
|
||||
</RecordFieldListComponentInstanceContext.Provider>
|
||||
</StyledContainer>
|
||||
|
||||
+3
-5
@@ -1,3 +1,4 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordFieldListInputContextProvider } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListInputContextProvider';
|
||||
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
|
||||
@@ -5,7 +6,6 @@ import { recordFieldListCellEditModePositionComponentState } from '@/object-reco
|
||||
import { FieldInput } from '@/object-record/record-field/ui/components/FieldInput';
|
||||
import { RecordInlineCellAnchoredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal';
|
||||
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
||||
import { useFieldsWidgetFlattenedFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -13,11 +13,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
type FieldsWidgetCellEditModePortalProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
recordId: string;
|
||||
flattenedFieldMetadataItems: FieldMetadataItem[];
|
||||
};
|
||||
|
||||
export const FieldsWidgetCellEditModePortal = ({
|
||||
objectMetadataItem,
|
||||
recordId,
|
||||
flattenedFieldMetadataItems,
|
||||
}: FieldsWidgetCellEditModePortalProps) => {
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldListComponentInstanceContext,
|
||||
@@ -27,10 +29,6 @@ export const FieldsWidgetCellEditModePortal = ({
|
||||
recordFieldListCellEditModePositionComponentState,
|
||||
);
|
||||
|
||||
const { flattenedFieldMetadataItems } = useFieldsWidgetFlattenedFields(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
const editedFieldMetadataItem = isDefined(editModePosition)
|
||||
? flattenedFieldMetadataItems.at(editModePosition)
|
||||
: undefined;
|
||||
|
||||
+3
-5
@@ -1,10 +1,10 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordFieldListCellHoveredPortalContent } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortalContent';
|
||||
import { RecordFieldListInputContextProvider } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListInputContextProvider';
|
||||
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
|
||||
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
|
||||
import { RecordInlineCellAnchoredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal';
|
||||
import { useFieldsWidgetFlattenedFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -12,11 +12,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
type FieldsWidgetCellHoveredPortalProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
recordId: string;
|
||||
flattenedFieldMetadataItems: FieldMetadataItem[];
|
||||
};
|
||||
|
||||
export const FieldsWidgetCellHoveredPortal = ({
|
||||
objectMetadataItem,
|
||||
recordId,
|
||||
flattenedFieldMetadataItems,
|
||||
}: FieldsWidgetCellHoveredPortalProps) => {
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldListComponentInstanceContext,
|
||||
@@ -26,10 +28,6 @@ export const FieldsWidgetCellHoveredPortal = ({
|
||||
recordFieldListHoverPositionComponentState,
|
||||
);
|
||||
|
||||
const { flattenedFieldMetadataItems } = useFieldsWidgetFlattenedFields(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
const hoveredFieldMetadataItem = isDefined(hoverPosition)
|
||||
? flattenedFieldMetadataItems.at(hoverPosition)
|
||||
: undefined;
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import isPropValid from '@emotion/is-prop-valid';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { IconChevronDown } from 'twenty-ui/display';
|
||||
@@ -28,24 +28,24 @@ const StyledChevronIcon = styled(IconChevronDown, {
|
||||
`transform ${theme.animation.duration.normal}s ease`};
|
||||
`;
|
||||
|
||||
type FieldsWidgetSectionContainerProps = {
|
||||
type FieldsWidgetGroupContainerProps = {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const FieldsWidgetSectionContainer = ({
|
||||
export const FieldsWidgetGroupContainer = ({
|
||||
children,
|
||||
title,
|
||||
}: FieldsWidgetSectionContainerProps) => {
|
||||
}: FieldsWidgetGroupContainerProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleToggleSection = () =>
|
||||
const handleToggleGroup = () =>
|
||||
setIsExpanded((previousIsExpanded) => !previousIsExpanded);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledHeader onClick={handleToggleSection}>
|
||||
<StyledHeader onClick={handleToggleGroup}>
|
||||
<StyledTitleLabel>{title}</StyledTitleLabel>
|
||||
<StyledChevronIcon
|
||||
isExpanded={isExpanded}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useFieldsWidgetEditorGroupsData } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData';
|
||||
import { useInitializeFieldsWidgetGroupsDraft } from '@/page-layout/widgets/fields/hooks/useInitializeFieldsWidgetGroupsDraft';
|
||||
|
||||
type FieldsWidgetGroupsDraftInitializationEffectProps = {
|
||||
viewId: string | null;
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const FieldsWidgetGroupsDraftInitializationEffect = ({
|
||||
viewId,
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: FieldsWidgetGroupsDraftInitializationEffectProps) => {
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const { groups: serverGroups } = useFieldsWidgetEditorGroupsData({
|
||||
viewId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
useInitializeFieldsWidgetGroupsDraft({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
serverGroups,
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
type UseCreateFieldsWidgetEditorGroupParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useCreateFieldsWidgetEditorGroup = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseCreateFieldsWidgetEditorGroupParams) => {
|
||||
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const createGroup = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(name: string) => {
|
||||
const allDraftGroups = snapshot
|
||||
.getLoadable(fieldsWidgetGroupsDraftState)
|
||||
.getValue();
|
||||
|
||||
const currentGroups = allDraftGroups[widgetId] ?? [];
|
||||
const maxPosition = Math.max(
|
||||
...currentGroups.map((g) => g.position),
|
||||
-1,
|
||||
);
|
||||
const newId = v4();
|
||||
|
||||
set(fieldsWidgetGroupsDraftState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: [
|
||||
...(prev[widgetId] ?? []),
|
||||
{
|
||||
id: newId,
|
||||
name,
|
||||
position: maxPosition + 1,
|
||||
isVisible: true,
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
return newId;
|
||||
},
|
||||
[fieldsWidgetGroupsDraftState, widgetId],
|
||||
);
|
||||
|
||||
const createGroupCallback = useCallback(
|
||||
(name: string) => createGroup(name),
|
||||
[createGroup],
|
||||
);
|
||||
|
||||
return { createGroup: createGroupCallback };
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
type UseDeleteFieldsWidgetEditorGroupParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useDeleteFieldsWidgetEditorGroup = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseDeleteFieldsWidgetEditorGroupParams) => {
|
||||
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const deleteGroupRecoilCallback = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(groupId: string) => {
|
||||
set(fieldsWidgetGroupsDraftState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: (prev[widgetId] ?? []).filter(
|
||||
(group) => group.id !== groupId,
|
||||
),
|
||||
}));
|
||||
},
|
||||
[fieldsWidgetGroupsDraftState, widgetId],
|
||||
);
|
||||
|
||||
const deleteGroup = useCallback(
|
||||
(groupId: string) => deleteGroupRecoilCallback(groupId),
|
||||
[deleteGroupRecoilCallback],
|
||||
);
|
||||
|
||||
return { deleteGroup };
|
||||
};
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { useGetViewById } from '@/views/hooks/useGetViewById';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseFieldsWidgetEditorGroupsDataParams = {
|
||||
viewId: string | null;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const useFieldsWidgetEditorGroupsData = ({
|
||||
viewId,
|
||||
objectNameSingular,
|
||||
}: UseFieldsWidgetEditorGroupsDataParams) => {
|
||||
const { t } = useLingui();
|
||||
const { view } = useGetViewById(viewId);
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const groups = useMemo<FieldsWidgetGroup[]>(() => {
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isDefined(view) && isDefined(view.viewFieldGroups)) {
|
||||
const viewFieldGroups = view.viewFieldGroups;
|
||||
|
||||
const sortedGroups = [...viewFieldGroups].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
let globalIndex = 0;
|
||||
|
||||
return sortedGroups.map((group) => {
|
||||
const groupFields = [...(group.viewFields ?? [])].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
const fields = groupFields
|
||||
.map((viewField) => {
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(f) => f.id === viewField.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fieldMetadataItem,
|
||||
position: viewField.position,
|
||||
isVisible: viewField.isVisible,
|
||||
globalIndex: globalIndex++,
|
||||
viewFieldId: viewField.id,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
position: group.position,
|
||||
isVisible: group.isVisible,
|
||||
fields,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(view) && view.viewFields.length > 0) {
|
||||
let globalIndex = 0;
|
||||
|
||||
const fields = [...view.viewFields]
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((viewField) => {
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(f) => f.id === viewField.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fieldMetadataItem,
|
||||
position: viewField.position,
|
||||
isVisible: viewField.isVisible,
|
||||
globalIndex: globalIndex++,
|
||||
viewFieldId: viewField.id,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (fields.length > 0) {
|
||||
return [
|
||||
{
|
||||
id: `${view.id}-group-general`,
|
||||
name: t`General`,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fields,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [objectMetadataItem, t, view]);
|
||||
|
||||
return {
|
||||
groups,
|
||||
isFromView:
|
||||
isDefined(view) &&
|
||||
(isDefined(view.viewFieldGroups) || view.viewFields.length > 0),
|
||||
};
|
||||
};
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import { useFieldsWidgetSectionsWithFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields';
|
||||
|
||||
export const useFieldsWidgetFlattenedFields = (objectNameSingular: string) => {
|
||||
const { sectionsWithFields } =
|
||||
useFieldsWidgetSectionsWithFields(objectNameSingular);
|
||||
|
||||
const flattenedFieldMetadataItems = sectionsWithFields.flatMap(
|
||||
(section) => section.fields,
|
||||
);
|
||||
|
||||
return { flattenedFieldMetadataItems };
|
||||
};
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useFieldsWidgetFieldMetadataItems } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems';
|
||||
import {
|
||||
type FieldsWidgetGroup,
|
||||
type FieldsWidgetGroupField,
|
||||
} from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { useGetViewById } from '@/views/hooks/useGetViewById';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseFieldsWidgetGroupsParams = {
|
||||
viewId: string | null;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const useFieldsWidgetGroups = ({
|
||||
viewId,
|
||||
objectNameSingular,
|
||||
}: UseFieldsWidgetGroupsParams) => {
|
||||
const { t } = useLingui();
|
||||
const { view } = useGetViewById(viewId);
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { inlineFieldMetadataItems } = useFieldsWidgetFieldMetadataItems({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const groups = useMemo<FieldsWidgetGroup[]>(() => {
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Views are seeded for object
|
||||
if (isDefined(view) && isDefined(view.viewFieldGroups)) {
|
||||
const sortedGroups = view.viewFieldGroups.toSorted(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
let globalIndex = 0;
|
||||
|
||||
return sortedGroups
|
||||
.filter((group) => group.isVisible)
|
||||
.map((group) => {
|
||||
const groupFields = [...(group.viewFields ?? [])].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
const fields: FieldsWidgetGroupField[] = groupFields
|
||||
.filter((field) => field.isVisible)
|
||||
.map((viewField) => {
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(f) => f.id === viewField.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const field: FieldsWidgetGroupField = {
|
||||
fieldMetadataItem,
|
||||
position: viewField.position,
|
||||
isVisible: viewField.isVisible,
|
||||
globalIndex: globalIndex++,
|
||||
};
|
||||
|
||||
return field;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
position: group.position,
|
||||
isVisible: group.isVisible,
|
||||
fields,
|
||||
};
|
||||
})
|
||||
.filter((group) => group.fields.length > 0);
|
||||
}
|
||||
|
||||
if (isDefined(view) && view.viewFields.length > 0) {
|
||||
let globalIndex = 0;
|
||||
|
||||
const fields: FieldsWidgetGroupField[] = [...view.viewFields]
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.filter((viewField) => viewField.isVisible)
|
||||
.map((viewField) => {
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(f) => f.id === viewField.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fieldMetadataItem,
|
||||
position: viewField.position,
|
||||
isVisible: viewField.isVisible,
|
||||
globalIndex: globalIndex++,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (fields.length > 0) {
|
||||
return [
|
||||
{
|
||||
id: `${view.id}-group-general`,
|
||||
name: t`General`,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fields,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const fieldsToDisplay = inlineFieldMetadataItems;
|
||||
|
||||
if (fieldsToDisplay.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const generalFields: Array<{
|
||||
fieldMetadataItem: (typeof fieldsToDisplay)[0];
|
||||
position: number;
|
||||
}> = [];
|
||||
const otherFields: Array<{
|
||||
fieldMetadataItem: (typeof fieldsToDisplay)[0];
|
||||
position: number;
|
||||
}> = [];
|
||||
|
||||
let generalPosition = 0;
|
||||
let otherPosition = 0;
|
||||
|
||||
fieldsToDisplay.forEach((field) => {
|
||||
if (field.isCustom === true) {
|
||||
otherFields.push({
|
||||
fieldMetadataItem: field,
|
||||
position: otherPosition++,
|
||||
});
|
||||
} else {
|
||||
generalFields.push({
|
||||
fieldMetadataItem: field,
|
||||
position: generalPosition++,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const groups: FieldsWidgetGroup[] = [];
|
||||
let globalIndex = 0;
|
||||
|
||||
if (generalFields.length > 0) {
|
||||
groups.push({
|
||||
id: `${objectNameSingular}-group-general`,
|
||||
name: t`General`,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fields: generalFields.map((field) => ({
|
||||
fieldMetadataItem: field.fieldMetadataItem,
|
||||
position: field.position,
|
||||
isVisible: true,
|
||||
globalIndex: globalIndex++,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (otherFields.length > 0) {
|
||||
groups.push({
|
||||
id: `${objectNameSingular}-group-other`,
|
||||
name: t`Other`,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
fields: otherFields.map((field) => ({
|
||||
fieldMetadataItem: field.fieldMetadataItem,
|
||||
position: field.position,
|
||||
isVisible: true,
|
||||
globalIndex: globalIndex++,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
t,
|
||||
view,
|
||||
inlineFieldMetadataItems,
|
||||
]);
|
||||
|
||||
return {
|
||||
groups,
|
||||
isFromView:
|
||||
isDefined(view) &&
|
||||
(isDefined(view.viewFieldGroups) || view.viewFields.length > 0),
|
||||
};
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type UseFieldsWidgetGroupsDraftParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useFieldsWidgetGroupsDraft = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseFieldsWidgetGroupsDraftParams): {
|
||||
draftGroups: FieldsWidgetGroup[];
|
||||
} => {
|
||||
const allDraftGroups = useRecoilComponentValue(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const draftGroups = useMemo(
|
||||
() => allDraftGroups[widgetId] ?? [],
|
||||
[allDraftGroups, widgetId],
|
||||
);
|
||||
|
||||
return { draftGroups };
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { filterDraftGroupsForDisplay } from '@/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseFieldsWidgetGroupsForDisplayParams = {
|
||||
widgetId: string;
|
||||
viewId: string | null;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const useFieldsWidgetGroupsForDisplay = ({
|
||||
widgetId,
|
||||
viewId,
|
||||
objectNameSingular,
|
||||
}: UseFieldsWidgetGroupsForDisplayParams) => {
|
||||
const isPageLayoutInEditMode = useRecoilComponentValue(
|
||||
isPageLayoutInEditModeComponentState,
|
||||
);
|
||||
|
||||
const allDraftGroups = useRecoilComponentValue(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
);
|
||||
|
||||
const viewGroups = useFieldsWidgetGroups({
|
||||
viewId,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const draftGroupsForWidget = allDraftGroups[widgetId];
|
||||
const hasDraftGroups =
|
||||
isDefined(draftGroupsForWidget) && draftGroupsForWidget.length > 0;
|
||||
|
||||
const groups = useMemo<FieldsWidgetGroup[]>(() => {
|
||||
if (isPageLayoutInEditMode && hasDraftGroups) {
|
||||
return filterDraftGroupsForDisplay(draftGroupsForWidget);
|
||||
}
|
||||
|
||||
return viewGroups.groups;
|
||||
}, [
|
||||
isPageLayoutInEditMode,
|
||||
hasDraftGroups,
|
||||
draftGroupsForWidget,
|
||||
viewGroups.groups,
|
||||
]);
|
||||
|
||||
return { groups };
|
||||
};
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration';
|
||||
import { buildWidgetVisibilityContext } from '@/page-layout/utils/buildWidgetVisibilityContext';
|
||||
import { useFieldsWidgetFieldMetadataItems } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems';
|
||||
import { filterAndOrderFieldsFromConfiguration } from '@/page-layout/widgets/fields/utils/filterAndOrderFieldsFromConfiguration';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
export const useFieldsWidgetSectionsWithFields = (
|
||||
objectNameSingular: string,
|
||||
) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { isInRightDrawer } = useLayoutRenderingContext();
|
||||
const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } =
|
||||
useFieldsWidgetFieldMetadataItems({
|
||||
objectNameSingular,
|
||||
});
|
||||
const temporaryConfiguration =
|
||||
useTemporaryFieldsConfiguration(objectNameSingular);
|
||||
|
||||
const context = buildWidgetVisibilityContext({ isMobile, isInRightDrawer });
|
||||
|
||||
const allFieldMetadataItems = [
|
||||
...legacyActivityTargetFieldMetadataItems,
|
||||
...inlineFieldMetadataItems,
|
||||
];
|
||||
|
||||
const sectionsWithFields = filterAndOrderFieldsFromConfiguration({
|
||||
configuration: temporaryConfiguration,
|
||||
availableFieldMetadataItems: allFieldMetadataItems,
|
||||
context,
|
||||
});
|
||||
|
||||
return { sectionsWithFields };
|
||||
};
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { useFieldsWidgetSectionsWithFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields';
|
||||
|
||||
export const useFieldsWidgetSectionsWithIndices = (
|
||||
objectNameSingular: string,
|
||||
) => {
|
||||
const { sectionsWithFields } =
|
||||
useFieldsWidgetSectionsWithFields(objectNameSingular);
|
||||
|
||||
const sectionsWithFieldIndices = sectionsWithFields.map(
|
||||
(section, sectionIndex) => {
|
||||
const startIndex = sectionsWithFields
|
||||
.slice(0, sectionIndex)
|
||||
.reduce((sum, s) => sum + s.fields.length, 0);
|
||||
|
||||
return {
|
||||
...section,
|
||||
fields: section.fields.map((field, fieldIndex) => ({
|
||||
field,
|
||||
globalIndex: startIndex + fieldIndex,
|
||||
})),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return { sectionsWithFieldIndices };
|
||||
};
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
|
||||
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
type UseInitializeFieldsWidgetGroupsDraftParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
serverGroups: FieldsWidgetGroup[];
|
||||
};
|
||||
|
||||
export const useInitializeFieldsWidgetGroupsDraft = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
serverGroups,
|
||||
}: UseInitializeFieldsWidgetGroupsDraftParams) => {
|
||||
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const fieldsWidgetGroupsPersistedState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsPersistedComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const hasInitializedFieldsWidgetGroupsDraftState =
|
||||
useRecoilComponentCallbackState(
|
||||
hasInitializedFieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const initializeDraft = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
() => {
|
||||
const hasInitialized = snapshot
|
||||
.getLoadable(hasInitializedFieldsWidgetGroupsDraftState)
|
||||
.getValue();
|
||||
|
||||
if (hasInitialized[widgetId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDraft = snapshot
|
||||
.getLoadable(fieldsWidgetGroupsDraftState)
|
||||
.getValue();
|
||||
|
||||
const hasDraftForWidget = widgetId in currentDraft;
|
||||
|
||||
if (!hasDraftForWidget) {
|
||||
set(fieldsWidgetGroupsDraftState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: serverGroups,
|
||||
}));
|
||||
|
||||
set(fieldsWidgetGroupsPersistedState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: serverGroups,
|
||||
}));
|
||||
}
|
||||
|
||||
set(hasInitializedFieldsWidgetGroupsDraftState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: true,
|
||||
}));
|
||||
},
|
||||
[
|
||||
hasInitializedFieldsWidgetGroupsDraftState,
|
||||
fieldsWidgetGroupsDraftState,
|
||||
fieldsWidgetGroupsPersistedState,
|
||||
widgetId,
|
||||
serverGroups,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverGroups.length > 0) {
|
||||
initializeDraft();
|
||||
}
|
||||
}, [serverGroups, initializeDraft]);
|
||||
};
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
type UseMoveFieldInDraftParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useMoveFieldInDraft = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseMoveFieldInDraftParams) => {
|
||||
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const moveFieldRecoilCallback = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(
|
||||
sourceGroupId: string,
|
||||
destinationGroupId: string,
|
||||
sourceIndex: number,
|
||||
destinationIndex: number,
|
||||
) => {
|
||||
set(fieldsWidgetGroupsDraftState, (prev) => {
|
||||
const currentGroups = prev[widgetId] ?? [];
|
||||
|
||||
const sourceGroup = currentGroups.find((g) => g.id === sourceGroupId);
|
||||
|
||||
if (!sourceGroup) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const sortedSourceFields = [...sourceGroup.fields].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
const [movedField] = sortedSourceFields.splice(sourceIndex, 1);
|
||||
|
||||
if (!movedField) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (sourceGroupId === destinationGroupId) {
|
||||
sortedSourceFields.splice(destinationIndex, 0, movedField);
|
||||
|
||||
const updatedFields = sortedSourceFields.map((field, index) => ({
|
||||
...field,
|
||||
position: index,
|
||||
}));
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: currentGroups.map((group) =>
|
||||
group.id === sourceGroupId
|
||||
? { ...group, fields: updatedFields }
|
||||
: group,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const destinationGroup = currentGroups.find(
|
||||
(g) => g.id === destinationGroupId,
|
||||
);
|
||||
|
||||
if (!destinationGroup) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const updatedSourceFields = sortedSourceFields.map(
|
||||
(field, index) => ({
|
||||
...field,
|
||||
position: index,
|
||||
}),
|
||||
);
|
||||
|
||||
const sortedDestFields = [...destinationGroup.fields].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
sortedDestFields.splice(destinationIndex, 0, movedField);
|
||||
|
||||
const updatedDestFields = sortedDestFields.map((field, index) => ({
|
||||
...field,
|
||||
position: index,
|
||||
}));
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: currentGroups.map((group) => {
|
||||
if (group.id === sourceGroupId) {
|
||||
return { ...group, fields: updatedSourceFields };
|
||||
}
|
||||
|
||||
if (group.id === destinationGroupId) {
|
||||
return { ...group, fields: updatedDestFields };
|
||||
}
|
||||
|
||||
return group;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[fieldsWidgetGroupsDraftState, widgetId],
|
||||
);
|
||||
|
||||
const moveField = useCallback(
|
||||
(
|
||||
sourceGroupId: string,
|
||||
destinationGroupId: string,
|
||||
sourceIndex: number,
|
||||
destinationIndex: number,
|
||||
) =>
|
||||
moveFieldRecoilCallback(
|
||||
sourceGroupId,
|
||||
destinationGroupId,
|
||||
sourceIndex,
|
||||
destinationIndex,
|
||||
),
|
||||
[moveFieldRecoilCallback],
|
||||
);
|
||||
|
||||
return { moveField };
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
type UseReorderFieldsWidgetEditorGroupsParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useReorderFieldsWidgetEditorGroups = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseReorderFieldsWidgetEditorGroupsParams) => {
|
||||
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const reorderGroupsRecoilCallback = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(reorderedGroupIds: string[]) => {
|
||||
set(fieldsWidgetGroupsDraftState, (prev) => {
|
||||
const currentGroups = prev[widgetId] ?? [];
|
||||
|
||||
const reorderedGroups = reorderedGroupIds
|
||||
.map((groupId, index) => {
|
||||
const group = currentGroups.find((g) => g.id === groupId);
|
||||
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { ...group, position: index };
|
||||
})
|
||||
.filter((g): g is NonNullable<typeof g> => g !== null);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: reorderedGroups,
|
||||
};
|
||||
});
|
||||
},
|
||||
[fieldsWidgetGroupsDraftState, widgetId],
|
||||
);
|
||||
|
||||
const reorderGroups = useCallback(
|
||||
(reorderedGroupIds: string[]) =>
|
||||
reorderGroupsRecoilCallback(reorderedGroupIds),
|
||||
[reorderGroupsRecoilCallback],
|
||||
);
|
||||
|
||||
return { reorderGroups };
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
type UseToggleFieldVisibilityInDraftParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useToggleFieldVisibilityInDraft = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseToggleFieldVisibilityInDraftParams) => {
|
||||
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const toggleFieldVisibilityRecoilCallback = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(groupId: string, fieldMetadataId: string) => {
|
||||
set(fieldsWidgetGroupsDraftState, (prev) => {
|
||||
const currentGroups = prev[widgetId] ?? [];
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: currentGroups.map((group) =>
|
||||
group.id === groupId
|
||||
? {
|
||||
...group,
|
||||
fields: group.fields.map((field) =>
|
||||
field.fieldMetadataItem.id === fieldMetadataId
|
||||
? { ...field, isVisible: !field.isVisible }
|
||||
: field,
|
||||
),
|
||||
}
|
||||
: group,
|
||||
),
|
||||
};
|
||||
});
|
||||
},
|
||||
[fieldsWidgetGroupsDraftState, widgetId],
|
||||
);
|
||||
|
||||
const toggleFieldVisibility = useCallback(
|
||||
(groupId: string, fieldMetadataId: string) =>
|
||||
toggleFieldVisibilityRecoilCallback(groupId, fieldMetadataId),
|
||||
[toggleFieldVisibilityRecoilCallback],
|
||||
);
|
||||
|
||||
return { toggleFieldVisibility };
|
||||
};
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UpdateGroupParams = {
|
||||
groupId: string;
|
||||
name?: string;
|
||||
position?: number;
|
||||
isVisible?: boolean;
|
||||
};
|
||||
|
||||
type UseUpdateFieldsWidgetEditorGroupParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useUpdateFieldsWidgetEditorGroup = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseUpdateFieldsWidgetEditorGroupParams) => {
|
||||
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const updateGroupRecoilCallback = useRecoilCallback(
|
||||
({ set }) =>
|
||||
({ groupId, name, position, isVisible }: UpdateGroupParams) => {
|
||||
set(fieldsWidgetGroupsDraftState, (prev) => {
|
||||
const currentGroups = prev[widgetId] ?? [];
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: currentGroups.map((group) =>
|
||||
group.id === groupId
|
||||
? {
|
||||
...group,
|
||||
...(isDefined(name) ? { name } : {}),
|
||||
...(isDefined(position) ? { position } : {}),
|
||||
...(isDefined(isVisible) ? { isVisible } : {}),
|
||||
}
|
||||
: group,
|
||||
),
|
||||
};
|
||||
});
|
||||
},
|
||||
[fieldsWidgetGroupsDraftState, widgetId],
|
||||
);
|
||||
|
||||
const updateGroup = useCallback(
|
||||
(params: UpdateGroupParams) => updateGroupRecoilCallback(params),
|
||||
[updateGroupRecoilCallback],
|
||||
);
|
||||
|
||||
return { updateGroup };
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
|
||||
export type FieldsWidgetGroupField = {
|
||||
fieldMetadataItem: FieldMetadataItem;
|
||||
position: number;
|
||||
isVisible: boolean;
|
||||
globalIndex: number;
|
||||
viewFieldId?: string;
|
||||
};
|
||||
|
||||
export type FieldsWidgetGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
position: number;
|
||||
isVisible: boolean;
|
||||
fields: FieldsWidgetGroupField[];
|
||||
};
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { filterDraftGroupsForDisplay } from '@/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay';
|
||||
|
||||
const mockFieldMetadataItem = {
|
||||
id: 'field-meta-1',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'TEXT',
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const makeGroup = (
|
||||
overrides: Partial<FieldsWidgetGroup> & { id: string },
|
||||
): FieldsWidgetGroup => ({
|
||||
name: 'Group',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fields: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeField = (
|
||||
overrides: Partial<FieldsWidgetGroup['fields'][number]> = {},
|
||||
): FieldsWidgetGroup['fields'][number] => ({
|
||||
fieldMetadataItem: mockFieldMetadataItem,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
globalIndex: 0,
|
||||
viewFieldId: 'vf1',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('filterDraftGroupsForDisplay', () => {
|
||||
it('should return empty array for empty input', () => {
|
||||
const result = filterDraftGroupsForDisplay([]);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out non-visible groups', () => {
|
||||
const groups: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
isVisible: true,
|
||||
fields: [makeField()],
|
||||
}),
|
||||
makeGroup({
|
||||
id: 'g2',
|
||||
isVisible: false,
|
||||
fields: [makeField({ viewFieldId: 'vf2' })],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = filterDraftGroupsForDisplay(groups);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('g1');
|
||||
});
|
||||
|
||||
it('should filter out non-visible fields within groups', () => {
|
||||
const groups: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
makeField({ isVisible: true, viewFieldId: 'vf1' }),
|
||||
makeField({ isVisible: false, viewFieldId: 'vf2' }),
|
||||
makeField({ isVisible: true, viewFieldId: 'vf3' }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = filterDraftGroupsForDisplay(groups);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fields).toHaveLength(2);
|
||||
expect(result[0].fields[0].viewFieldId).toBe('vf1');
|
||||
expect(result[0].fields[1].viewFieldId).toBe('vf3');
|
||||
});
|
||||
|
||||
it('should remove groups where all fields are hidden', () => {
|
||||
const groups: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [makeField({ isVisible: false })],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = filterDraftGroupsForDisplay(groups);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should sort groups by position', () => {
|
||||
const groups: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g3',
|
||||
position: 2,
|
||||
fields: [makeField({ viewFieldId: 'vf3' })],
|
||||
}),
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
position: 0,
|
||||
fields: [makeField({ viewFieldId: 'vf1' })],
|
||||
}),
|
||||
makeGroup({
|
||||
id: 'g2',
|
||||
position: 1,
|
||||
fields: [makeField({ viewFieldId: 'vf2' })],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = filterDraftGroupsForDisplay(groups);
|
||||
|
||||
expect(result.map((g) => g.id)).toEqual(['g1', 'g2', 'g3']);
|
||||
});
|
||||
|
||||
it('should sort fields by position within each group', () => {
|
||||
const groups: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
fields: [
|
||||
makeField({ position: 2, viewFieldId: 'vf2' }),
|
||||
makeField({ position: 0, viewFieldId: 'vf0' }),
|
||||
makeField({ position: 1, viewFieldId: 'vf1' }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = filterDraftGroupsForDisplay(groups);
|
||||
|
||||
expect(result[0].fields.map((f) => f.viewFieldId)).toEqual([
|
||||
'vf0',
|
||||
'vf1',
|
||||
'vf2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should compute correct globalIndex across groups', () => {
|
||||
const groups: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
position: 0,
|
||||
fields: [
|
||||
makeField({ position: 0, viewFieldId: 'vf1' }),
|
||||
makeField({ position: 1, viewFieldId: 'vf2' }),
|
||||
],
|
||||
}),
|
||||
makeGroup({
|
||||
id: 'g2',
|
||||
position: 1,
|
||||
fields: [makeField({ position: 0, viewFieldId: 'vf3' })],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = filterDraftGroupsForDisplay(groups);
|
||||
|
||||
expect(result[0].fields[0].globalIndex).toBe(0);
|
||||
expect(result[0].fields[1].globalIndex).toBe(1);
|
||||
expect(result[1].fields[0].globalIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should not count hidden fields in globalIndex', () => {
|
||||
const groups: FieldsWidgetGroup[] = [
|
||||
makeGroup({
|
||||
id: 'g1',
|
||||
position: 0,
|
||||
fields: [
|
||||
makeField({ position: 0, isVisible: true, viewFieldId: 'vf1' }),
|
||||
makeField({ position: 1, isVisible: false, viewFieldId: 'vf2' }),
|
||||
],
|
||||
}),
|
||||
makeGroup({
|
||||
id: 'g2',
|
||||
position: 1,
|
||||
fields: [
|
||||
makeField({ position: 0, isVisible: true, viewFieldId: 'vf3' }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = filterDraftGroupsForDisplay(groups);
|
||||
|
||||
expect(result[0].fields).toHaveLength(1);
|
||||
expect(result[0].fields[0].globalIndex).toBe(0);
|
||||
expect(result[1].fields[0].globalIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
|
||||
import { type WidgetVisibilityContext } from '@/page-layout/types/WidgetVisibilityContext';
|
||||
import { evaluateWidgetVisibility } from '@/page-layout/utils/evaluateWidgetVisibility';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type FieldsConfigurationSectionWithFields = {
|
||||
id: string;
|
||||
title: string;
|
||||
position: number;
|
||||
fields: FieldMetadataItem[];
|
||||
};
|
||||
|
||||
type FilterAndOrderFieldsFromConfigurationParams = {
|
||||
configuration: FieldsConfiguration;
|
||||
availableFieldMetadataItems: FieldMetadataItem[];
|
||||
context: WidgetVisibilityContext;
|
||||
};
|
||||
|
||||
export const filterAndOrderFieldsFromConfiguration = ({
|
||||
configuration,
|
||||
availableFieldMetadataItems,
|
||||
context,
|
||||
}: FilterAndOrderFieldsFromConfigurationParams): FieldsConfigurationSectionWithFields[] => {
|
||||
const sortedSections = [...configuration.sections].sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
const sectionsWithFields = sortedSections
|
||||
.map((section) => {
|
||||
const sectionFieldIds = new Set(
|
||||
section.fields.map((f) => f.fieldMetadataId),
|
||||
);
|
||||
|
||||
const visibleFields = availableFieldMetadataItems
|
||||
.filter((fieldMetadataItem) =>
|
||||
sectionFieldIds.has(fieldMetadataItem.id),
|
||||
)
|
||||
.filter((fieldMetadataItem) => {
|
||||
const fieldConfig = section.fields.find(
|
||||
(f) => f.fieldMetadataId === fieldMetadataItem.id,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldConfig)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check user-controlled visibility flag
|
||||
// TODO: This will be replaced by proper conditional display later
|
||||
if (fieldConfig.isVisible === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return evaluateWidgetVisibility({
|
||||
conditionalDisplay: fieldConfig.conditionalDisplay,
|
||||
context,
|
||||
});
|
||||
});
|
||||
|
||||
if (visibleFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
position: section.position,
|
||||
fields: visibleFields,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return sectionsWithFields;
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
|
||||
export const filterDraftGroupsForDisplay = (
|
||||
draftGroups: FieldsWidgetGroup[],
|
||||
): FieldsWidgetGroup[] => {
|
||||
const sortedGroups = [...draftGroups].sort((a, b) => a.position - b.position);
|
||||
|
||||
let globalIndex = 0;
|
||||
|
||||
return sortedGroups
|
||||
.filter((group) => group.isVisible)
|
||||
.map((group) => {
|
||||
const visibleFields = [...group.fields]
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.filter((field) => field.isVisible)
|
||||
.map((field) => ({
|
||||
...field,
|
||||
globalIndex: globalIndex++,
|
||||
}));
|
||||
|
||||
return {
|
||||
...group,
|
||||
fields: visibleFields,
|
||||
};
|
||||
})
|
||||
.filter((group) => group.fields.length > 0);
|
||||
};
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
|
||||
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type WidgetConfiguration } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
type WidgetConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const hasMinimalRequiredConfigForGraph = (
|
||||
configuration: WidgetConfiguration | FieldsConfiguration | FieldConfiguration,
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { getWidgetCardVariant } from '@/page-layout/widgets/utils/getWidgetCardVariant';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('getWidgetCardVariant', () => {
|
||||
it('should return dashboard for DASHBOARD page layout type', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
isInPinnedTab: false,
|
||||
pageLayoutType: PageLayoutType.DASHBOARD,
|
||||
isMobile: false,
|
||||
isInRightDrawer: false,
|
||||
}),
|
||||
).toBe('dashboard');
|
||||
});
|
||||
|
||||
it('should return canvas for CANVAS layout mode', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
isInPinnedTab: false,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
isMobile: false,
|
||||
isInRightDrawer: false,
|
||||
}),
|
||||
).toBe('canvas');
|
||||
});
|
||||
|
||||
it('should return side-column when isInPinnedTab is true', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
isInPinnedTab: true,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
isMobile: false,
|
||||
isInRightDrawer: false,
|
||||
}),
|
||||
).toBe('side-column');
|
||||
});
|
||||
|
||||
it('should return side-column when isMobile is true', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
isInPinnedTab: false,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
isMobile: true,
|
||||
isInRightDrawer: false,
|
||||
}),
|
||||
).toBe('side-column');
|
||||
});
|
||||
|
||||
it('should return side-column when isInRightDrawer is true', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
isInPinnedTab: false,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
isMobile: false,
|
||||
isInRightDrawer: true,
|
||||
}),
|
||||
).toBe('side-column');
|
||||
});
|
||||
|
||||
it('should return record-page as default', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
isInPinnedTab: false,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
isMobile: false,
|
||||
isInRightDrawer: false,
|
||||
}),
|
||||
).toBe('record-page');
|
||||
});
|
||||
|
||||
it('should prioritize dashboard over canvas', () => {
|
||||
expect(
|
||||
getWidgetCardVariant({
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
isInPinnedTab: false,
|
||||
pageLayoutType: PageLayoutType.DASHBOARD,
|
||||
isMobile: false,
|
||||
isInRightDrawer: false,
|
||||
}),
|
||||
).toBe('dashboard');
|
||||
});
|
||||
});
|
||||
@@ -26,11 +26,13 @@ import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { AppPath, type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type WorkspaceMember,
|
||||
ViewType as CoreViewType,
|
||||
useFindAllCoreViewsQuery,
|
||||
useFindAllRecordPageLayoutsQuery,
|
||||
useGetCurrentUserQuery,
|
||||
useFindFieldsWidgetCoreViewsQuery,
|
||||
useFindManyLogicFunctionsQuery,
|
||||
useGetCurrentUserQuery,
|
||||
type WorkspaceMember,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
|
||||
@@ -38,6 +40,14 @@ import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
const INDEX_VIEW_TYPES = [
|
||||
CoreViewType.TABLE,
|
||||
CoreViewType.KANBAN,
|
||||
CoreViewType.CALENDAR,
|
||||
];
|
||||
|
||||
const FIELDS_WIDGET_VIEW_TYPES = [CoreViewType.FIELDS_WIDGET];
|
||||
|
||||
export const MetadataProviderEffect = () => {
|
||||
const location = useLocation();
|
||||
|
||||
@@ -77,15 +87,41 @@ export const MetadataProviderEffect = () => {
|
||||
[store],
|
||||
);
|
||||
|
||||
const setCoreViews = useRecoilCallback(
|
||||
const setIndexCoreViews = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(coreViews: CoreViewWithRelations[]) => {
|
||||
(indexViews: CoreViewWithRelations[]) => {
|
||||
const existingCoreViews = snapshot
|
||||
.getLoadable(coreViewsState)
|
||||
.getValue();
|
||||
|
||||
if (!isDeeplyEqual(existingCoreViews, coreViews)) {
|
||||
set(coreViewsState, coreViews);
|
||||
const existingFieldsWidgetViews = existingCoreViews.filter(
|
||||
(view) => view.type === CoreViewType.FIELDS_WIDGET,
|
||||
);
|
||||
|
||||
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
|
||||
|
||||
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
|
||||
set(coreViewsState, mergedViews);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const setFieldsWidgetCoreViews = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(fieldsWidgetViews: CoreViewWithRelations[]) => {
|
||||
const existingCoreViews = snapshot
|
||||
.getLoadable(coreViewsState)
|
||||
.getValue();
|
||||
|
||||
const existingIndexViews = existingCoreViews.filter(
|
||||
(view) => view.type !== CoreViewType.FIELDS_WIDGET,
|
||||
);
|
||||
|
||||
const mergedViews = [...existingIndexViews, ...fieldsWidgetViews];
|
||||
|
||||
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
|
||||
set(coreViewsState, mergedViews);
|
||||
}
|
||||
},
|
||||
[],
|
||||
@@ -129,6 +165,13 @@ export const MetadataProviderEffect = () => {
|
||||
const { data: queryDataCoreViews, loading: queryLoadingCoreViews } =
|
||||
useFindAllCoreViewsQuery({
|
||||
skip: shouldSkip,
|
||||
variables: { viewTypes: INDEX_VIEW_TYPES },
|
||||
});
|
||||
|
||||
const { data: queryDataFieldsWidgetCoreViews } =
|
||||
useFindFieldsWidgetCoreViewsQuery({
|
||||
skip: shouldSkip,
|
||||
variables: { viewTypes: FIELDS_WIDGET_VIEW_TYPES },
|
||||
});
|
||||
|
||||
const { data: queryDataRecordPageLayouts } = useFindAllRecordPageLayoutsQuery(
|
||||
@@ -239,7 +282,7 @@ export const MetadataProviderEffect = () => {
|
||||
initializeFormatPreferences,
|
||||
setCurrentWorkspaceMembersWithDeleted,
|
||||
updateLocaleCatalog,
|
||||
setCoreViews,
|
||||
setIndexCoreViews,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -249,8 +292,18 @@ export const MetadataProviderEffect = () => {
|
||||
|
||||
if (!isDefined(queryDataCoreViews?.getCoreViews)) return;
|
||||
|
||||
setCoreViews(queryDataCoreViews.getCoreViews);
|
||||
}, [queryDataCoreViews?.getCoreViews, setCoreViews, queryLoadingCoreViews]);
|
||||
setIndexCoreViews(queryDataCoreViews.getCoreViews);
|
||||
}, [
|
||||
queryDataCoreViews?.getCoreViews,
|
||||
setIndexCoreViews,
|
||||
queryLoadingCoreViews,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(queryDataFieldsWidgetCoreViews?.getCoreViews)) return;
|
||||
|
||||
setFieldsWidgetCoreViews(queryDataFieldsWidgetCoreViews.getCoreViews);
|
||||
}, [queryDataFieldsWidgetCoreViews?.getCoreViews, setFieldsWidgetCoreViews]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(queryDataRecordPageLayouts?.getPageLayouts)) return;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const VIEW_FIELD_GROUP_FRAGMENT = gql`
|
||||
${VIEW_FIELD_FRAGMENT}
|
||||
fragment ViewFieldGroupFragment on CoreViewFieldGroup {
|
||||
id
|
||||
name
|
||||
position
|
||||
isVisible
|
||||
viewId
|
||||
createdAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
viewFields {
|
||||
...ViewFieldFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment';
|
||||
import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment';
|
||||
import { VIEW_FILTER_FRAGMENT } from '@/views/graphql/fragments/viewFilterFragment';
|
||||
import { VIEW_FILTER_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFilterGroupFragment';
|
||||
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
|
||||
@@ -6,6 +7,7 @@ import { VIEW_SORT_FRAGMENT } from '@/views/graphql/fragments/viewSortFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const VIEW_FRAGMENT = gql`
|
||||
${VIEW_FIELD_GROUP_FRAGMENT}
|
||||
${VIEW_FIELD_FRAGMENT}
|
||||
${VIEW_FILTER_FRAGMENT}
|
||||
${VIEW_FILTER_GROUP_FRAGMENT}
|
||||
@@ -34,6 +36,9 @@ export const VIEW_FRAGMENT = gql`
|
||||
viewFields {
|
||||
...ViewFieldFragment
|
||||
}
|
||||
viewFieldGroups {
|
||||
...ViewFieldGroupFragment
|
||||
}
|
||||
viewFilters {
|
||||
...ViewFilterFragment
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_CORE_VIEW_FIELD_GROUP = gql`
|
||||
${VIEW_FIELD_GROUP_FRAGMENT}
|
||||
mutation CreateCoreViewFieldGroup($input: CreateViewFieldGroupInput!) {
|
||||
createCoreViewFieldGroup(input: $input) {
|
||||
...ViewFieldGroupFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_MANY_CORE_VIEW_FIELD_GROUPS = gql`
|
||||
${VIEW_FIELD_GROUP_FRAGMENT}
|
||||
mutation CreateManyCoreViewFieldGroups(
|
||||
$inputs: [CreateViewFieldGroupInput!]!
|
||||
) {
|
||||
createManyCoreViewFieldGroups(inputs: $inputs) {
|
||||
...ViewFieldGroupFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_CORE_VIEW_FIELD_GROUP = gql`
|
||||
${VIEW_FIELD_GROUP_FRAGMENT}
|
||||
mutation DeleteCoreViewFieldGroup($input: DeleteViewFieldGroupInput!) {
|
||||
deleteCoreViewFieldGroup(input: $input) {
|
||||
...ViewFieldGroupFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { VIEW_FIELD_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFieldGroupFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_CORE_VIEW_FIELD_GROUP = gql`
|
||||
${VIEW_FIELD_GROUP_FRAGMENT}
|
||||
mutation UpdateCoreViewFieldGroup($input: UpdateViewFieldGroupInput!) {
|
||||
updateCoreViewFieldGroup(input: $input) {
|
||||
...ViewFieldGroupFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -3,8 +3,8 @@ import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_ALL_CORE_VIEWS = gql`
|
||||
${VIEW_FRAGMENT}
|
||||
query FindAllCoreViews {
|
||||
getCoreViews {
|
||||
query FindAllCoreViews($viewTypes: [ViewType!]) {
|
||||
getCoreViews(viewTypes: $viewTypes) {
|
||||
...ViewFragment
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_FIELDS_WIDGET_CORE_VIEWS = gql`
|
||||
${VIEW_FRAGMENT}
|
||||
query FindFieldsWidgetCoreViews($viewTypes: [ViewType!]) {
|
||||
getCoreViews(viewTypes: $viewTypes) {
|
||||
...ViewFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -3,8 +3,8 @@ import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_MANY_CORE_VIEWS = gql`
|
||||
${VIEW_FRAGMENT}
|
||||
query FindManyCoreViews($objectMetadataId: String) {
|
||||
getCoreViews(objectMetadataId: $objectMetadataId) {
|
||||
query FindManyCoreViews($objectMetadataId: String, $viewTypes: [ViewType!]) {
|
||||
getCoreViews(objectMetadataId: $objectMetadataId, viewTypes: $viewTypes) {
|
||||
...ViewFragment
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user