Fix aggregate bar (#11620)

Closes https://github.com/twentyhq/twenty/issues/10943

Also adds stories:
<img width="1512" alt="image"
src="https://github.com/user-attachments/assets/377059b1-f6b5-4d8c-b7d1-e74e70448445"
/>
This commit is contained in:
Charles Bochet
2025-04-17 15:29:20 +02:00
committed by GitHub
parent d2881bb4a2
commit 56874bf84b
14 changed files with 339 additions and 98 deletions

View File

@ -23,8 +23,8 @@ import { ViewField } from '@/views/types/ViewField';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions'; import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useLoadRecordIndexStates = () => { export const useLoadRecordIndexStates = () => {
const setContextStoreTargetedRecordsRuleComponentState = const setContextStoreTargetedRecordsRuleComponentState =

View File

@ -0,0 +1,117 @@
import { Meta, StoryObj } from '@storybook/react';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
import { fireEvent, userEvent, within } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedViewsData } from '~/testing/mock-data/views';
import { sleep } from '~/utils/sleep';
const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTable',
component: RecordTableWithWrappers,
decorators: [
ComponentDecorator,
MemoryRouterDecorator,
RecordTableDecorator,
ContextStoreDecorator,
SnackBarDecorator,
ObjectMetadataItemsDecorator,
I18nFrontDecorator,
],
args: {
recordTableId: `companies-${mockedViewsData[0].id}`,
viewBarId: 'view-bar',
objectNameSingular: 'company',
},
parameters: {
recordTableObjectNameSingular: 'company',
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RecordTableEmptyStateNoGroupNoRecordAtAll>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Linkedin');
},
};
export const HeaderMenuOpen: Story = {
play: async () => {
const canvas = within(document.body);
await canvas.findByText('Linkedin');
const headerMenuButton = await canvas.findByText('Domain Name');
await userEvent.click(headerMenuButton);
await canvas.findByText('Move right');
},
};
export const ScrolledLeft: Story = {
play: async () => {
const canvas = within(document.body);
await canvas.findByText('Linkedin');
const scrollWrapper = document.body.querySelector(
'.scroll-wrapper-x-enabled',
);
if (!scrollWrapper) {
throw new Error('Scroll wrapper not found');
}
await sleep(1000);
fireEvent.scroll(scrollWrapper, {
target: {
scrollLeft: 100,
},
});
await canvas.findByText('Facebook');
},
};
export const ScrolledBottom: Story = {
parameters: {
container: {
height: 300,
},
},
play: async () => {
const canvas = within(document.body);
await canvas.findByText('Linkedin');
const scrollWrapper = document.body.querySelector(
'.scroll-wrapper-y-enabled',
);
if (!scrollWrapper) {
throw new Error('Scroll wrapper not found');
}
await sleep(1000);
fireEvent.scroll(scrollWrapper, {
target: {
scrollTop: 80,
},
});
await canvas.findByText('Facebook');
},
};

View File

@ -64,7 +64,7 @@ export const RecordTable = () => {
tableBodyRef={tableBodyRef} tableBodyRef={tableBodyRef}
/> />
{recordTableIsEmpty ? ( {recordTableIsEmpty && !hasRecordGroups ? (
<RecordTableEmpty <RecordTableEmpty
tableBodyRef={tableBodyRef} tableBodyRef={tableBodyRef}
hasRecordGroups={hasRecordGroups} hasRecordGroups={hasRecordGroups}

View File

@ -1,6 +1,5 @@
import { StyledTable } from '@/object-record/record-table/components/RecordTableStyles'; import { StyledTable } from '@/object-record/record-table/components/RecordTableStyles';
import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState'; import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
export interface RecordTableEmptyProps { export interface RecordTableEmptyProps {
@ -8,18 +7,11 @@ export interface RecordTableEmptyProps {
hasRecordGroups: boolean; hasRecordGroups: boolean;
} }
export const RecordTableEmpty = ({ export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => (
tableBodyRef,
hasRecordGroups,
}: RecordTableEmptyProps) => (
<> <>
<StyledTable ref={tableBodyRef}> <StyledTable ref={tableBodyRef}>
<RecordTableHeader /> <RecordTableHeader />
</StyledTable> </StyledTable>
{hasRecordGroups ? ( <RecordTableEmptyState />
<RecordTableRecordGroupsBody />
) : (
<RecordTableEmptyState />
)}
</> </>
); );

View File

@ -1,35 +1,40 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll'; import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { ComponentDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator'; import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = { const meta: Meta = {
title: title:
'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoGroupNoRecordAtAll', 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoGroupNoRecordAtAll',
component: RecordTableEmptyStateNoGroupNoRecordAtAll, component: RecordTableEmptyStateNoGroupNoRecordAtAll,
decorators: [ decorators: [
(Story) => (
<RecordTableContextProvider
recordTableId="persons"
viewBarId="view-bar"
objectNameSingular="person"
>
<Story />
</RecordTableContextProvider>
),
ComponentDecorator, ComponentDecorator,
MemoryRouterDecorator, MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator, RecordTableDecorator,
(Story) => ( ContextStoreDecorator,
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> SnackBarDecorator,
<RecordTableComponentInstance ObjectMetadataItemsDecorator,
recordTableId="persons" I18nFrontDecorator,
onColumnsChange={() => {}}
>
<Story />
</RecordTableComponentInstance>
</SnackBarProviderScope>
),
], ],
parameters: { parameters: {
recordTableObjectNameSingular: 'person',
msw: graphqlMocks, msw: graphqlMocks,
}, },
}; };

View File

@ -1,35 +1,40 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter'; import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { ComponentDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator'; import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = { const meta: Meta = {
title: title:
'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoRecordFoundForFilter', 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoRecordFoundForFilter',
component: RecordTableEmptyStateNoRecordFoundForFilter, component: RecordTableEmptyStateNoRecordFoundForFilter,
decorators: [ decorators: [
(Story) => (
<RecordTableContextProvider
recordTableId="persons"
viewBarId="view-bar"
objectNameSingular="person"
>
<Story />
</RecordTableContextProvider>
),
ComponentDecorator, ComponentDecorator,
MemoryRouterDecorator, MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator, RecordTableDecorator,
(Story) => ( ContextStoreDecorator,
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> SnackBarDecorator,
<RecordTableComponentInstance ObjectMetadataItemsDecorator,
recordTableId="persons" I18nFrontDecorator,
onColumnsChange={() => {}}
>
<Story />
</RecordTableComponentInstance>
</SnackBarProviderScope>
),
], ],
parameters: { parameters: {
recordTableObjectNameSingular: 'person',
msw: graphqlMocks, msw: graphqlMocks,
}, },
}; };

View File

@ -1,34 +1,39 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote'; import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { ComponentDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator'; import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = { const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateRemote', title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateRemote',
component: RecordTableEmptyStateRemote, component: RecordTableEmptyStateRemote,
decorators: [ decorators: [
(Story) => (
<RecordTableContextProvider
recordTableId="persons"
viewBarId="view-bar"
objectNameSingular="person"
>
<Story />
</RecordTableContextProvider>
),
ComponentDecorator, ComponentDecorator,
MemoryRouterDecorator, MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator, RecordTableDecorator,
(Story) => ( ContextStoreDecorator,
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> SnackBarDecorator,
<RecordTableComponentInstance ObjectMetadataItemsDecorator,
recordTableId="persons" I18nFrontDecorator,
onColumnsChange={() => {}}
>
<Story />
</RecordTableComponentInstance>
</SnackBarProviderScope>
),
], ],
parameters: { parameters: {
recordTableObjectNameSingular: 'person',
msw: graphqlMocks, msw: graphqlMocks,
}, },
}; };

View File

@ -1,39 +1,39 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete'; import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { ComponentDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator'; import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = { const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateSoftDelete', title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateSoftDelete',
component: RecordTableEmptyStateSoftDelete, component: RecordTableEmptyStateSoftDelete,
decorators: [ decorators: [
(Story) => (
<RecordTableContextProvider
recordTableId="persons"
viewBarId="view-bar"
objectNameSingular="person"
>
<Story />
</RecordTableContextProvider>
),
ComponentDecorator, ComponentDecorator,
MemoryRouterDecorator, MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator, RecordTableDecorator,
(Story) => ( ContextStoreDecorator,
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> SnackBarDecorator,
<RecordFiltersComponentInstanceContext.Provider ObjectMetadataItemsDecorator,
value={{ instanceId: 'record-filters-component-instance' }} I18nFrontDecorator,
>
<RecordTableComponentInstance
recordTableId="persons"
onColumnsChange={() => {}}
>
<Story />
</RecordTableComponentInstance>
</RecordFiltersComponentInstanceContext.Provider>
</SnackBarProviderScope>
),
], ],
parameters: { parameters: {
recordTableObjectNameSingular: 'person',
msw: graphqlMocks, msw: graphqlMocks,
}, },
}; };

View File

@ -5,20 +5,20 @@ const StyledTbody = styled.tbody`
td:nth-of-type(1) { td:nth-of-type(1) {
position: sticky; position: sticky;
left: 0; left: 0;
z-index: 5; z-index: 6;
transition: 0.3s ease; transition: 0.3s ease;
} }
td:nth-of-type(2) { td:nth-of-type(2) {
position: sticky; position: sticky;
left: 11px; left: 11px;
z-index: 5; z-index: 6;
transition: 0.3s ease; transition: 0.3s ease;
} }
tr:not(:last-child) td:nth-of-type(3) { tr:not(:last-child) td:nth-of-type(3) {
// Last row is aggregate footer // Last row is aggregate footer
position: sticky; position: sticky;
left: 43px; left: 43px;
z-index: 5; z-index: 6;
transition: 0.3s ease; transition: 0.3s ease;
&:not(.disable-shadow)::after { &:not(.disable-shadow)::after {

View File

@ -26,21 +26,21 @@ const StyledTableHead = styled.thead`
th:nth-of-type(1) { th:nth-of-type(1) {
position: sticky; position: sticky;
left: 0; left: 0;
z-index: 5; z-index: 6;
transition: 0.3s ease; transition: 0.3s ease;
} }
th:nth-of-type(2) { th:nth-of-type(2) {
position: sticky; position: sticky;
left: 11px; left: 11px;
z-index: 5; z-index: 6;
transition: 0.3s ease; transition: 0.3s ease;
} }
th:nth-of-type(3) { th:nth-of-type(3) {
position: sticky; position: sticky;
left: 43px; left: 43px;
z-index: 5; z-index: 6;
transition: 0.3s ease; transition: 0.3s ease;
&::after { &::after {
@ -65,7 +65,7 @@ const StyledTableHead = styled.thead`
th { th {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 5; z-index: 6;
} }
} }

View File

@ -1,41 +1,81 @@
import { Decorator } from '@storybook/react'; import { Decorator } from '@storybook/react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { View } from '@/views/types/View';
import { useEffect, useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { mockedViewFieldsData } from '~/testing/mock-data/view-fields';
import { mockedViewsData } from '~/testing/mock-data/views';
export const RecordTableDecorator: Decorator = (Story) => { const InternalTableStateLoaderEffect = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const { loadRecordIndexStates } = useLoadRecordIndexStates();
const personObjectMetadataItem = objectMetadataItems.find( const view = useMemo(() => {
(objectMetadataItem) => objectMetadataItem.nameSingular === 'person', return {
...mockedViewsData[0],
viewFields: mockedViewFieldsData.filter(
(viewField) => viewField.viewId === mockedViewsData[0].id,
),
} as unknown as View;
}, []);
useEffect(() => {
loadRecordIndexStates(view, objectMetadataItem);
}, [loadRecordIndexStates, objectMetadataItem, view]);
return null;
};
const InternalTableContextProviders = ({
children,
objectMetadataItem,
}: {
children: React.ReactNode;
objectMetadataItem: ObjectMetadataItem;
}) => {
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
); );
if (!isDefined(personObjectMetadataItem)) {
return <Story />;
}
return ( return (
<RecordIndexContextProvider <RecordIndexContextProvider
value={{ value={{
indexIdentifierUrl: () => '', indexIdentifierUrl: () => '',
onIndexRecordsLoaded: () => {}, onIndexRecordsLoaded: () => {},
objectNamePlural: personObjectMetadataItem.namePlural, objectNamePlural: objectMetadataItem.namePlural,
objectNameSingular: personObjectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
objectMetadataItem: personObjectMetadataItem, objectMetadataItem: objectMetadataItem,
recordIndexId: 'record-index', recordIndexId: 'record-index',
}} }}
> >
<RecordTableContextProvider <RecordTableContextProvider
value={{ value={{
objectNameSingular: personObjectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
objectMetadataItem: personObjectMetadataItem, objectMetadataItem: objectMetadataItem,
recordTableId: 'persons', recordTableId: objectMetadataItem.namePlural,
viewBarId: 'view-bar', viewBarId: 'view-bar',
visibleTableColumns: [], visibleTableColumns: visibleTableColumns,
}} }}
> >
<RecordTableBodyContextProvider <RecordTableBodyContextProvider
@ -48,9 +88,71 @@ export const RecordTableDecorator: Decorator = (Story) => {
onMoveSoftFocusToCurrentCell: () => {}, onMoveSoftFocusToCurrentCell: () => {},
}} }}
> >
<Story /> {children}
</RecordTableBodyContextProvider> </RecordTableBodyContextProvider>
</RecordTableContextProvider> </RecordTableContextProvider>
</RecordIndexContextProvider> </RecordIndexContextProvider>
); );
}; };
export const RecordTableDecorator: Decorator = (Story, context) => {
const { recordTableObjectNameSingular: objectNameSingular } =
context.parameters;
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
if (!isDefined(objectMetadataItem)) {
throw new Error(
'Object metadata item not found while loading RecordTableDecorator',
);
}
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
mockedViewsData[0].id,
);
return (
<RecordFieldValueSelectorContextProvider>
<RecordTableComponentInstanceContext.Provider
value={{ instanceId: recordIndexId, onColumnsChange: () => {} }}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<InternalTableStateLoaderEffect
objectMetadataItem={objectMetadataItem}
/>
<InternalTableContextProviders
objectMetadataItem={objectMetadataItem}
>
<Story />
</InternalTableContextProviders>
</ActionMenuComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordFieldValueSelectorContextProvider>
);
};

View File

@ -12,6 +12,7 @@ const StyledContainer = styled.div`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative; position: relative;
min-height: 100px;
`; `;
interface StyledImageProps { interface StyledImageProps {

View File

@ -1,8 +1,10 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
const StyledLayout = styled.div<{ const StyledLayout = styled.div<{
width?: number; width?: number;
backgroundColor?: string | undefined; backgroundColor?: string | undefined;
height: number | 'fit-content';
}>` }>`
background: ${({ theme, backgroundColor }) => background: ${({ theme, backgroundColor }) =>
backgroundColor ?? theme.background.primary}; backgroundColor ?? theme.background.primary};
@ -12,7 +14,12 @@ const StyledLayout = styled.div<{
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: fit-content; height: ${({ height }) =>
height === 'fit-content'
? 'fit-content'
: `
${height}px
`};
max-width: calc(100% - 40px); max-width: calc(100% - 40px);
min-width: ${({ width }) => (width ? 'unset' : '300px')}; min-width: ${({ width }) => (width ? 'unset' : '300px')};
padding: 20px; padding: 20px;
@ -22,15 +29,21 @@ const StyledLayout = styled.div<{
type ComponentStorybookLayoutProps = { type ComponentStorybookLayoutProps = {
width?: number; width?: number;
backgroundColor?: string | undefined; backgroundColor?: string | undefined;
height?: number;
children: JSX.Element; children: JSX.Element;
}; };
export const ComponentStorybookLayout = ({ export const ComponentStorybookLayout = ({
width, width,
backgroundColor, backgroundColor,
height,
children, children,
}: ComponentStorybookLayoutProps) => ( }: ComponentStorybookLayoutProps) => (
<StyledLayout width={width} backgroundColor={backgroundColor}> <StyledLayout
width={width}
backgroundColor={backgroundColor}
height={isDefined(height) ? height : 'fit-content'}
>
{children} {children}
</StyledLayout> </StyledLayout>
); );

View File

@ -28,6 +28,7 @@ export const ComponentDecorator: Decorator = (Story, context) => {
return ( return (
<ComponentStorybookLayout <ComponentStorybookLayout
width={container?.width} width={container?.width}
height={container?.height}
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
> >
<Story /> <Story />