feat: record group add new (#8925)

Fix #8757

This PR is adding the Add new button on view groups.
Also this PR fix an issue where the pending record can be draggable, and
is causing error.

<img width="1119" alt="Screenshot 2024-12-10 at 4 24 43 PM"
src="https://github.com/user-attachments/assets/4fd01e99-c85e-4a06-a733-cbf3cc32957d">

It also start to issues with the way we're using Context.
We're initializing pretty much all Context like this:

```typescript
export const RecordTableContext = createContext<RecordTableContextProps>(
   {} as RecordTableContextProps,
 );
```

This is causing issues when by mistake we use the context like this
outside the Provider hierarchy:

```typescript
const context = useContext(RecordTableContext);
```

This is going to fail silently, and all the context variables become
undefined...

To fix this I've introduced an util called `createRequiredContext`, this
one is returning an array containing the provider and the hook to
retrieve the context.
The context is initialized to undefined inside this utility, this way we
can check if the value has been initialized with the provider to check
if we're inside it. It'll throw an error if this one is used outside the
provider.
The return values are properly typed, so `undefined` is not added to the
value of the Context.

I'll create a followup ticket to use this new utility function, if
that's ok and replace it everywhere in the codebase.
We can also consider adding a eslint rule to warn about the use of
`createContext` directly.
This commit is contained in:
Jérémy M
2024-12-12 11:50:13 +01:00
committed by GitHub
parent 182ebb6394
commit d7da73f0ec
81 changed files with 1546 additions and 684 deletions

View File

@ -9,7 +9,7 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdow
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
@ -20,7 +20,6 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
@ -57,7 +56,7 @@ type ObjectFilterDropdownFilterSelectProps = {
export const ObjectFilterDropdownFilterSelect = ({
isAdvancedFilterButtonVisible,
}: ObjectFilterDropdownFilterSelectProps) => {
const { recordIndexId } = useContext(RecordIndexRootPropsContext);
const { recordIndexId } = useRecordIndexContextOrThrow();
const {
setObjectFilterDropdownSearchInput,

View File

@ -1,9 +1,12 @@
import { Meta, StoryObj } from '@storybook/react';
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { MultipleFiltersDropdownButton } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
@ -19,6 +22,7 @@ import { FieldMetadataType } from '~/generated/graphql';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const meta: Meta<typeof MultipleFiltersDropdownButton> = {
title:
@ -26,6 +30,9 @@ const meta: Meta<typeof MultipleFiltersDropdownButton> = {
component: MultipleFiltersDropdownButton,
decorators: [
(Story) => {
const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === CoreObjectNameSingular.Company,
)!;
const instanceId = 'entity-tasks-filter-scope';
const setAvailableFilterDefinitions = useSetRecoilComponentStateV2(
availableFilterDefinitionsComponentState,
@ -91,19 +98,30 @@ const meta: Meta<typeof MultipleFiltersDropdownButton> = {
},
]);
return (
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId }}
<RecordIndexContextProvider
value={{
indexIdentifierUrl: () => '',
onIndexRecordsLoaded: () => {},
objectNamePlural: CoreObjectNamePlural.Company,
objectNameSingular: CoreObjectNameSingular.Company,
objectMetadataItem: companyObjectMetadataItem,
recordIndexId: instanceId,
}}
>
<RecordTableComponentInstanceContext.Provider
value={{ instanceId: instanceId, onColumnsChange: () => {} }}
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId }}
>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<ObjectFilterDropdownScope filterScopeId={instanceId}>
<Story />
</ObjectFilterDropdownScope>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</ObjectFilterDropdownComponentInstanceContext.Provider>
<RecordTableComponentInstanceContext.Provider
value={{ instanceId: instanceId, onColumnsChange: () => {} }}
>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<ObjectFilterDropdownScope filterScopeId={instanceId}>
<Story />
</ObjectFilterDropdownScope>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</ObjectFilterDropdownComponentInstanceContext.Provider>
</RecordIndexContextProvider>
);
},
ObjectMetadataItemsDecorator,

View File

@ -7,7 +7,7 @@ import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dro
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 { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
@ -76,11 +76,10 @@ const createStory = (contentId: ObjectOptionsContentId | null): Story => ({
)!;
return (
<RecordIndexRootPropsContext.Provider
<RecordIndexContextProvider
value={{
indexIdentifierUrl: () => '',
onIndexRecordsLoaded: () => {},
onCreateRecord: () => {},
objectNamePlural: 'companies',
objectNameSingular: 'company',
objectMetadataItem: companyObjectMetadataItem,
@ -102,7 +101,7 @@ const createStory = (contentId: ObjectOptionsContentId | null): Story => ({
<Story />
</DropdownMenu>
</ObjectOptionsDropdownContext.Provider>
</RecordIndexRootPropsContext.Provider>
</RecordIndexContextProvider>
);
},
],

View File

@ -1,5 +1,5 @@
import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
@ -11,13 +11,13 @@ describe('useSearchRecordGroupField', () => {
renderHook(() => useSearchRecordGroupField(), {
wrapper: ({ children }) => (
<RecoilRoot>
<RecordIndexRootPropsContext.Provider value={contextValue}>
<RecordIndexContextProvider value={contextValue}>
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'myViewInstanceId' }}
>
{children}
</ViewComponentInstanceContext.Provider>
</RecordIndexRootPropsContext.Provider>
</RecordIndexContextProvider>
</RecoilRoot>
),
});

View File

@ -1,11 +1,11 @@
import { objectOptionsDropdownSearchInputComponentState } from '@/object-record/object-options-dropdown/states/objectOptionsDropdownSearchInputComponentState';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useContext, useMemo } from 'react';
import { useMemo } from 'react';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useSearchRecordGroupField = () => {
const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const [recordGroupFieldSearchInput, setRecordGroupFieldSearchInput] =
useRecoilComponentStateV2(objectOptionsDropdownSearchInputComponentState);

View File

@ -5,7 +5,7 @@ import { IconChevronDown, MenuItem, useIcons } from 'twenty-ui';
import { OBJECT_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ObjectSortDropdownId';
import { useObjectSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useObjectSortDropdown';
import { ObjectSortDropdownScope } from '@/object-record/object-sort-dropdown/scopes/ObjectSortDropdownScope';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@ -16,7 +16,6 @@ import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/Styl
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useContext } from 'react';
import { SORT_DIRECTIONS } from '../types/SortDirection';
export const StyledInput = styled.input`
@ -77,7 +76,7 @@ export const ObjectSortDropdownButton = ({
resetSearchInput,
} = useObjectSortDropdown();
const { recordIndexId } = useContext(RecordIndexRootPropsContext);
const { recordIndexId } = useRecordIndexContextOrThrow();
const { isDropdownOpen } = useDropdown(OBJECT_SORT_DROPDOWN_ID);

View File

@ -1,15 +1,41 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ChipFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ChipFieldDisplay';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
const meta: Meta = {
title: 'UI/Data/Field/Display/ChipFieldDisplay',
decorators: [
(Story) => {
const instanceId = 'child-field-display-scope';
const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === CoreObjectNameSingular.Company,
)!;
return (
<RecordIndexContextProvider
value={{
indexIdentifierUrl: () => '',
onIndexRecordsLoaded: () => {},
objectNamePlural: CoreObjectNamePlural.Company,
objectNameSingular: CoreObjectNameSingular.Company,
objectMetadataItem: companyObjectMetadataItem,
recordIndexId: instanceId,
}}
>
<Story />
</RecordIndexContextProvider>
);
},
MemoryRouterDecorator,
ChipGeneratorsDecorator,
getFieldDecorator('person', 'name'),

View File

@ -1,7 +1,7 @@
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
import { useContext } from 'react';
export const useCurrentRecordGroupId = () => {
export const useCurrentRecordGroupId = (): string => {
const context = useContext(RecordGroupContext);
if (!context) {

View File

@ -5,7 +5,7 @@ import { RecordBoardColumnContext } from '@/object-record/record-board/record-bo
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useContext, useMemo } from 'react';
@ -17,9 +17,7 @@ export const useRecordGroupActions = () => {
const navigate = useNavigate();
const location = useLocation();
const { objectNameSingular, recordIndexId } = useContext(
RecordIndexRootPropsContext,
);
const { objectNameSingular, recordIndexId } = useRecordIndexContextOrThrow();
const { columnDefinition: recordGroupDefinition } = useContext(
RecordBoardColumnContext,

View File

@ -2,15 +2,14 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useSetRecordGroup = (viewId?: string) => {
const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2(
recordGroupIdsComponentState,

View File

@ -17,7 +17,7 @@ import { recordIndexSortsState } from '@/object-record/record-index/states/recor
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
@ -39,7 +39,7 @@ import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useCallback, useContext } from 'react';
import { useCallback } from 'react';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const StyledContainer = styled.div`
@ -67,7 +67,7 @@ export const RecordIndexContainer = () => {
recordIndexId,
objectMetadataItem,
objectNameSingular,
} = useContext(RecordIndexRootPropsContext);
} = useRecordIndexContextOrThrow();
const setRecordGroup = useSetRecordGroup(recordIndexId);

View File

@ -5,11 +5,11 @@ import { computeContextStoreFilters } from '@/context-store/utils/computeContext
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useContext, useEffect } from 'react';
import { useEffect } from 'react';
export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
() => {
@ -21,7 +21,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
contextStoreTargetedRecordsRuleComponentState,
);
const { objectNamePlural } = useContext(RecordIndexRootPropsContext);
const { objectNamePlural } = useRecordIndexContextOrThrow();
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,

View File

@ -1,15 +1,15 @@
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useContext, useEffect } from 'react';
import { useEffect } from 'react';
export const RecordIndexContainerContextStoreObjectMetadataEffect = () => {
const setContextStoreCurrentObjectMetadataItem = useSetRecoilComponentStateV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
const { objectNamePlural } = useContext(RecordIndexRootPropsContext);
const { objectNamePlural } = useRecordIndexContextOrThrow();
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,

View File

@ -1,8 +1,8 @@
import { useContext, useEffect } from 'react';
import { useEffect } from 'react';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState';
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
@ -12,7 +12,7 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
import { useRecoilValue } from 'recoil';
export const RecordIndexFiltersToContextStoreEffect = () => {
const { recordIndexId } = useContext(RecordIndexRootPropsContext);
const { recordIndexId } = useRecordIndexContextOrThrow();
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);

View File

@ -3,16 +3,14 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordIndexPageTableAddButton } from '@/object-record/record-index/components/RecordIndexPageTableAddButton';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined, useIcons } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize';
@ -21,17 +19,13 @@ export const RecordIndexPageHeader = () => {
const { findObjectMetadataItemByNamePlural } =
useFilteredObjectMetadataItems();
const { objectNamePlural, onCreateRecord } = useContext(
RecordIndexRootPropsContext,
);
const { objectNamePlural } = useRecordIndexContextOrThrow();
const objectMetadataItem =
findObjectMetadataItemByNamePlural(objectNamePlural);
const { getIcon } = useIcons();
const Icon = getIcon(
findObjectMetadataItemByNamePlural(objectNamePlural)?.icon,
);
const Icon = getIcon(objectMetadataItem?.icon);
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
@ -56,17 +50,14 @@ export const RecordIndexPageHeader = () => {
const pageHeaderTitle =
objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural);
const handleAddButtonClick = () => {
onCreateRecord();
};
return (
<PageHeader title={pageHeaderTitle} Icon={Icon}>
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
{shouldDisplayAddButton &&
/**
* TODO: Logic between Table and Kanban should be merged here when we move some states to record-index
*/
(isTable ? (
<PageAddButton onClick={handleAddButtonClick} />
<RecordIndexPageTableAddButton />
) : (
<RecordIndexPageKanbanAddButton />
))}

View File

@ -1,4 +1,3 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
@ -6,33 +5,20 @@ import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-re
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { useCallback, useContext } from 'react';
import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
width: 100%;
`;
const StyledDropDownMenu = styled(DropdownMenu)`
width: 200px;
`;
export const RecordIndexPageKanbanAddButton = () => {
const dropdownId = `record-index-page-add-button-dropdown`;
const { recordIndexId, objectNameSingular } = useContext(
RecordIndexRootPropsContext,
);
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
const visibleRecordGroupIds = useRecoilComponentValueV2(
visibleRecordGroupIdsComponentSelector,
@ -95,17 +81,15 @@ export const RecordIndexPageKanbanAddButton = () => {
clickableComponent={<PageAddButton />}
dropdownId={dropdownId}
dropdownComponents={
<StyledDropDownMenu>
<StyledDropdownMenuItemsContainer>
{visibleRecordGroupIds.map((recordGroupId) => (
<RecordIndexPageKanbanAddMenuItem
key={recordGroupId}
columnId={recordGroupId}
onItemClick={handleItemClick}
/>
))}
</StyledDropdownMenuItemsContainer>
</StyledDropDownMenu>
<DropdownMenuItemsContainer>
{visibleRecordGroupIds.map((recordGroupId) => (
<RecordIndexPageKanbanAddMenuItem
key={recordGroupId}
columnId={recordGroupId}
onItemClick={handleItemClick}
/>
))}
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownId }}
/>

View File

@ -0,0 +1,16 @@
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { RecordIndexPageTableAddButtonInGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup';
import { RecordIndexPageTableAddButtonNoGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordIndexPageTableAddButton = () => {
const hasRecordGroups = useRecoilComponentValueV2(
hasRecordGroupsComponentSelector,
);
if (!hasRecordGroups) {
return <RecordIndexPageTableAddButtonNoGroup />;
}
return <RecordIndexPageTableAddButtonInGroup />;
};

View File

@ -0,0 +1,80 @@
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useCreateNewTableRecordInGroup } from '@/object-record/record-table/hooks/useCreateNewTableRecordInGroup';
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil';
export const RecordIndexPageTableAddButtonInGroup = () => {
const dropdownId = `record-index-page-table-add-button-dropdown`;
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const visibleRecordGroupIds = useRecoilComponentValueV2(
visibleRecordGroupIdsComponentSelector,
);
const recordGroupFieldMetadata = useRecoilComponentValueV2(
recordGroupFieldMetadataComponentState,
);
const isRecordGroupTableSectionToggledState =
useRecoilComponentCallbackStateV2(
isRecordGroupTableSectionToggledComponentState,
);
const selectFieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.id === recordGroupFieldMetadata?.id,
);
const { createNewTableRecordInGroup } = useCreateNewTableRecordInGroup();
const { closeDropdown } = useDropdown(dropdownId);
const handleCreateNewTableRecordInGroup = useRecoilCallback(
({ set }) =>
(recordGroup: RecordGroupDefinition) => {
set(isRecordGroupTableSectionToggledState(recordGroup.id), true);
createNewTableRecordInGroup(recordGroup.id);
closeDropdown();
},
[
closeDropdown,
createNewTableRecordInGroup,
isRecordGroupTableSectionToggledState,
],
);
if (!selectFieldMetadataItem) {
return null;
}
return (
<Dropdown
dropdownMenuWidth="200px"
dropdownPlacement="bottom-start"
clickableComponent={<PageAddButton />}
dropdownId={dropdownId}
dropdownComponents={
<DropdownMenuItemsContainer>
{visibleRecordGroupIds.map((recordGroupId) => (
<RecordIndexPageKanbanAddMenuItem
key={recordGroupId}
columnId={recordGroupId}
onItemClick={handleCreateNewTableRecordInGroup}
/>
))}
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownId }}
/>
);
};

View File

@ -0,0 +1,21 @@
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
export const RecordIndexPageTableAddButtonNoGroup = () => {
const { recordIndexId } = useRecordIndexContextOrThrow();
const { createNewTableRecord } = useCreateNewTableRecord(recordIndexId);
const handleCreateNewTableRecord = () => {
createNewTableRecord();
};
return (
<>
<PageHotkeysEffect onAddButtonClick={handleCreateNewTableRecord} />
<PageAddButton onClick={handleCreateNewTableRecord} />
</>
);
};

View File

@ -1,8 +1,7 @@
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useContext } from 'react';
import { AvatarChip, AvatarChipVariant, ChipSize } from 'twenty-ui';
export type RecordIdentifierChipProps = {
@ -20,7 +19,7 @@ export const RecordIdentifierChip = ({
size,
maxWidth,
}: RecordIdentifierChipProps) => {
const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext);
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const { recordChipData } = useRecordChipData({
objectNameSingular,
record,

View File

@ -1,9 +1,8 @@
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { useContext } from 'react';
type RecordIndexTableContainerProps = {
recordTableId: string;
@ -14,7 +13,7 @@ export const RecordIndexTableContainer = ({
recordTableId,
viewBarId,
}: RecordIndexTableContainerProps) => {
const { objectNameSingular } = useContext(RecordIndexRootPropsContext);
const { objectNameSingular } = useRecordIndexContextOrThrow();
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,

View File

@ -1,8 +1,8 @@
import { useContext, useEffect } from 'react';
import { useEffect } from 'react';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
@ -13,9 +13,7 @@ import { ViewField } from '@/views/types/ViewField';
import { useRecoilCallback } from 'recoil';
export const RecordIndexTableContainerEffect = () => {
const { recordIndexId, objectNameSingular } = useContext(
RecordIndexRootPropsContext,
);
const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow();
const viewBarId = recordIndexId;

View File

@ -1,15 +1,14 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createRootPropsContext } from '~/utils/createRootPropsContext';
import { createRequiredContext } from '~/utils/createRequiredContext';
export type RecordIndexRootPropsContextProps = {
export type RecordIndexContextValue = {
indexIdentifierUrl: (recordId: string) => string;
onIndexRecordsLoaded: () => void;
onCreateRecord: () => void;
objectNamePlural: string;
objectNameSingular: string;
objectMetadataItem: ObjectMetadataItem;
recordIndexId: string;
};
export const RecordIndexRootPropsContext =
createRootPropsContext<RecordIndexRootPropsContextProps>();
export const [RecordIndexContextProvider, useRecordIndexContextOrThrow] =
createRequiredContext<RecordIndexContextValue>('RecordIndexContext');

View File

@ -3,10 +3,9 @@ import { isNonEmptyString, isNull } from '@sniptt/guards';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { RecordTableBodyUnselectEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect';
@ -29,19 +28,9 @@ const StyledTable = styled.table`
width: 100%;
`;
type RecordTableProps = {
viewBarId: string;
recordTableId: string;
objectNameSingular: string;
onColumnsChange: (columns: any) => void;
};
export const RecordTable = () => {
const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow();
export const RecordTable = ({
viewBarId,
recordTableId,
objectNameSingular,
onColumnsChange,
}: RecordTableProps) => {
const tableBodyRef = useRef<HTMLTableElement>(null);
const { toggleClickOutsideListener } = useClickOutsideListener(
@ -82,51 +71,39 @@ export const RecordTable = ({
}
return (
<RecordTableComponentInstance
recordTableId={recordTableId}
onColumnsChange={onColumnsChange}
>
<RecordTableContextProvider
objectNameSingular={objectNameSingular}
recordTableId={recordTableId}
viewBarId={viewBarId}
>
{!hasRecordGroups ? (
<RecordTableNoRecordGroupBodyEffect />
) : (
<RecordTableRecordGroupBodyEffects />
)}
<RecordTableBodyUnselectEffect
tableBodyRef={tableBodyRef}
recordTableId={recordTableId}
/>
{recordTableIsEmpty ? (
<RecordTableEmptyState />
) : (
<>
<StyledTable className="entity-table-cell" ref={tableBodyRef}>
<RecordTableHeader objectNameSingular={objectNameSingular} />
{!hasRecordGroups ? (
<RecordTableNoRecordGroupBody />
) : (
<RecordTableRecordGroupsBody />
)}
<RecordTableStickyEffect />
</StyledTable>
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={() => {
resetTableRowSelection();
toggleClickOutsideListener(false);
}}
onDragSelectionChange={setRowSelected}
onDragSelectionEnd={() => {
toggleClickOutsideListener(true);
}}
/>
</>
)}
</RecordTableContextProvider>
</RecordTableComponentInstance>
<>
{!hasRecordGroups ? (
<RecordTableNoRecordGroupBodyEffect />
) : (
<RecordTableRecordGroupBodyEffects />
)}
<RecordTableBodyUnselectEffect tableBodyRef={tableBodyRef} />
{recordTableIsEmpty ? (
<RecordTableEmptyState />
) : (
<>
<StyledTable className="entity-table-cell" ref={tableBodyRef}>
<RecordTableHeader />
{!hasRecordGroups ? (
<RecordTableNoRecordGroupBody />
) : (
<RecordTableRecordGroupsBody />
)}
<RecordTableStickyEffect />
</StyledTable>
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={() => {
resetTableRowSelection();
toggleClickOutsideListener(false);
}}
onDragSelectionChange={setRowSelected}
onDragSelectionEnd={() => {
toggleClickOutsideListener(true);
}}
/>
</>
)}
</>
);
};

View File

@ -1,117 +1,44 @@
import { ReactNode } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useCloseRecordTableCellV2 } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2';
import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
import {
OpenTableCellArgs,
useOpenRecordTableCellV2,
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown';
import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord';
import { RecordTableContextProvider as RecordTableContextInternalProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
type RecordTableContextProviderProps = {
viewBarId: string;
recordTableId: string;
objectNameSingular: string;
children: ReactNode;
};
export const RecordTableContextProvider = ({
viewBarId,
recordTableId,
objectNameSingular,
children,
}: {
viewBarId: string;
recordTableId: string;
objectNameSingular: string;
children: ReactNode;
}) => {
}: RecordTableContextProviderProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { upsertRecord } = useUpsertRecord({
objectNameSingular,
recordTableId,
});
const handleUpsertRecord = ({
persistField,
recordId,
fieldName,
}: {
persistField: () => void;
recordId: string;
fieldName: string;
}) => {
upsertRecord(persistField, recordId, fieldName);
};
const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
const handleOpenTableCell = (args: OpenTableCellArgs) => {
openTableCell(args);
};
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
const handleMoveFocus = (direction: MoveFocusDirection) => {
moveFocus(direction);
};
const { closeTableCell } = useCloseRecordTableCellV2(recordTableId);
const handleCloseTableCell = () => {
closeTableCell();
};
const { moveSoftFocusToCell } =
useMoveSoftFocusToCellOnHoverV2(recordTableId);
const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
moveSoftFocusToCell(cellPosition);
};
const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({
recordTableId,
});
const handleActionMenuDropdown = (
event: React.MouseEvent,
recordId: string,
) => {
triggerActionMenuDropdown(event, recordId);
};
const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
recordTableId,
});
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
recordTableId,
);
return (
<RecordTableContext.Provider
<RecordTableContextInternalProvider
value={{
viewBarId,
objectMetadataItem,
onUpsertRecord: handleUpsertRecord,
onOpenTableCell: handleOpenTableCell,
onMoveFocus: handleMoveFocus,
onCloseTableCell: handleCloseTableCell,
onMoveSoftFocusToCell: handleMoveSoftFocusToCell,
onActionMenuDropdownOpened: handleActionMenuDropdown,
onCellMouseEnter: handleContainerMouseEnter,
visibleTableColumns,
recordTableId,
objectNameSingular,
}}
>
{children}
</RecordTableContext.Provider>
</RecordTableContextInternalProvider>
);
};

View File

@ -0,0 +1,95 @@
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useUpsertTableRecordNoGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup';
import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
import {
OpenTableCellArgs,
useOpenRecordTableCellV2,
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { ReactNode } from 'react';
type RecordTableNoRecordGroupBodyContextProviderProps = {
children?: ReactNode;
};
export const RecordTableNoRecordGroupBodyContextProvider = ({
children,
}: RecordTableNoRecordGroupBodyContextProviderProps) => {
const { recordTableId } = useRecordTableContextOrThrow();
const { upsertTableRecordNoGroup } = useUpsertTableRecordNoGroup();
const handleUpsertTableRecordNoRecordGroup = ({
persistField,
recordId,
fieldName,
}: {
persistField: () => void;
recordId: string;
fieldName: string;
}) => {
upsertTableRecordNoGroup(persistField, recordId, fieldName);
};
const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
const handleOpenTableCell = (args: OpenTableCellArgs) => {
openTableCell(args);
};
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
const handleMoveFocus = (direction: MoveFocusDirection) => {
moveFocus(direction);
};
const { closeTableCellNoGroup } = useCloseRecordTableCellNoGroup();
const handleCloseTableCell = () => {
closeTableCellNoGroup();
};
const { moveSoftFocusToCell } =
useMoveSoftFocusToCellOnHoverV2(recordTableId);
const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
moveSoftFocusToCell(cellPosition);
};
const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({
recordTableId,
});
const handleActionMenuDropdown = (
event: React.MouseEvent,
recordId: string,
) => {
triggerActionMenuDropdown(event, recordId);
};
const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
recordTableId,
});
return (
<RecordTableBodyContextProvider
value={{
onUpsertRecord: handleUpsertTableRecordNoRecordGroup,
onOpenTableCell: handleOpenTableCell,
onMoveFocus: handleMoveFocus,
onCloseTableCell: handleCloseTableCell,
onMoveSoftFocusToCell: handleMoveSoftFocusToCell,
onActionMenuDropdownOpened: handleActionMenuDropdown,
onCellMouseEnter: handleContainerMouseEnter,
}}
>
{children}
</RecordTableBodyContextProvider>
);
};

View File

@ -0,0 +1,99 @@
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { useUpsertTableRecordInGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup';
import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup';
import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
import {
OpenTableCellArgs,
useOpenRecordTableCellV2,
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { ReactNode } from 'react';
type RecordTableRecordGroupBodyContextProviderProps = {
recordGroupId: string;
children?: ReactNode;
};
export const RecordTableRecordGroupBodyContextProvider = ({
recordGroupId,
children,
}: RecordTableRecordGroupBodyContextProviderProps) => {
const { recordTableId } = useRecordTableContextOrThrow();
const { upsertTableRecordInGroup } =
useUpsertTableRecordInGroup(recordGroupId);
const handleupsertTableRecordInGroup = ({
persistField,
recordId,
fieldName,
}: {
persistField: () => void;
recordId: string;
fieldName: string;
}) => {
upsertTableRecordInGroup(persistField, recordId, fieldName);
};
const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
const handleOpenTableCell = (args: OpenTableCellArgs) => {
openTableCell(args);
};
const { moveFocus } = useRecordTableMoveFocus(recordTableId);
const handleMoveFocus = (direction: MoveFocusDirection) => {
moveFocus(direction);
};
const { closeTableCellInGroup } =
useCloseRecordTableCellInGroup(recordGroupId);
const handlecloseTableCellInGroup = () => {
closeTableCellInGroup();
};
const { moveSoftFocusToCell } =
useMoveSoftFocusToCellOnHoverV2(recordTableId);
const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
moveSoftFocusToCell(cellPosition);
};
const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({
recordTableId,
});
const handleActionMenuDropdown = (
event: React.MouseEvent,
recordId: string,
) => {
triggerActionMenuDropdown(event, recordId);
};
const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
recordTableId,
});
return (
<RecordTableBodyContextProvider
value={{
onUpsertRecord: handleupsertTableRecordInGroup,
onOpenTableCell: handleOpenTableCell,
onMoveFocus: handleMoveFocus,
onCloseTableCell: handlecloseTableCellInGroup,
onMoveSoftFocusToCell: handleMoveSoftFocusToCell,
onActionMenuDropdownOpened: handleActionMenuDropdown,
onCellMouseEnter: handleContainerMouseEnter,
}}
>
{children}
</RecordTableBodyContextProvider>
);
};

View File

@ -1,7 +1,10 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore';
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -30,24 +33,32 @@ export const RecordTableRecordGroupRows = () => {
[allRecordIds],
);
if (!isRecordGroupTableSectionToggled) {
return null;
}
return (
isRecordGroupTableSectionToggled &&
recordIdsByGroup.map((recordId, rowIndexInGroup) => {
const rowIndex = rowIndexMap.get(recordId);
<>
{recordIdsByGroup.map((recordId, rowIndexInGroup) => {
const rowIndex = rowIndexMap.get(recordId);
if (!isDefined(rowIndex)) {
return null;
}
if (!isDefined(rowIndex)) {
return null;
}
return (
<RecordTableRow
key={recordId}
recordId={recordId}
rowIndexForFocus={rowIndex}
rowIndexForDrag={rowIndexInGroup}
isPendingRow={!isRecordGroupTableSectionToggled}
/>
);
})
return (
<RecordTableRow
key={recordId}
recordId={recordId}
rowIndexForFocus={rowIndex}
rowIndexForDrag={rowIndexInGroup}
isPendingRow={!isRecordGroupTableSectionToggled}
/>
);
})}
<RecordTableRecordGroupSectionLoadMore />
<RecordTablePendingRecordGroupRow />
<RecordTableRecordGroupSectionAddNew />
</>
);
};

View File

@ -16,6 +16,8 @@ import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext
import { useRecordTable } from '../hooks/useRecordTable';
import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Key } from 'ts-key-enum';
@ -96,27 +98,33 @@ export const RecordTableWithWrappers = ({
);
return (
<EntityDeleteContext.Provider value={deleteOneRecord}>
<ScrollWrapper
enableXScroll={isScrollEnabledForRecordTable.enableXScroll}
enableYScroll={isScrollEnabledForRecordTable.enableYScroll}
contextProviderName="recordTableWithWrappers"
<RecordTableComponentInstance
recordTableId={recordTableId}
onColumnsChange={handleColumnsChange}
>
<RecordTableContextProvider
recordTableId={recordTableId}
viewBarId={viewBarId}
objectNameSingular={objectNameSingular}
>
<RecordUpdateContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader>
<StyledTableContainer>
<StyledTableInternalContainer>
<RecordTable
viewBarId={viewBarId}
recordTableId={recordTableId}
objectNameSingular={objectNameSingular}
onColumnsChange={handleColumnsChange}
/>
</StyledTableInternalContainer>
</StyledTableContainer>
</StyledTableWithHeader>
</RecordUpdateContext.Provider>
</ScrollWrapper>
</EntityDeleteContext.Provider>
<EntityDeleteContext.Provider value={deleteOneRecord}>
<ScrollWrapper
enableXScroll={isScrollEnabledForRecordTable.enableXScroll}
enableYScroll={isScrollEnabledForRecordTable.enableYScroll}
contextProviderName="recordTableWithWrappers"
>
<RecordUpdateContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader>
<StyledTableContainer>
<StyledTableInternalContainer>
<RecordTable />
</StyledTableInternalContainer>
</StyledTableContainer>
</StyledTableWithHeader>
</RecordUpdateContext.Provider>
</ScrollWrapper>
</EntityDeleteContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>
);
};

View File

@ -14,12 +14,13 @@ import {
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { mockPerformance } from './mock';
@ -61,78 +62,83 @@ const meta: Meta = {
(Story) => {
return (
<RecordFieldValueSelectorContextProvider>
<RecordTableContext.Provider
<RecordTableContextProvider
value={{
recordTableId: 'recordTableId',
viewBarId: mockPerformance.recordId,
objectMetadataItem: mockPerformance.objectMetadataItem as any,
onUpsertRecord: () => {},
onOpenTableCell: () => {},
onMoveFocus: () => {},
onCloseTableCell: () => {},
onMoveSoftFocusToCell: () => {},
onActionMenuDropdownOpened: () => {},
onCellMouseEnter: () => {},
visibleTableColumns: mockPerformance.visibleTableColumns as any,
objectNameSingular:
mockPerformance.objectMetadataItem.nameSingular,
recordTableId: 'recordTableId',
}}
>
<RecordTableComponentInstance
recordTableId="asd"
onColumnsChange={() => {}}
>
<RecordTableRowContext.Provider
<RecordTableBodyContextProvider
value={{
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
recordId: mockPerformance.recordId,
rowIndex: 0,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
}) + mockPerformance.recordId,
isSelected: false,
isDragging: false,
dragHandleProps: null,
inView: true,
isPendingRow: false,
onUpsertRecord: () => {},
onOpenTableCell: () => {},
onMoveFocus: () => {},
onCloseTableCell: () => {},
onMoveSoftFocusToCell: () => {},
onActionMenuDropdownOpened: () => {},
onCellMouseEnter: () => {},
}}
>
<RecordTableCellContext.Provider
<RecordTableRowContext.Provider
value={{
columnDefinition: mockPerformance.fieldDefinition,
columnIndex: 0,
cellPosition: { row: 0, column: 0 },
hasSoftFocus: false,
isInEditMode: false,
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
recordId: mockPerformance.recordId,
rowIndex: 0,
pathToShowPage:
getBasePathToShowPage({
objectNameSingular:
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
}) + mockPerformance.recordId,
isSelected: false,
isDragging: false,
dragHandleProps: null,
inView: true,
isPendingRow: false,
}}
>
<FieldContext.Provider
<RecordTableCellContext.Provider
value={{
recordId: mockPerformance.recordId,
basePathToShowPage: '/object-record/',
isLabelIdentifier: false,
fieldDefinition: {
...mockPerformance.fieldDefinition,
},
hotkeyScope: 'hotkey-scope',
columnDefinition: mockPerformance.fieldDefinition,
columnIndex: 0,
cellPosition: { row: 0, column: 0 },
hasSoftFocus: false,
isInEditMode: false,
}}
>
<RelationFieldValueSetterEffect />
<table>
<tbody>
<tr>
<Story />
</tr>
</tbody>
</table>
</FieldContext.Provider>
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
<FieldContext.Provider
value={{
recordId: mockPerformance.recordId,
basePathToShowPage: '/object-record/',
isLabelIdentifier: false,
fieldDefinition: {
...mockPerformance.fieldDefinition,
},
hotkeyScope: 'hotkey-scope',
}}
>
<RelationFieldValueSetterEffect />
<table>
<tbody>
<tr>
<Story />
</tr>
</tbody>
</table>
</FieldContext.Provider>
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
</RecordTableBodyContextProvider>
</RecordTableComponentInstance>
</RecordTableContext.Provider>
</RecordTableContextProvider>
</RecordFieldValueSelectorContextProvider>
);
},

View File

@ -0,0 +1,36 @@
import React from 'react';
import { HandleContainerMouseEnterArgs } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { OpenTableCellArgs } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { createRequiredContext } from '~/utils/createRequiredContext';
export type RecordTableBodyContextProps = {
recordGroupId?: string;
onUpsertRecord: ({
persistField,
recordId,
fieldName,
}: {
persistField: () => void;
recordId: string;
fieldName: string;
}) => void;
onOpenTableCell: (args: OpenTableCellArgs) => void;
onMoveFocus: (direction: MoveFocusDirection) => void;
onCloseTableCell: () => void;
onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void;
onActionMenuDropdownOpened: (
event: React.MouseEvent,
recordId: string,
) => void;
onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
};
export const [
RecordTableBodyContextProvider,
useRecordTableBodyContextOrThrow,
] = createRequiredContext<RecordTableBodyContextProps>(
'RecordTableBodyContext',
);

View File

@ -1,39 +1,15 @@
import React, { createContext } from 'react';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { HandleContainerMouseEnterArgs } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
import { OpenTableCellArgs } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { createRequiredContext } from '~/utils/createRequiredContext';
export type RecordTableContextProps = {
viewBarId: string;
objectMetadataItem: ObjectMetadataItem;
onUpsertRecord: ({
persistField,
recordId,
fieldName,
}: {
persistField: () => void;
recordId: string;
fieldName: string;
}) => void;
onOpenTableCell: (args: OpenTableCellArgs) => void;
onMoveFocus: (direction: MoveFocusDirection) => void;
onCloseTableCell: () => void;
onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void;
onActionMenuDropdownOpened: (
event: React.MouseEvent,
recordId: string,
) => void;
onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
visibleTableColumns: ColumnDefinition<FieldMetadata>[];
export type RecordTableContextValue = {
recordTableId: string;
viewBarId: string;
objectNameSingular: string;
objectMetadataItem: ObjectMetadataItem;
visibleTableColumns: ColumnDefinition<FieldMetadata>[];
};
export const RecordTableContext = createContext<RecordTableContextProps>(
{} as RecordTableContextProps,
);
export const [RecordTableContextProvider, useRecordTableContextOrThrow] =
createRequiredContext<RecordTableContextValue>('RecordTableContext');

View File

@ -11,6 +11,7 @@ export type RecordTableRowContextProps = {
isDragging: boolean;
dragHandleProps: DraggableProvidedDragHandleProps | null;
inView?: boolean;
isDragDisabled?: boolean;
};
export const RecordTableRowContext = createContext<RecordTableRowContextProps>(

View File

@ -1,16 +1,15 @@
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useContext } from 'react';
export const RecordTableEmptyState = () => {
const { objectNameSingular, recordTableId, objectMetadataItem } =
useContext(RecordTableContext);
const { recordTableId, objectNameSingular, objectMetadataItem } =
useRecordTableContextOrThrow();
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0;

View File

@ -1,6 +1,5 @@
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useContext } from 'react';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@ -29,7 +28,7 @@ export const RecordTableEmptyStateDisplay = ({
subTitle,
title,
}: RecordTableEmptyStateDisplayProps) => {
const { objectMetadataItem } = useContext(RecordTableContext);
const { objectMetadataItem } = useRecordTableContextOrThrow();
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
return (

View File

@ -1,15 +1,14 @@
import { IconPlus } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { useContext } from 'react';
export const RecordTableEmptyStateNoRecordAtAll = () => {
const { createNewTableRecord } = useCreateNewTableRecord();
const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow();
const { objectMetadataItem } = useContext(RecordTableContext);
const { createNewTableRecord } = useCreateNewTableRecord(recordTableId);
const handleButtonClick = () => {
createNewTableRecord();

View File

@ -1,15 +1,14 @@
import { IconPlus } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { useContext } from 'react';
export const RecordTableEmptyStateNoRecordFoundForFilter = () => {
const { createNewTableRecord } = useCreateNewTableRecord();
const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow();
const { objectMetadataItem } = useContext(RecordTableContext);
const { createNewTableRecord } = useCreateNewTableRecord(recordTableId);
const handleButtonClick = () => {
createNewTableRecord();

View File

@ -2,16 +2,15 @@ import { IconFilterOff } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
import { useContext } from 'react';
export const RecordTableEmptyStateSoftDelete = () => {
const { objectMetadataItem, objectNameSingular, recordTableId } =
useContext(RecordTableContext);
useRecordTableContextOrThrow();
const { deleteCombinedViewFilter } =
useDeleteCombinedViewFilters(recordTableId);

View File

@ -1,5 +1,5 @@
import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import { ReactNode, act } from 'react';
import { RecoilRoot } from 'recoil';
import { createState } from 'twenty-ui';
@ -9,12 +9,16 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { useUpsertTableRecordInGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const draftValue = 'updated Name';
const recordGroupId = 'recordGroupId';
// Todo refactor this test to inject the states in a cleaner way instead of mocking hooks
// (this is not easy to maintain while refactoring)
@ -37,8 +41,11 @@ jest.mock(
}),
);
const pendingRecordIdState = createState<string | null>({
key: 'pendingRecordIdState',
const recordTablePendingRecordIdByGroupComponentFamilyState = createFamilyState<
string | null,
string
>({
key: 'recordTablePendingRecordIdByGroupComponentFamilyState',
defaultValue: null,
});
@ -60,46 +67,64 @@ const Wrapper = ({
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
snapshot.set(pendingRecordIdState, pendingRecordIdMockedValue);
snapshot.set(
recordTablePendingRecordIdByGroupComponentFamilyState(recordGroupId),
pendingRecordIdMockedValue,
);
snapshot.set(draftValueState, draftValueMockedValue);
}}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: CoreObjectNamePlural.Person }}
<RecordTableContextProvider
recordTableId="recordTableId"
objectNameSingular={CoreObjectNameSingular.Person}
viewBarId="viewBarId"
>
<FieldContext.Provider
<RecordTableComponentInstanceContext.Provider
value={{
recordId: 'recordId',
fieldDefinition: {
...textfieldDefinition,
metadata: {
...textfieldDefinition.metadata,
objectMetadataNameSingular: CoreObjectNameSingular.Person,
},
},
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
instanceId: CoreObjectNamePlural.Person,
onColumnsChange: jest.fn(),
}}
>
{children}
</FieldContext.Provider>
</ViewComponentInstanceContext.Provider>
<ViewComponentInstanceContext.Provider
value={{ instanceId: CoreObjectNamePlural.Person }}
>
<FieldContext.Provider
value={{
recordId: 'recordId',
fieldDefinition: {
...textfieldDefinition,
metadata: {
...textfieldDefinition.metadata,
objectMetadataNameSingular: CoreObjectNameSingular.Person,
},
},
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
}}
>
{children}
</FieldContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordTableContextProvider>
</RecoilRoot>
);
describe('useUpsertRecord', () => {
describe('useUpsertTableRecordInGroup', () => {
beforeEach(async () => {
createOneRecordMock.mockClear();
updateOneRecordMock.mockClear();
});
it('calls update record if there is no pending record', async () => {
const { result } = renderHook(
() =>
useUpsertRecord({
/**
* {
objectNameSingular: 'person',
recordTableId: 'recordTableId',
}),
}
*/
const { result } = renderHook(
() => useUpsertTableRecordInGroup(recordGroupId),
{
wrapper: ({ children }) =>
Wrapper({
@ -111,7 +136,7 @@ describe('useUpsertRecord', () => {
);
await act(async () => {
await result.current.upsertRecord(
await result.current.upsertTableRecordInGroup(
updateOneRecordMock,
'recordId',
'name',
@ -124,11 +149,7 @@ describe('useUpsertRecord', () => {
it('calls update record if pending record is empty', async () => {
const { result } = renderHook(
() =>
useUpsertRecord({
objectNameSingular: 'person',
recordTableId: 'recordTableId',
}),
() => useUpsertTableRecordInGroup(recordGroupId),
{
wrapper: ({ children }) =>
Wrapper({
@ -140,7 +161,7 @@ describe('useUpsertRecord', () => {
);
await act(async () => {
await result.current.upsertRecord(
await result.current.upsertTableRecordInGroup(
updateOneRecordMock,
'recordId',
'name',

View File

@ -0,0 +1,160 @@
import { renderHook } from '@testing-library/react';
import { ReactNode, act } from 'react';
import { RecoilRoot } from 'recoil';
import { createState } from 'twenty-ui';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { useUpsertTableRecordNoGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const draftValue = 'updated Name';
// Todo refactor this test to inject the states in a cleaner way instead of mocking hooks
// (this is not easy to maintain while refactoring)
jest.mock('@/object-record/hooks/useCreateOneRecord', () => ({
__esModule: true,
useCreateOneRecord: jest.fn(),
}));
const draftValueState = createState<string | null>({
key: 'draftValueState',
defaultValue: null,
});
jest.mock(
'@/object-record/record-field/hooks/internal/useRecordFieldInputStates',
() => ({
__esModule: true,
useRecordFieldInputStates: jest.fn(() => ({
getDraftValueSelector: () => draftValueState,
})),
}),
);
const pendingRecordIdState = createState<string | null>({
key: 'pendingRecordIdState',
defaultValue: null,
});
const createOneRecordMock = jest.fn();
const updateOneRecordMock = jest.fn();
(useCreateOneRecord as jest.Mock).mockReturnValue({
createOneRecord: createOneRecordMock,
});
const Wrapper = ({
children,
pendingRecordIdMockedValue,
draftValueMockedValue,
}: {
children: ReactNode;
pendingRecordIdMockedValue: string | null;
draftValueMockedValue: string | null;
}) => (
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
snapshot.set(pendingRecordIdState, pendingRecordIdMockedValue);
snapshot.set(draftValueState, draftValueMockedValue);
}}
>
<RecordTableContextProvider
recordTableId="recordTableId"
objectNameSingular={CoreObjectNameSingular.Person}
viewBarId="viewBarId"
>
<RecordTableComponentInstanceContext.Provider
value={{
instanceId: CoreObjectNamePlural.Person,
onColumnsChange: jest.fn(),
}}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: CoreObjectNamePlural.Person }}
>
<FieldContext.Provider
value={{
recordId: 'recordId',
fieldDefinition: {
...textfieldDefinition,
metadata: {
...textfieldDefinition.metadata,
objectMetadataNameSingular: CoreObjectNameSingular.Person,
},
},
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
}}
>
{children}
</FieldContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordTableContextProvider>
</RecoilRoot>
);
describe('useUpsertTableRecordNoGroup', () => {
beforeEach(async () => {
createOneRecordMock.mockClear();
updateOneRecordMock.mockClear();
});
it('calls update record if there is no pending record', async () => {
/**
* {
objectNameSingular: 'person',
recordTableId: 'recordTableId',
}
*/
const { result } = renderHook(() => useUpsertTableRecordNoGroup(), {
wrapper: ({ children }) =>
Wrapper({
pendingRecordIdMockedValue: null,
draftValueMockedValue: null,
children,
}),
});
await act(async () => {
await result.current.upsertTableRecordNoGroup(
updateOneRecordMock,
'recordId',
'name',
);
});
expect(createOneRecordMock).not.toHaveBeenCalled();
expect(updateOneRecordMock).toHaveBeenCalled();
});
it('calls update record if pending record is empty', async () => {
const { result } = renderHook(() => useUpsertTableRecordNoGroup(), {
wrapper: ({ children }) =>
Wrapper({
pendingRecordIdMockedValue: null,
draftValueMockedValue: draftValue,
children,
}),
});
await act(async () => {
await result.current.upsertTableRecordNoGroup(
updateOneRecordMock,
'recordId',
'name',
);
});
expect(createOneRecordMock).not.toHaveBeenCalled();
expect(updateOneRecordMock).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,87 @@
import { useRecoilCallback } from 'recoil';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
import { isDefined } from '~/utils/isDefined';
export const useUpsertTableRecordInGroup = (recordGroupId: string) => {
const { objectMetadataItem, objectNameSingular } =
useRecordTableContextOrThrow();
const { createOneRecord } = useCreateOneRecord({
objectNameSingular,
shouldMatchRootQueryFilter: true,
});
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
);
const upsertTableRecordInGroup = useRecoilCallback(
({ snapshot }) =>
(persistField: () => void, recordId: string, fieldName: string) => {
const labelIdentifierFieldMetadataItem =
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
const fieldScopeId = getScopeIdFromComponentId(
`${recordId}-${fieldName}`,
);
const draftValueSelector = extractComponentSelector(
recordFieldInputDraftValueComponentSelector,
fieldScopeId,
);
const draftValue = getSnapshotValue(snapshot, draftValueSelector());
// We're in a record group
const recordTablePendingRecordId = getSnapshotValue(
snapshot,
recordTablePendingRecordIdByGroupFamilyState(recordGroupId),
);
const recordGroupDefinition = getSnapshotValue(
snapshot,
recordGroupDefinitionFamilyState(recordGroupId),
);
const recordGroupFieldMetadataItem = objectMetadataItem.fields.find(
(fieldMetadata) =>
fieldMetadata.id === recordGroupDefinition?.fieldMetadataId,
);
if (
isDefined(recordTablePendingRecordId) &&
isDefined(recordGroupDefinition) &&
isDefined(recordGroupFieldMetadataItem) &&
isDefined(draftValue)
) {
createOneRecord({
id: recordTablePendingRecordId,
[labelIdentifierFieldMetadataItem?.name ?? 'name']: draftValue,
[recordGroupFieldMetadataItem.name]: recordGroupDefinition.value,
position: 'first',
});
} else if (!recordTablePendingRecordId) {
persistField();
}
},
[
createOneRecord,
objectMetadataItem,
recordGroupId,
recordTablePendingRecordIdByGroupFamilyState,
],
);
return { upsertTableRecordInGroup };
};

View File

@ -1,32 +1,22 @@
import { useRecoilCallback } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
import { isDefined } from '~/utils/isDefined';
export const useUpsertRecord = ({
objectNameSingular,
recordTableId,
}: {
objectNameSingular: string;
recordTableId: string;
}) => {
const hasRecordGroups = useRecoilComponentValueV2(
hasRecordGroupsComponentSelector,
);
export const useUpsertTableRecordNoGroup = () => {
const { objectMetadataItem, objectNameSingular, recordTableId } =
useRecordTableContextOrThrow();
const { createOneRecord } = useCreateOneRecord({
objectNameSingular,
shouldMatchRootQueryFilter: hasRecordGroups,
});
const recordTablePendingRecordIdState = useRecoilComponentCallbackStateV2(
@ -34,28 +24,12 @@ export const useUpsertRecord = ({
recordTableId,
);
const upsertRecord = useRecoilCallback(
const upsertTableRecordNoGroup = useRecoilCallback(
({ snapshot }) =>
(persistField: () => void, recordId: string, fieldName: string) => {
const objectMetadataItems = snapshot
.getLoadable(objectMetadataItemsState)
.getValue();
const foundObjectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === objectNameSingular,
);
if (!foundObjectMetadataItem) {
throw new Error('Object metadata item cannot be found');
}
const labelIdentifierFieldMetadataItem =
getLabelIdentifierFieldMetadataItem(foundObjectMetadataItem);
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
const recordTablePendingRecordId = getSnapshotValue(
snapshot,
recordTablePendingRecordIdState,
);
const fieldScopeId = getScopeIdFromComponentId(
`${recordId}-${fieldName}`,
);
@ -67,6 +41,11 @@ export const useUpsertRecord = ({
const draftValue = getSnapshotValue(snapshot, draftValueSelector());
const recordTablePendingRecordId = getSnapshotValue(
snapshot,
recordTablePendingRecordIdState,
);
if (isDefined(recordTablePendingRecordId) && isDefined(draftValue)) {
createOneRecord({
id: recordTablePendingRecordId,
@ -77,8 +56,8 @@ export const useUpsertRecord = ({
persistField();
}
},
[createOneRecord, objectNameSingular, recordTablePendingRecordIdState],
[createOneRecord, objectMetadataItem, recordTablePendingRecordIdState],
);
return { upsertRecord };
return { upsertTableRecordNoGroup };
};

View File

@ -0,0 +1,68 @@
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { isDefined } from '~/utils/isDefined';
export const useCreateNewTableRecordInGroup = () => {
const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
scopeId: recordIndexId,
});
const setHotkeyScope = useSetHotkeyScope();
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
recordIndexId,
);
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const createNewTableRecordInGroup = useRecoilCallback(
({ set }) =>
(recordGroupId: string) => {
const recordId = v4();
set(
recordTablePendingRecordIdByGroupFamilyState(recordGroupId),
recordId,
);
setSelectedTableCellEditMode(-1, 0);
setHotkeyScope(
DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes,
);
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
recordId,
objectMetadataItem.labelIdentifierFieldMetadataId,
'table-cell',
),
);
}
},
[
objectMetadataItem,
recordTablePendingRecordIdByGroupFamilyState,
setActiveDropdownFocusIdAndMemorizePrevious,
setHotkeyScope,
setSelectedTableCellEditMode,
],
);
return {
createNewTableRecordInGroup,
};
};

View File

@ -1,33 +1,94 @@
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useContext } from 'react';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { isDefined } from '~/utils/isDefined';
export const useCreateNewTableRecord = (recordTableIdFromProps?: string) => {
const { recordTableId } = useContext(RecordTableContext);
const recordTableIdToUse = recordTableIdFromProps ?? recordTableId;
export const useCreateNewTableRecord = (recordTableId: string) => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
scopeId: recordTableIdToUse,
scopeId: recordTableId,
});
const setHotkeyScope = useSetHotkeyScope();
const { setPendingRecordId } = useRecordTable({
recordTableId: recordTableIdToUse,
});
const setPendingRecordId = useSetRecoilComponentStateV2(
recordTablePendingRecordIdComponentState,
recordTableId,
);
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
recordTableId,
);
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const createNewTableRecord = () => {
setPendingRecordId(v4());
const recordId = v4();
setPendingRecordId(recordId);
setSelectedTableCellEditMode(-1, 0);
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
recordId,
objectMetadataItem.labelIdentifierFieldMetadataId,
'table-cell',
),
);
}
};
const createNewTableRecordInGroup = useRecoilCallback(
({ set }) =>
(recordGroupId: string) => {
const recordId = v4();
set(
recordTablePendingRecordIdByGroupFamilyState(recordGroupId),
recordId,
);
setSelectedTableCellEditMode(-1, 0);
setHotkeyScope(
DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes,
);
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
recordId,
objectMetadataItem.labelIdentifierFieldMetadataId,
'table-cell',
),
);
}
},
[
objectMetadataItem,
recordTablePendingRecordIdByGroupFamilyState,
setActiveDropdownFocusIdAndMemorizePrevious,
setHotkeyScope,
setSelectedTableCellEditMode,
],
);
return {
createNewTableRecord,
createNewTableRecordInGroup,
};
};

View File

@ -22,7 +22,6 @@ import { onColumnsChangeComponentState } from '@/object-record/record-table/stat
import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState';
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
@ -240,11 +239,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
const isSomeCellInEditModeState =
useGetIsSomeCellInEditModeState(recordTableId);
const setPendingRecordId = useSetRecoilComponentStateV2(
recordTablePendingRecordIdComponentState,
recordTableId,
);
return {
onColumnsChange,
setAvailableTableColumns,
@ -272,6 +266,5 @@ export const useRecordTable = (props?: useRecordTableProps) => {
setHasUserSelectedAllRows,
setOnToggleColumnFilter,
setOnToggleColumnSort,
setPendingRecordId,
};
};

View File

@ -1,12 +1,12 @@
import { DragDropContext, DropResult } from '@hello-pangea/dnd';
import { ReactNode, useContext } from 'react';
import { ReactNode } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
@ -18,7 +18,7 @@ export const RecordTableBodyDragDropContextProvider = ({
}: {
children: ReactNode;
}) => {
const { objectNameSingular, recordTableId } = useContext(RecordTableContext);
const { objectNameSingular, recordTableId } = useRecordTableContextOrThrow();
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
objectNameSingular,

View File

@ -1,5 +1,5 @@
import { DragDropContext, DropResult } from '@hello-pangea/dnd';
import { ReactNode, useContext } from 'react';
import { ReactNode } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
@ -7,7 +7,7 @@ import { getDraggedRecordPosition } from '@/object-record/record-board/utils/get
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
@ -20,7 +20,7 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
children: ReactNode;
}) => {
const { objectNameSingular, recordTableId, objectMetadataItem } =
useContext(RecordTableContext);
useRecordTableContextOrThrow();
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
objectNameSingular,

View File

@ -1,6 +1,7 @@
import { Key } from 'ts-key-enum';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
@ -9,13 +10,13 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
type RecordTableBodyUnselectEffectProps = {
tableBodyRef: React.RefObject<HTMLDivElement>;
recordTableId: string;
};
export const RecordTableBodyUnselectEffect = ({
tableBodyRef,
recordTableId,
}: RecordTableBodyUnselectEffectProps) => {
const { recordTableId } = useRecordTableContextOrThrow();
const leaveTableFocus = useLeaveTableFocus(recordTableId);
const { resetTableRowSelection, useMapKeyboardToSoftFocus } = useRecordTable({

View File

@ -1,4 +1,5 @@
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableNoRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider';
import { RecordTableNoRecordGroupRows } from '@/object-record/record-table/components/RecordTableNoRecordGroupRows';
import { RecordTableBodyDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider';
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
@ -21,11 +22,13 @@ export const RecordTableNoRecordGroupBody = () => {
}
return (
<RecordTableBodyDragDropContextProvider>
<RecordTableBodyDroppable>
<RecordTablePendingRow />
<RecordTableNoRecordGroupRows />
</RecordTableBodyDroppable>
</RecordTableBodyDragDropContextProvider>
<RecordTableNoRecordGroupBodyContextProvider>
<RecordTableBodyDragDropContextProvider>
<RecordTableBodyDroppable>
<RecordTablePendingRow />
<RecordTableNoRecordGroupRows />
</RecordTableBodyDroppable>
</RecordTableBodyDragDropContextProvider>
</RecordTableNoRecordGroupBodyContextProvider>
);
};

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
@ -6,7 +6,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
@ -18,7 +18,7 @@ import { isNonEmptyString, isNull } from '@sniptt/guards';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
export const RecordTableNoRecordGroupBodyEffect = () => {
const { objectNameSingular } = useContext(RecordTableContext);
const { objectNameSingular } = useRecordTableContextOrThrow();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
@ -7,13 +7,13 @@ import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useC
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
export const RecordTableRecordGroupBodyEffect = () => {
const { objectNameSingular } = useContext(RecordTableContext);
const { objectNameSingular } = useRecordTableContextOrThrow();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);

View File

@ -1,13 +1,13 @@
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider';
import { RecordTableRecordGroupRows } from '@/object-record/record-table/components/RecordTableRecordGroupRows';
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow';
import { RecordTableRecordGroupEmptyRow } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow';
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -30,20 +30,19 @@ export const RecordTableRecordGroupsBody = () => {
return (
<RecordTableBodyRecordGroupDragDropContextProvider>
<RecordTableBodyDroppable isDropDisabled>
<RecordTablePendingRow />
</RecordTableBodyDroppable>
{visibleRecordGroupIds.map((recordGroupId) => (
<RecordGroupContext.Provider
{visibleRecordGroupIds.map((recordGroupId, index) => (
<RecordTableRecordGroupBodyContextProvider
key={recordGroupId}
value={{ recordGroupId }}
recordGroupId={recordGroupId}
>
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
<RecordTableRecordGroupSection />
<RecordTableRecordGroupRows />
<RecordTableRecordGroupSectionLoadMore />
</RecordTableBodyDroppable>
</RecordGroupContext.Provider>
{index > 0 && <RecordTableRecordGroupEmptyRow />}
<RecordGroupContext.Provider value={{ recordGroupId }}>
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
<RecordTableRecordGroupSection />
<RecordTableRecordGroupRows />
</RecordTableBodyDroppable>
</RecordGroupContext.Provider>
</RecordTableRecordGroupBodyContextProvider>
))}
</RecordTableBodyRecordGroupDragDropContextProvider>
);

View File

@ -5,8 +5,8 @@ import { BORDER_COMMON, ThemeContext } from 'twenty-ui';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import {
DEFAULT_CELL_SCOPE,
useOpenRecordTableCellFromCell,
@ -47,7 +47,7 @@ export const RecordTableCellBaseContainer = ({
const { hasSoftFocus, cellPosition } = useContext(RecordTableCellContext);
const { onMoveSoftFocusToCell, onCellMouseEnter } =
useContext(RecordTableContext);
useRecordTableBodyContextOrThrow();
const handleContainerMouseMove = () => {
setIsFocused(true);

View File

@ -1,5 +1,5 @@
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useContext } from 'react';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
@ -7,9 +7,10 @@ export const RecordTableCellDisplayMode = ({
children,
softFocus,
}: React.PropsWithChildren<{ softFocus?: boolean }>) => {
const { onActionMenuDropdownOpened } = useContext(RecordTableContext);
const { recordId } = useContext(FieldContext);
const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
const handleActionMenuDropdown = (event: React.MouseEvent) => {
onActionMenuDropdownOpened(event, recordId);
};

View File

@ -6,7 +6,7 @@ import { isFieldRelation } from '@/object-record/record-field/types/guards/isFie
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
@ -18,8 +18,10 @@ export const RecordTableCellFieldContextWrapper = ({
}: {
children: ReactNode;
}) => {
const { objectMetadataItem } = useContext(RecordTableContext);
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId, pathToShowPage } = useContext(RecordTableRowContext);
const updateRecord = useContext(RecordUpdateContext);

View File

@ -4,17 +4,18 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { useRecoilCallback } from 'recoil';
export const RecordTableCellFieldInput = () => {
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
useContext(RecordTableContext);
const { recordId, fieldDefinition } = useContext(FieldContext);
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
useRecordTableBodyContextOrThrow();
const isFieldReadOnly = useIsFieldValueReadOnly();
const handleEnter: FieldInputEvent = (persistField) => {

View File

@ -3,19 +3,25 @@ import { useContext } from 'react';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { css } from '@emotion/react';
import { IconListViewGrip } from 'twenty-ui';
const StyledContainer = styled.div`
cursor: grab;
width: 16px;
height: 32px;
z-index: 200;
display: flex;
&:hover .icon {
opacity: 1;
}
const StyledContainer = styled.div<{ isPendingRow?: boolean }>`
border-color: transparent;
cursor: grab;
display: flex;
height: 32px;
width: 16px;
${({ isPendingRow }) =>
!isPendingRow
? css`
&:hover .icon {
opacity: 1;
}
`
: ''};
z-index: 200;
`;
const StyledIconWrapper = styled.div<{ isDragging: boolean }>`
@ -24,7 +30,9 @@ const StyledIconWrapper = styled.div<{ isDragging: boolean }>`
`;
export const RecordTableCellGrip = () => {
const { dragHandleProps, isDragging } = useContext(RecordTableRowContext);
const { dragHandleProps, isDragging, isPendingRow } = useContext(
RecordTableRowContext,
);
return (
<RecordTableTd
@ -34,7 +42,7 @@ export const RecordTableCellGrip = () => {
hasRightBorder={false}
hasBottomBorder={false}
>
<StyledContainer>
<StyledContainer isPendingRow={isPendingRow}>
<StyledIconWrapper className="icon" isDragging={isDragging}>
<IconListViewGrip />
</StyledIconWrapper>

View File

@ -21,7 +21,7 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
type RecordTableCellSoftFocusModeProps = {
@ -36,6 +36,8 @@ export const RecordTableCellSoftFocusMode = ({
const { columnIndex } = useContext(RecordTableCellContext);
const { recordId } = useContext(FieldContext);
const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
const isFieldReadOnly = useIsFieldValueReadOnly();
const { openTableCell } = useOpenRecordTableCellFromCell();
@ -135,8 +137,6 @@ export const RecordTableCellSoftFocusMode = ({
*/
};
const { onActionMenuDropdownOpened } = useContext(RecordTableContext);
const handleActionMenuDropdown = (event: React.MouseEvent) => {
onActionMenuDropdownOpened(event, recordId);
};

View File

@ -0,0 +1,101 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import {
recordTableCell,
recordTableRow,
} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup';
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const setHotkeyScope = jest.fn();
jest.mock('@/ui/utilities/hotkey/hooks/useSetHotkeyScope', () => ({
useSetHotkeyScope: () => setHotkeyScope,
}));
const onColumnsChange = jest.fn();
const recordTableId = 'scopeId';
const recordGroupId = 'recordGroupId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
}}
>
<RecordTableComponentInstance
recordTableId={recordTableId}
onColumnsChange={onColumnsChange}
>
<RecordTableContextProvider
recordTableId={recordTableId}
viewBarId="viewBarId"
objectNameSingular={CoreObjectNameSingular.Person}
>
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
}}
>
<RecordTableRowContext.Provider value={recordTableRow}>
<RecordTableCellContext.Provider
value={{ ...recordTableCell, columnIndex: 0 }}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>
</RecoilRoot>
);
describe('useCloseRecordTableCellInGroup', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => {
const currentTableCellInEditModePosition = useRecoilComponentValueV2(
currentTableCellInEditModePositionComponentState,
);
const isTableCellInEditMode = useRecoilComponentFamilyValueV2(
isTableCellInEditModeComponentFamilyState,
currentTableCellInEditModePosition,
);
return {
...useCloseRecordTableCellInGroup(recordGroupId),
...useDragSelect(),
isTableCellInEditMode,
};
},
{
wrapper: Wrapper,
},
);
act(() => {
result.current.closeTableCellInGroup();
});
expect(result.current.isDragSelectionStartEnabled()).toBe(true);
expect(result.current.isTableCellInEditMode).toBe(false);
expect(setHotkeyScope).toHaveBeenCalledWith('table-soft-focus');
});
});

View File

@ -1,22 +1,26 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import {
recordTableCell,
recordTableRow,
} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
import { useCloseRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell';
import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup';
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const setHotkeyScope = jest.fn();
@ -28,32 +32,42 @@ const onColumnsChange = jest.fn();
const recordTableId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
}}
>
<RecordTableComponentInstance
recordTableId={recordTableId}
onColumnsChange={onColumnsChange}
>
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
}}
<RecordTableContextProvider
recordTableId={recordTableId}
viewBarId="viewBarId"
objectNameSingular={CoreObjectNameSingular.Person}
>
<RecordTableRowContext.Provider value={recordTableRow}>
<RecordTableCellContext.Provider
value={{ ...recordTableCell, columnIndex: 0 }}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
</FieldContext.Provider>
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
}}
>
<RecordTableRowContext.Provider value={recordTableRow}>
<RecordTableCellContext.Provider
value={{ ...recordTableCell, columnIndex: 0 }}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowContext.Provider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>
</RecoilRoot>
);
describe('useCloseRecordTableCell', () => {
describe('useCloseRecordTableCellNoGroup', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => {
@ -65,7 +79,7 @@ describe('useCloseRecordTableCell', () => {
currentTableCellInEditModePosition,
);
return {
...useCloseRecordTableCell(),
...useCloseRecordTableCellNoGroup(),
...useDragSelect(),
isTableCellInEditMode,
};
@ -76,7 +90,7 @@ describe('useCloseRecordTableCell', () => {
);
act(() => {
result.current.closeTableCell();
result.current.closeTableCellNoGroup();
});
expect(result.current.isDragSelectionStartEnabled()).toBe(true);

View File

@ -0,0 +1,56 @@
import { useRecoilCallback } from 'recoil';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
export const useCloseRecordTableCellInGroup = (recordGroupId: string) => {
const { recordTableId } = useRecordTableContextOrThrow();
const setHotkeyScope = useSetHotkeyScope();
const { setDragSelectionStartEnabled } = useDragSelect();
const { toggleClickOutsideListener } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
);
const closeCurrentTableCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableId);
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
recordTableId,
);
const closeTableCellInGroup = useRecoilCallback(
({ reset }) =>
() => {
toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
reset(recordTablePendingRecordIdByGroupFamilyState(recordGroupId));
},
[
closeCurrentTableCellInEditMode,
recordGroupId,
recordTablePendingRecordIdByGroupFamilyState,
setDragSelectionStartEnabled,
setHotkeyScope,
toggleClickOutsideListener,
],
);
return {
closeTableCellInGroup,
};
};

View File

@ -5,13 +5,18 @@ import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useCloseCurrentTableCellInEditMode } from '../../hooks/internal/useCloseCurrentTableCellInEditMode';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useCallback } from 'react';
export const useCloseRecordTableCellNoGroup = () => {
const { recordTableId } = useRecordTableContextOrThrow();
export const useCloseRecordTableCellV2 = (recordTableId: string) => {
const setHotkeyScope = useSetHotkeyScope();
const { setDragSelectionStartEnabled } = useDragSelect();
const { toggleClickOutsideListener } = useClickOutsideListener(
@ -25,18 +30,25 @@ export const useCloseRecordTableCellV2 = (recordTableId: string) => {
recordTablePendingRecordIdComponentState,
recordTableId,
);
const resetRecordTablePendingRecordId =
useResetRecoilState(pendingRecordIdState);
const closeTableCell = async () => {
const closeTableCellNoGroup = useCallback(() => {
toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
resetRecordTablePendingRecordId();
};
}, [
closeCurrentTableCellInEditMode,
resetRecordTablePendingRecordId,
setDragSelectionStartEnabled,
setHotkeyScope,
toggleClickOutsideListener,
]);
return {
closeTableCell,
closeTableCellNoGroup,
};
};

View File

@ -1,38 +0,0 @@
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useResetRecoilState } from 'recoil';
import { useCloseCurrentTableCellInEditMode } from '../../hooks/internal/useCloseCurrentTableCellInEditMode';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const useCloseRecordTableCell = () => {
const setHotkeyScope = useSetHotkeyScope();
const { setDragSelectionStartEnabled } = useDragSelect();
const { toggleClickOutsideListener } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
);
const closeCurrentTableCellInEditMode = useCloseCurrentTableCellInEditMode();
const pendingRecordIdState = useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdComponentState,
);
const resetRecordTablePendingRecordId =
useResetRecoilState(pendingRecordIdState);
const closeTableCell = async () => {
toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
resetRecordTablePendingRecordId();
};
return {
closeTableCell,
};
};

View File

@ -5,12 +5,12 @@ import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useI
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
@ -28,13 +28,16 @@ export type OpenTableCellArgs = {
};
export const useOpenRecordTableCellFromCell = () => {
const { onOpenTableCell } = useContext(RecordTableContext);
const cellPosition = useCurrentTableCellPosition();
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
const { recordId, fieldDefinition } = useContext(FieldContext);
const { pathToShowPage, objectNameSingular } = useContext(
RecordTableRowContext,
);
const { onOpenTableCell } = useRecordTableBodyContextOrThrow();
const cellPosition = useCurrentTableCellPosition();
const isFieldReadOnly = useIsFieldValueReadOnly();
const openTableCell = (

View File

@ -20,12 +20,11 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { isDefined } from '~/utils/isDefined';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
@ -49,7 +48,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const { getClickOutsideListenerIsActivatedState } =
useClickOustideListenerStates(RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID);
const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext);
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const moveEditModeToTableCellPosition =
useMoveEditModeToTableCellPosition(tableScopeId);

View File

@ -1,12 +1,11 @@
import styled from '@emotion/styled';
import { MOBILE_VIEWPORT } from 'twenty-ui';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell';
import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn';
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
const StyledTableHead = styled.thead`
cursor: pointer;
@ -77,14 +76,8 @@ const StyledTableHead = styled.thead`
}
`;
export const RecordTableHeader = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
);
export const RecordTableHeader = () => {
const { visibleTableColumns } = useRecordTableContextOrThrow();
return (
<StyledTableHead id="record-table-header" data-select-disable>
@ -92,11 +85,7 @@ export const RecordTableHeader = ({
<RecordTableHeaderDragDropColumn />
<RecordTableHeaderCheckboxColumn />
{visibleTableColumns.map((column) => (
<RecordTableHeaderCell
key={column.fieldMetadataId}
column={column}
objectNameSingular={objectNameSingular}
/>
<RecordTableHeaderCell key={column.fieldMetadataId} column={column} />
))}
<RecordTableHeaderLastColumn />
</tr>

View File

@ -3,9 +3,9 @@ import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { IconPlus, LightIconButton } from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
@ -95,16 +95,14 @@ const StyledHeaderIcon = styled.div`
margin: ${({ theme }) => theme.spacing(1, 1, 1, 1.5)};
`;
type RecordTableHeaderCellProps = {
column: ColumnDefinition<FieldMetadata>;
};
export const RecordTableHeaderCell = ({
column,
objectNameSingular,
}: {
column: ColumnDefinition<FieldMetadata>;
objectNameSingular: string;
}) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
}: RecordTableHeaderCellProps) => {
const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow();
const resizeFieldOffsetState = useRecoilComponentCallbackStateV2(
resizeFieldOffsetComponentState,
@ -199,7 +197,7 @@ export const RecordTableHeaderCell = ({
const disableColumnResize =
column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft;
const { createNewTableRecord } = useCreateNewTableRecord();
const { createNewTableRecord } = useCreateNewTableRecord(recordTableId);
const handlePlusButtonClick = () => {
createNewTableRecord();

View File

@ -1,11 +1,11 @@
import { useCallback, useContext } from 'react';
import { useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { IconSettings, MenuItem, UndecoratedLink, useIcons } from 'twenty-ui';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
@ -16,7 +16,8 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableHeaderPlusButtonContent = () => {
const { objectMetadataItem } = useContext(RecordTableContext);
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { closeDropdown } = useDropdown();
const hiddenTableColumns = useRecoilComponentValueV2(

View File

@ -0,0 +1,25 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
export const RecordTablePendingRecordGroupRow = () => {
const currentRecordGroupId = useCurrentRecordGroupId();
const pendingRecordId = useRecoilComponentFamilyValueV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
currentRecordGroupId,
);
if (!pendingRecordId) return <></>;
return (
<RecordTableRow
key={pendingRecordId}
recordId={pendingRecordId}
rowIndexForDrag={-1}
rowIndexForFocus={-1}
isPendingRow
/>
);
};

View File

@ -4,8 +4,8 @@ import { ReactNode, useContext, useEffect, useRef } from 'react';
import { useInView } from 'react-intersection-observer';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
@ -31,8 +31,8 @@ export const RecordTableRowWrapper = ({
}: RecordTableRowWrapperProps) => {
const trRef = useRef<HTMLTableRowElement>(null);
const { objectMetadataItem } = useContext(RecordTableContext);
const { onIndexRecordsLoaded } = useContext(RecordIndexRootPropsContext);
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { onIndexRecordsLoaded } = useRecordIndexContextOrThrow();
const theme = useTheme();
@ -78,7 +78,12 @@ export const RecordTableRowWrapper = ({
}, [inView, onIndexRecordsLoaded]);
return (
<Draggable key={recordId} draggableId={recordId} index={rowIndexForDrag}>
<Draggable
key={recordId}
draggableId={recordId}
index={rowIndexForDrag}
isDragDisabled={isPendingRow}
>
{(draggableProvided, draggableSnapshot) => (
<RecordTableTr
ref={(node) => {

View File

@ -0,0 +1,7 @@
import styled from '@emotion/styled';
const StyledTrContainer = styled.tr`
height: 32px;
`;
export const RecordTableRecordGroupEmptyRow = StyledTrContainer;

View File

@ -0,0 +1,36 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { IconPlus } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
export const RecordTableRecordGroupSectionAddNew = () => {
const { recordTableId } = useRecordTableContextOrThrow();
const currentRecordGroupId = useCurrentRecordGroupId();
const pendingRecordId = useRecoilComponentFamilyValueV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
currentRecordGroupId,
);
const { createNewTableRecordInGroup } =
useCreateNewTableRecord(recordTableId);
const handleAddNewRecord = () => {
createNewTableRecordInGroup(currentRecordGroupId);
};
if (isDefined(pendingRecordId)) return null;
return (
<RecordTableActionRow
LeftIcon={IconPlus}
text="Add new"
onClick={handleAddNewRecord}
/>
);
};

View File

@ -1,14 +1,13 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useContext } from 'react';
import { IconArrowDown } from 'twenty-ui';
export const RecordTableRecordGroupSectionLoadMore = () => {
const { objectNameSingular } = useContext(RecordTableContext);
const { objectNameSingular } = useRecordTableContextOrThrow();
const currentRecordGroupId = useCurrentRecordGroupId();

View File

@ -0,0 +1,10 @@
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
export const recordTablePendingRecordIdByGroupComponentFamilyState =
createComponentFamilyStateV2<string | null, RecordGroupDefinition['id']>({
key: 'recordTablePendingRecordIdByGroupComponentFamilyState',
defaultValue: null,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -4,7 +4,7 @@ import { createComponentStateV2 } from '@/ui/utilities/state/component-state/uti
export const recordTablePendingRecordIdComponentState = createComponentStateV2<
string | null
>({
key: 'recordTablePendingRecordIdState',
key: 'recordTablePendingRecordIdComponentState',
defaultValue: null,
componentInstanceContext: RecordTableComponentInstanceContext,
});

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
import { ViewBar } from '@/views/components/ViewBar';
@ -28,7 +28,7 @@ export const SignInBackgroundMockContainer = () => {
return (
<StyledContainer>
<RecordIndexRootPropsContext.Provider
<RecordIndexContextProvider
value={{
recordIndexId,
objectNamePlural,
@ -36,7 +36,6 @@ export const SignInBackgroundMockContainer = () => {
objectMetadataItem,
onIndexRecordsLoaded: () => {},
indexIdentifierUrl: () => '',
onCreateRecord: () => {},
}}
>
<ViewComponentInstanceContext.Provider
@ -69,7 +68,7 @@ export const SignInBackgroundMockContainer = () => {
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordIndexRootPropsContext.Provider>
</RecordIndexContextProvider>
</StyledContainer>
);
};

View File

@ -1,6 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
@ -18,7 +18,6 @@ import { GraphQLView } from '@/views/types/GraphQLView';
import { View } from '@/views/types/View';
import { ViewGroup } from '@/views/types/ViewGroup';
import { ViewType } from '@/views/types/ViewType';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-ui';
import { v4 } from 'uuid';
@ -57,7 +56,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const { createViewFilterGroupRecords } = usePersistViewFilterGroupRecords();
const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const createViewFromCurrentView = useRecoilCallback(
({ snapshot, set }) =>

View File

@ -12,9 +12,8 @@ import { RecordIndexContainer } from '@/object-record/record-index/components/Re
import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect';
import { RecordIndexContainerContextStoreObjectMetadataEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect';
import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
@ -41,12 +40,6 @@ export const RecordIndexPage = () => {
objectNameSingular,
});
const { createNewTableRecord } = useCreateNewTableRecord(recordIndexId);
const handleCreateRecord = () => {
createNewTableRecord();
};
const { indexIdentifierUrl } = useHandleIndexIdentifierClick({
objectMetadataItem,
recordIndexId,
@ -63,7 +56,7 @@ export const RecordIndexPage = () => {
return (
<PageContainer>
<RecordIndexRootPropsContext.Provider
<RecordIndexContextProvider
value={{
recordIndexId,
objectNamePlural,
@ -71,7 +64,6 @@ export const RecordIndexPage = () => {
objectMetadataItem,
onIndexRecordsLoaded: handleIndexRecordsLoaded,
indexIdentifierUrl,
onCreateRecord: handleCreateRecord,
}}
>
<ViewComponentInstanceContext.Provider
@ -100,7 +92,7 @@ export const RecordIndexPage = () => {
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordIndexRootPropsContext.Provider>
</RecordIndexContextProvider>
</PageContainer>
);
};

View File

@ -2,7 +2,8 @@ import { Decorator } from '@storybook/react';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { isDefined } from 'twenty-ui';
export const RecordTableDecorator: Decorator = (Story) => {
@ -17,23 +18,28 @@ export const RecordTableDecorator: Decorator = (Story) => {
}
return (
<RecordTableContext.Provider
<RecordTableContextProvider
value={{
objectNameSingular: personObjectMetadataItem?.nameSingular,
objectNameSingular: personObjectMetadataItem.nameSingular,
objectMetadataItem: personObjectMetadataItem,
onCellMouseEnter: () => {},
onCloseTableCell: () => {},
onOpenTableCell: () => {},
onActionMenuDropdownOpened: () => {},
onMoveFocus: () => {},
onMoveSoftFocusToCell: () => {},
onUpsertRecord: () => {},
recordTableId: 'persons',
viewBarId: 'view-bar',
visibleTableColumns: [],
}}
>
<Story />
</RecordTableContext.Provider>
<RecordTableBodyContextProvider
value={{
onCellMouseEnter: () => {},
onCloseTableCell: () => {},
onOpenTableCell: () => {},
onActionMenuDropdownOpened: () => {},
onMoveFocus: () => {},
onMoveSoftFocusToCell: () => {},
onUpsertRecord: () => {},
}}
>
<Story />
</RecordTableBodyContextProvider>
</RecordTableContextProvider>
);
};

View File

@ -0,0 +1,20 @@
import React, { useContext } from 'react';
export const createRequiredContext = <TContext>(debugName: string) => {
const Context = React.createContext<TContext | undefined>(undefined);
Context.displayName = `${debugName}Provider`;
const useRequiredContextOrThrow = (): TContext => {
const context = useContext(Context);
if (context === undefined) {
throw new Error(
`${debugName} Context not found. Please wrap your component tree with <${Context.displayName}> before using use${debugName}OrThrow().`,
);
}
return context;
};
return [Context.Provider, useRequiredContextOrThrow] as const;
};

View File

@ -1,11 +0,0 @@
import { Context, createContext } from 'react';
type RootProps = Record<string, any>;
export type RootPropsContext<T extends RootProps> = T extends RootProps
? T
: never;
export const createRootPropsContext = <T extends RootProps>(): Context<
RootPropsContext<T>
> => createContext<RootPropsContext<T>>({} as RootPropsContext<T>);