Add RecordField abstraction (#13923)

This PR adds a new RecordField abstraction to mimick the behavior of
RecordFilter, RecordSort and other similar concepts that allow to have
something that is not related directly to views.

It is closely following the same pattern with an Effect component to
initialize it from views and CRUD hooks to modify it.

Although for this concept we save to view fields automatically for every
modification.

This PR does not implement saving to view fields, it is just a parallel
code path that is meant to be built to see if everything behaves the
same then we'll remove the old code path at the end by saving record
fields instead of the many states for columns, fields definitions, etc.
This commit is contained in:
Lucas Bordeau
2025-08-14 15:00:07 +02:00
committed by GitHub
parent 8c78314b52
commit fd2b063d84
31 changed files with 717 additions and 423 deletions
+3 -3
View File
@@ -58,9 +58,9 @@ const jestConfig: JestConfigWithTsJest = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: {
global: {
statements: 55,
lines: 54,
functions: 44,
statements: 54,
lines: 53,
functions: 43,
},
},
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
@@ -8,9 +8,7 @@ import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { AnimatePresence } from 'framer-motion';
@@ -50,35 +48,23 @@ export const CommandMenuContainer = ({
useCommandMenuHotKeys();
return (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<RecordComponentInstanceContextsWrapper componentInstanceId={recordIndexId}>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
<AnimatePresence
mode="wait"
onExitComplete={commandMenuCloseAnimationCompleteCleanup}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
>
<AnimatePresence
mode="wait"
onExitComplete={commandMenuCloseAnimationCompleteCleanup}
>
{isCommandMenuOpened && (
<CommandMenuOpenContainer>
{children}
</CommandMenuOpenContainer>
)}
</AnimatePresence>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
{isCommandMenuOpened && (
<CommandMenuOpenContainer>{children}</CommandMenuOpenContainer>
)}
</AnimatePresence>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
);
};
@@ -25,9 +25,7 @@ import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpe
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { HttpResponse, graphql } from 'msw';
import { IconDotsVertical } from 'twenty-ui/display';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
@@ -38,33 +36,25 @@ const openTimeout = 50;
const ContextStoreDecorator: Decorator = (Story) => {
return (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={COMMAND_MENU_COMPONENT_INSTANCE_ID}
>
<RecordFiltersComponentInstanceContext.Provider
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
>
<RecordSortsComponentInstanceContext.Provider
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
<JestContextStoreSetter
contextStoreCurrentObjectMetadataNameSingular="company"
contextStoreCurrentViewId="1"
contextStoreCurrentViewType={ContextStoreViewType.Table}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
>
<JestContextStoreSetter
contextStoreCurrentObjectMetadataNameSingular="company"
contextStoreCurrentViewId="1"
contextStoreCurrentViewType={ContextStoreViewType.Table}
>
<Story />
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<Story />
</JestContextStoreSetter>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
);
};
@@ -3,10 +3,8 @@ import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/c
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { MergeRecordsContainer } from '@/object-record/record-merge/components/MergeRecordsContainer';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
import styled from '@emotion/styled';
@@ -35,31 +33,23 @@ export const CommandMenuMergeRecordPage = () => {
}
return (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: `record-merge-${commandMenuPageInstanceId}` }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={`record-merge-${commandMenuPageInstanceId}`}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: `record-merge-${commandMenuPageInstanceId}` }}
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: `record-merge-${commandMenuPageInstanceId}` }}
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: commandMenuPageInstanceId }}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: commandMenuPageInstanceId }}
>
<StyledRightDrawerRecord isMobile={isMobile}>
<MergeRecordsContainer
objectNameSingular={objectMetadataItem.nameSingular}
componentInstanceId={commandMenuPageInstanceId}
/>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<StyledRightDrawerRecord isMobile={isMobile}>
<MergeRecordsContainer
objectNameSingular={objectMetadataItem.nameSingular}
componentInstanceId={commandMenuPageInstanceId}
/>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
);
};
@@ -5,12 +5,10 @@ import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/r
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { INFORMATION_BANNER_HEIGHT } from '@/information-banner/constants/InformationBannerHeight';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
@@ -71,48 +69,40 @@ export const CommandMenuRecordPage = () => {
}
return (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={`record-show-${objectRecordId}`}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: commandMenuPageInstanceId,
}}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: commandMenuPageInstanceId }}
>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: commandMenuPageInstanceId,
}}
<StyledRightDrawerRecord
isMobile={isMobile}
hasDeletedRecordBanner={!!recordDeletedAt}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: commandMenuPageInstanceId }}
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
}}
>
<StyledRightDrawerRecord
isMobile={isMobile}
hasDeletedRecordBanner={!!recordDeletedAt}
>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
}}
>
<RecordShowEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
/>
</TimelineActivityContext.Provider>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<RecordShowEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
/>
</TimelineActivityContext.Provider>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
);
};
@@ -0,0 +1,34 @@
import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { type PropsWithChildren } from 'react';
export type RecordComponentInstanceContextsWrapperProps = PropsWithChildren<{
componentInstanceId: string;
}>;
export const RecordComponentInstanceContextsWrapper = ({
componentInstanceId,
children,
}: RecordComponentInstanceContextsWrapperProps) => {
return (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<RecordFieldsComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
{children}
</RecordFieldsComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
);
};
@@ -1,14 +1,12 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { type ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
@@ -42,30 +40,20 @@ const meta: Meta<typeof ObjectOptionsDropdownContent> = {
}, [setObjectMetadataItems]);
return (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={instanceId}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId }}
>
<RecordTableComponentInstanceContext.Provider
value={{ instanceId }}
<RecordTableComponentInstanceContext.Provider value={{ instanceId }}>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
<Story />
</MemoryRouter>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<Story />
</MemoryRouter>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
);
},
ContextStoreDecorator,
@@ -1,6 +1,7 @@
import { type OnDragEndResponder } from '@hello-pangea/dnd';
import { useCallback } from 'react';
import { useReorderRecordFields } from '@/object-record/record-field/hooks/useReorderRecordFields';
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
@@ -24,6 +25,8 @@ export const useObjectOptionsForTable = (
{ recordTableId, objectMetadataId },
);
const { reorderRecordFields } = useReorderRecordFields();
const handleReorderColumns: OnDragEndResponder = useCallback(
async (result) => {
if (
@@ -39,9 +42,14 @@ export const useObjectOptionsForTable = (
toIndex: result.destination.index - 1,
});
reorderRecordFields({
fromIndex: result.source.index - 1,
toIndex: result.destination.index - 1,
});
handleColumnReorder(reorderedFields);
},
[visibleTableColumns, handleColumnReorder],
[visibleTableColumns, handleColumnReorder, reorderRecordFields],
);
return {
@@ -0,0 +1,70 @@
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
export const useMoveRecordField = () => {
const currentRecordFieldsCallbackState = useRecoilComponentCallbackState(
currentRecordFieldsComponentState,
);
const moveRecordField = useRecoilCallback(
({ set, snapshot }) =>
({
direction,
fieldMetadataItemIdToMove,
}: {
direction: 'before' | 'after';
fieldMetadataItemIdToMove: string;
}) => {
const currentRecordFields = snapshot
.getLoadable(currentRecordFieldsCallbackState)
.getValue();
const indexOfRecordFieldToMove = currentRecordFields.findIndex(
(recordField) =>
recordField.fieldMetadataItemId === fieldMetadataItemIdToMove,
);
if (indexOfRecordFieldToMove === -1) {
return;
}
const newRecordFields = [...currentRecordFields];
const targetArrayIndex =
direction === 'before'
? indexOfRecordFieldToMove - 1
: indexOfRecordFieldToMove + 1;
const targetArraySize = newRecordFields.length - 1;
if (
indexOfRecordFieldToMove >= 0 &&
targetArrayIndex >= 0 &&
indexOfRecordFieldToMove <= targetArraySize &&
targetArrayIndex <= targetArraySize
) {
const currentRecordField = newRecordFields[indexOfRecordFieldToMove];
const targetRecordField = newRecordFields[targetArrayIndex];
const targetRecordFieldNewPosition = currentRecordField.position;
const currentRecordFieldNewPosition = targetRecordField.position;
newRecordFields[indexOfRecordFieldToMove] = {
...newRecordFields[indexOfRecordFieldToMove],
position: currentRecordFieldNewPosition,
};
newRecordFields[targetRecordFieldNewPosition] = {
...newRecordFields[targetRecordFieldNewPosition],
position: targetRecordFieldNewPosition,
};
set(currentRecordFieldsCallbackState, newRecordFields);
}
},
[currentRecordFieldsCallbackState],
);
return { moveRecordField };
};
@@ -0,0 +1,39 @@
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
export const useReorderRecordFields = () => {
const currentRecordFieldsCallbackState = useRecoilComponentCallbackState(
currentRecordFieldsComponentState,
);
const reorderRecordFields = useRecoilCallback(
({ set, snapshot }) =>
({ fromIndex, toIndex }: { fromIndex: number; toIndex: number }) => {
const currentRecordFields = snapshot
.getLoadable(currentRecordFieldsCallbackState)
.getValue();
const reorderedRecordFields = moveArrayItem(currentRecordFields, {
fromIndex,
toIndex,
});
const reorderedRecordFieldsWithNewPosition =
reorderedRecordFields.map<RecordField>((recordField, index) => ({
...recordField,
position: index,
}));
set(
currentRecordFieldsCallbackState,
reorderedRecordFieldsWithNewPosition,
);
},
[currentRecordFieldsCallbackState],
);
return { reorderRecordFields };
};
@@ -0,0 +1,58 @@
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useRecoilCallback } from 'recoil';
export const useUpdateRecordField = () => {
const currentRecordFieldsCallbackState = useRecoilComponentCallbackState(
currentRecordFieldsComponentState,
);
const updateRecordField = useRecoilCallback(
({ set, snapshot }) =>
(
fieldMetadataItemId: string,
partialRecordField: Partial<
Pick<RecordField, 'isVisible' | 'size' | 'position'>
>,
) => {
const currentRecordFields = getSnapshotValue(
snapshot,
currentRecordFieldsCallbackState,
);
const foundRecordFieldInCurrentRecordFields = currentRecordFields.some(
(existingRecordField) =>
existingRecordField.fieldMetadataItemId === fieldMetadataItemId,
);
if (!foundRecordFieldInCurrentRecordFields) {
throw new Error(
`Cannot find record field to update with field metadata item id : ${fieldMetadataItemId}`,
);
} else {
set(currentRecordFieldsCallbackState, (currentRecordFields) => {
const newCurrentRecordFields = [...currentRecordFields];
const indexOfRecordFieldToUpdate = newCurrentRecordFields.findIndex(
(existingRecordField) =>
existingRecordField.fieldMetadataItemId === fieldMetadataItemId,
);
newCurrentRecordFields[indexOfRecordFieldToUpdate] = {
...newCurrentRecordFields[indexOfRecordFieldToUpdate],
...partialRecordField,
};
return newCurrentRecordFields;
});
}
},
[currentRecordFieldsCallbackState],
);
return {
updateRecordField,
};
};
@@ -0,0 +1,53 @@
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useRecoilCallback } from 'recoil';
export const useUpsertRecordField = () => {
const currentRecordFieldsCallbackState = useRecoilComponentCallbackState(
currentRecordFieldsComponentState,
);
const upsertRecordField = useRecoilCallback(
({ set, snapshot }) =>
(recordFieldToUpsert: RecordField) => {
const currentRecordFields = getSnapshotValue(
snapshot,
currentRecordFieldsCallbackState,
);
const foundRecordFieldInCurrentRecordFields = currentRecordFields.some(
(existingRecordField) =>
existingRecordField.id === recordFieldToUpsert.id,
);
if (!foundRecordFieldInCurrentRecordFields) {
set(currentRecordFieldsCallbackState, [
...currentRecordFields,
recordFieldToUpsert,
]);
} else {
set(currentRecordFieldsCallbackState, (currentRecordFields) => {
const newCurrentRecordFields = [...currentRecordFields];
const indexOfRecordFieldToUpdate = newCurrentRecordFields.findIndex(
(existingRecordField) =>
existingRecordField.id === recordFieldToUpsert.id,
);
newCurrentRecordFields[indexOfRecordFieldToUpdate] = {
...recordFieldToUpsert,
};
return newCurrentRecordFields;
});
}
},
[currentRecordFieldsCallbackState],
);
return {
upsertRecordField,
};
};
@@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const RecordFieldsComponentInstanceContext =
createComponentInstanceContext();
@@ -0,0 +1,11 @@
import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext';
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const currentRecordFieldsComponentState = createComponentState<
RecordField[]
>({
key: 'currentRecordFieldsComponentState',
defaultValue: [],
componentInstanceContext: RecordFieldsComponentInstanceContext,
});
@@ -0,0 +1,10 @@
import { type AggregateOperations } from '~/generated/graphql';
export type RecordField = {
id: string;
fieldMetadataItemId: string;
position: number;
isVisible: boolean;
size: number;
aggregateOperation?: AggregateOperations | null;
};
@@ -3,17 +3,15 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect';
import { RecordIndexLoadBaseOnContextStoreEffect } from '@/object-record/record-index/components/RecordIndexLoadBaseOnContextStoreEffect';
import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader';
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-select/constants/RecordIndecDragSelectBoundaryClass';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
@@ -74,34 +72,26 @@ export const RecordIndexContainerGater = () => {
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordIndexId}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
<PageTitle title={objectMetadataItem.labelPlural} />
<RecordIndexPageHeader />
<PageBody>
<StyledIndexContainer
className={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
>
<PageTitle title={objectMetadataItem.labelPlural} />
<RecordIndexPageHeader />
<PageBody>
<StyledIndexContainer
className={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
>
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>
</ActionMenuComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>
</ActionMenuComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
<RecordIndexLoadBaseOnContextStoreEffect />
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
@@ -5,6 +5,7 @@ import { type ColumnDefinition } from '@/object-record/record-table/types/Column
// TODO: see how we can better abstract this and set the correct interaction between view and table
// but for now it allows to have a cleaner API globally.
// TODO: should be solved with new RecordField abstraction
export const useHandleColumnsChange = () => {
const { saveColumnsToView } = useSaveColumnsToView();
const { setTableColumns } = useSetTableColumns();
@@ -4,11 +4,15 @@ import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldM
import { useUnfocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useUnfocusRecordTableCell';
import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
import { useMoveRecordField } from '@/object-record/record-field/hooks/useMoveRecordField';
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
import { useUpsertRecordField } from '@/object-record/record-field/hooks/useUpsertRecordField';
import { useHandleColumnsChange } from '@/object-record/record-table/hooks/useHandleColumnsChange';
import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { v4 } from 'uuid';
import { type ColumnDefinition } from '../types/ColumnDefinition';
type useRecordTableProps = {
@@ -40,6 +44,9 @@ export const useTableColumns = ({
const { handleColumnsChange } = useHandleColumnsChange();
const { updateRecordField } = useUpdateRecordField();
const { upsertRecordField } = useUpsertRecordField();
const handleColumnVisibilityChange = useCallback(
async (
viewField: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
@@ -68,6 +75,14 @@ export const useTableColumns = ({
{ ...newColumn, isVisible: true, position: lastPosition + 1 },
];
upsertRecordField({
id: v4(),
fieldMetadataItemId: viewField.fieldMetadataId,
size: 100,
isVisible: true,
position: lastPosition + 1,
});
await handleColumnsChange({
columns: nextColumns,
objectMetadataId,
@@ -80,6 +95,10 @@ export const useTableColumns = ({
: previousColumn,
);
updateRecordField(viewField.fieldMetadataId, {
isVisible: !viewField.isVisible,
});
await handleColumnsChange({
columns: nextColumns,
objectMetadataId,
@@ -88,15 +107,19 @@ export const useTableColumns = ({
}
},
[
upsertRecordField,
tableColumns,
availableTableColumns,
handleColumnsChange,
visibleTableColumns,
objectMetadataId,
recordTableId,
updateRecordField,
],
);
const { moveRecordField } = useMoveRecordField();
const handleMoveTableColumn = useCallback(
async (
direction: 'left' | 'right',
@@ -115,6 +138,11 @@ export const useTableColumns = ({
visibleTableColumns,
);
moveRecordField({
direction: direction === 'left' ? 'before' : 'after',
fieldMetadataItemIdToMove: column.fieldMetadataId,
});
await handleColumnsChange({
columns,
objectMetadataId,
@@ -128,6 +156,7 @@ export const useTableColumns = ({
handleColumnsChange,
objectMetadataId,
recordTableId,
moveRecordField,
],
);
@@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
import { isObjectReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isObjectReadOnly';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
@@ -135,13 +136,17 @@ export const RecordTableHeaderCell = ({
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
number | null
>(null);
const [resizedFieldKey, setResizedFieldKey] = useState<string | null>(null);
const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] = useState<
string | null
>(null);
const { handleColumnsChange } = useTableColumns({
objectMetadataId: objectMetadataItem.id,
recordTableId,
});
const { updateRecordField } = useUpdateRecordField();
const handleResizeHandlerStart = useCallback<PointerEventListener>(
({ x }) => {
setInitialPointerPositionX(x);
@@ -162,7 +167,7 @@ export const RecordTableHeaderCell = ({
const handleResizeHandlerEnd = useRecoilCallback(
({ snapshot, set }) =>
async () => {
if (!resizedFieldKey) return;
if (!resizedFieldMetadataItemId) return;
const resizeFieldOffset = getSnapshotValue(
snapshot,
@@ -171,22 +176,27 @@ export const RecordTableHeaderCell = ({
const nextWidth = Math.round(
Math.max(
tableColumnsByKey[resizedFieldKey].size + resizeFieldOffset,
tableColumnsByKey[resizedFieldMetadataItemId].size +
resizeFieldOffset,
COLUMN_MIN_WIDTH,
),
);
set(resizeFieldOffsetState, 0);
setInitialPointerPositionX(null);
setResizedFieldKey(null);
setResizedFieldMetadataItemId(null);
if (nextWidth !== tableColumnsByKey[resizedFieldKey].size) {
if (nextWidth !== tableColumnsByKey[resizedFieldMetadataItemId].size) {
const nextColumns = tableColumns.map((column) =>
column.fieldMetadataId === resizedFieldKey
column.fieldMetadataId === resizedFieldMetadataItemId
? { ...column, size: nextWidth }
: column,
);
updateRecordField(resizedFieldMetadataItemId, {
size: nextWidth,
});
await handleColumnsChange({
columns: nextColumns,
objectMetadataId: objectMetadataItem.id,
@@ -195,19 +205,20 @@ export const RecordTableHeaderCell = ({
}
},
[
resizedFieldKey,
resizedFieldMetadataItemId,
resizeFieldOffsetState,
tableColumnsByKey,
setResizedFieldKey,
setResizedFieldMetadataItemId,
tableColumns,
handleColumnsChange,
objectMetadataItem,
recordTableId,
updateRecordField,
],
);
useTrackPointer({
shouldTrackPointer: resizedFieldKey !== null,
shouldTrackPointer: resizedFieldMetadataItemId !== null,
onMouseDown: handleResizeHandlerStart,
onMouseMove: handleResizeHandlerMove,
onMouseUp: handleResizeHandlerEnd,
@@ -251,10 +262,12 @@ export const RecordTableHeaderCell = ({
return (
<StyledColumnHeaderCell
key={column.fieldMetadataId}
isResizing={resizedFieldKey === column.fieldMetadataId}
isResizing={resizedFieldMetadataItemId === column.fieldMetadataId}
columnWidth={Math.max(
tableColumnsByKey[column.fieldMetadataId].size +
(resizedFieldKey === column.fieldMetadataId ? resizeFieldOffset : 0) +
(resizedFieldMetadataItemId === column.fieldMetadataId
? resizeFieldOffset
: 0) +
24,
COLUMN_MIN_WIDTH,
)}
@@ -286,7 +299,7 @@ export const RecordTableHeaderCell = ({
className="cursor-col-resize"
role="separator"
onPointerDown={() => {
setResizedFieldKey(column.fieldMetadataId);
setResizedFieldMetadataItemId(column.fieldMetadataId);
}}
/>
)}
@@ -5,10 +5,8 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainCo
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -57,48 +55,40 @@ export const SignInBackgroundMockContainer = () => {
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordIndexId}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
}}
>
<RecordSortsComponentInstanceContext.Provider
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordIndexId}
viewId={viewBarId}
/>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
}}
>
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordIndexId}
viewId={viewBarId}
/>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
{isDefined(objectMetadataItem) && (
<>
<ViewBar
viewBarId={viewBarId}
optionsDropdownButton={<></>}
/>
{isDefined(objectMetadataItem) && (
<>
<ViewBar
viewBarId={viewBarId}
optionsDropdownButton={<></>}
/>
<RecordTableWithWrappers
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={viewBarId}
updateRecordMutation={() => {}}
/>
</>
)}
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<RecordTableWithWrappers
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={viewBarId}
updateRecordMutation={() => {}}
/>
</>
)}
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
</StyledContainer>
@@ -14,6 +14,7 @@ import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/cons
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { ViewBarAnyFieldFilterEffect } from '@/views/components/ViewBarAnyFieldFilterEffect';
import { ViewBarFilterDropdown } from '@/views/components/ViewBarFilterDropdown';
import { ViewBarRecordFieldEffect } from '@/views/components/ViewBarRecordFieldEffect';
import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect';
import { ViewBarRecordFilterGroupEffect } from '@/views/components/ViewBarRecordFilterGroupEffect';
import { ViewBarRecordSortEffect } from '@/views/components/ViewBarRecordSortEffect';
@@ -45,6 +46,7 @@ export const ViewBar = ({
>
<ViewBarRecordFilterGroupEffect />
<ViewBarAnyFieldFilterEffect />
<ViewBarRecordFieldEffect />
<ViewBarRecordFilterEffect />
<ViewBarRecordSortEffect />
<QueryParamsFiltersEffect />
@@ -0,0 +1,65 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { hasInitializedCurrentRecordFieldsComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFieldsComponentFamilyState';
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const ViewBarRecordFieldEffect = () => {
const currentViewId = useRecoilComponentValue(
contextStoreCurrentViewIdComponentState,
);
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
const [
hasInitializedCurrentRecordFields,
setHasInitializedCurrentRecordFields,
] = useRecoilComponentFamilyState(
hasInitializedCurrentRecordFieldsComponentFamilyState,
{
viewId: currentViewId ?? undefined,
},
);
const setCurrentRecordFields = useSetRecoilComponentState(
currentRecordFieldsComponentState,
);
useEffect(() => {
if (!hasInitializedCurrentRecordFields && isDefined(currentView)) {
if (currentView.objectMetadataId !== objectMetadataItem.id) {
return;
}
const recordFields = currentView.viewFields
.map(mapViewFieldToRecordField)
.filter(isDefined);
setCurrentRecordFields(recordFields);
setHasInitializedCurrentRecordFields(true);
}
}, [
currentViewId,
setCurrentRecordFields,
hasInitializedCurrentRecordFields,
setHasInitializedCurrentRecordFields,
currentView,
objectMetadataItem,
]);
return null;
};
@@ -6,10 +6,7 @@ import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlur
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
@@ -20,6 +17,7 @@ import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { ViewType } from '@/views/types/ViewType';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
import { type View } from '@/views/types/View';
@@ -106,33 +104,23 @@ const meta: Meta<typeof ViewBarFilterDropdown> = {
recordIndexId: instanceId,
}}
>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={instanceId}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId }}
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId: VIEW_BAR_FILTER_DROPDOWN_ID }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId }}
<RecordTableComponentInstanceContext.Provider
value={{
instanceId: instanceId,
}}
>
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId: VIEW_BAR_FILTER_DROPDOWN_ID }}
>
<RecordTableComponentInstanceContext.Provider
value={{
instanceId: instanceId,
}}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId }}
>
<Story />
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</ObjectFilterDropdownComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<Story />
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</ObjectFilterDropdownComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
</RecordIndexContextProvider>
);
},
@@ -0,0 +1,9 @@
import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext';
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
export const hasInitializedCurrentRecordFieldsComponentFamilyState =
createComponentFamilyState<boolean, { viewId?: string }>({
key: 'hasInitializedCurrentRecordFieldsComponentFamilyState',
defaultValue: false,
componentInstanceContext: RecordFieldsComponentInstanceContext,
});
@@ -0,0 +1,22 @@
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
export const areRecordFieldsEqual = (
recordFieldA: RecordField,
recordFieldB: RecordField,
) => {
const propertiesToCompare: (keyof RecordField)[] = [
'fieldMetadataItemId',
'isVisible',
'position',
'size',
'aggregateOperation',
];
return propertiesToCompare.every((property) =>
compareStrictlyExceptForNullAndUndefined(
recordFieldA[property],
recordFieldB[property],
),
);
};
@@ -0,0 +1,15 @@
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { type ViewField } from '@/views/types/ViewField';
export const mapViewFieldToRecordField = (viewField: ViewField) => {
const recordField: RecordField = {
id: viewField.id,
fieldMetadataItemId: viewField.fieldMetadataId,
isVisible: viewField.isVisible,
position: viewField.position,
size: viewField.size,
aggregateOperation: viewField.aggregateOperation,
};
return recordField;
};
@@ -5,12 +5,10 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
import { computeRecordShowComponentInstanceId } from '@/object-record/record-show/utils/computeRecordShowComponentInstanceId';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { PageHeaderToggleCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderToggleCommandMenuButton';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
@@ -33,55 +31,47 @@ export const RecordShowPage = () => {
computeRecordShowComponentInstanceId(objectRecordId);
return (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: recordShowComponentInstanceId }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordShowComponentInstanceId}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordShowComponentInstanceId }}
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
>
<RecordSortsComponentInstanceContext.Provider
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: recordShowComponentInstanceId }}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: recordShowComponentInstanceId }}
<PageContainer>
<RecordShowPageTitle
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
/>
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
>
<PageContainer>
<RecordShowPageTitle
<RecordShowActionMenu />
<PageHeaderToggleCommandMenuButton />
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
}}
>
<RecordShowEffect
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
recordId={objectRecordId}
/>
<RecordShowPageHeader
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
>
<RecordShowActionMenu />
<PageHeaderToggleCommandMenuButton />
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
}}
>
<RecordShowEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
/>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
loading={false}
/>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
);
};
@@ -24,9 +24,7 @@ import { MainContextStoreProvider } from '@/context-store/components/MainContext
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
@@ -97,26 +95,11 @@ const Providers = () => {
<HelmetProvider>
<IconsProvider>
<PrefetchDataProvider>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{
instanceId:
'storybook-test-record-filter-groups',
}}
<RecordComponentInstanceContextsWrapper
componentInstanceId={'storybook-test-record'}
>
<RecordFiltersComponentInstanceContext.Provider
value={{
instanceId: 'storybook-test-record-filters',
}}
>
<RecordSortsComponentInstanceContext.Provider
value={{
instanceId: 'storybook-test-record-sorts',
}}
>
<Outlet />
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<Outlet />
</RecordComponentInstanceContextsWrapper>
</PrefetchDataProvider>
</IconsProvider>
</HelmetProvider>
@@ -5,12 +5,10 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import {
RecordTableContextProvider,
@@ -145,32 +143,24 @@ export const RecordTableDecorator: Decorator = (Story, context) => {
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordIndexId}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
<InternalTableContextProviders
objectMetadataItem={objectMetadataItem}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<InternalTableContextProviders
objectMetadataItem={objectMetadataItem}
>
<InternalTableStateLoaderEffect
objectMetadataItem={objectMetadataItem}
/>
<Story />
</InternalTableContextProviders>
</ActionMenuComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<InternalTableStateLoaderEffect
objectMetadataItem={objectMetadataItem}
/>
<Story />
</InternalTableContextProviders>
</ActionMenuComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
);
@@ -1,9 +1,7 @@
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { type MockedResponse } from '@apollo/client/testing';
import { type ReactNode } from 'react';
import { type MutableSnapshot } from 'recoil';
@@ -51,58 +49,48 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
return ({ children }: { children: ReactNode }) => (
<Wrapper>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={componentInstanceId}
>
<RecordFiltersComponentInstanceContext.Provider
value={{
instanceId: componentInstanceId,
}}
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: componentInstanceId,
}}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
<RecordIndexContextProvider
value={{
objectPermissionsByObjectMetadataId: {},
indexIdentifierUrl: () => 'indexIdentifierUrl',
onIndexRecordsLoaded: () => {},
objectNamePlural: mockObjectMetadataItem.namePlural,
objectNameSingular: mockObjectMetadataItem.nameSingular,
objectMetadataItem: mockObjectMetadataItem,
recordIndexId: 'recordIndexId',
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: componentInstanceId,
}}
<JestContextStoreSetter
contextStoreCurrentViewId={contextStoreCurrentViewId}
contextStoreFilters={contextStoreFilters}
contextStoreTargetedRecordsRule={
contextStoreTargetedRecordsRule
}
contextStoreNumberOfSelectedRecords={
contextStoreNumberOfSelectedRecords
}
contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular
}
contextStoreCurrentViewType={contextStoreCurrentViewType}
>
<RecordIndexContextProvider
value={{
objectPermissionsByObjectMetadataId: {},
indexIdentifierUrl: () => 'indexIdentifierUrl',
onIndexRecordsLoaded: () => {},
objectNamePlural: mockObjectMetadataItem.namePlural,
objectNameSingular: mockObjectMetadataItem.nameSingular,
objectMetadataItem: mockObjectMetadataItem,
recordIndexId: 'recordIndexId',
}}
>
<JestContextStoreSetter
contextStoreCurrentViewId={contextStoreCurrentViewId}
contextStoreFilters={contextStoreFilters}
contextStoreTargetedRecordsRule={
contextStoreTargetedRecordsRule
}
contextStoreNumberOfSelectedRecords={
contextStoreNumberOfSelectedRecords
}
contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular
}
contextStoreCurrentViewType={contextStoreCurrentViewType}
>
{children}
</JestContextStoreSetter>
</RecordIndexContextProvider>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
{children}
</JestContextStoreSetter>
</RecordIndexContextProvider>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
</Wrapper>
);
};
@@ -1,12 +1,10 @@
import { MockedProvider, type MockedResponse } from '@apollo/client/testing';
import { type ReactNode } from 'react';
import { type MutableSnapshot, RecoilRoot } from 'recoil';
import { RecoilRoot, type MutableSnapshot } from 'recoil';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { type InMemoryCache } from '@apollo/client';
@@ -30,31 +28,21 @@ export const getJestMetadataAndApolloMocksWrapper = ({
value={{ instanceId: 'snack-bar-manager' }}
>
<MockedProvider mocks={apolloMocks} addTypename={false} cache={cache}>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
<RecordComponentInstanceContextsWrapper
componentInstanceId={'instanceId'}
>
<RecordFiltersComponentInstanceContext.Provider
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<ViewComponentInstanceContext.Provider
<JestObjectMetadataItemSetter>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<JestObjectMetadataItemSetter>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<JestContextStoreSetter>
{children}
</JestContextStoreSetter>
</ContextStoreComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</ViewComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
<JestContextStoreSetter>{children}</JestContextStoreSetter>
</ContextStoreComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</ViewComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
</MockedProvider>
</SnackBarComponentInstanceContext.Provider>
</RecoilRoot>