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 { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useLoadRecordIndexStates = () => {
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}
/>
{recordTableIsEmpty ? (
{recordTableIsEmpty && !hasRecordGroups ? (
<RecordTableEmpty
tableBodyRef={tableBodyRef}
hasRecordGroups={hasRecordGroups}

View File

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

View File

@ -1,35 +1,40 @@
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 { 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 { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = {
title:
'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoGroupNoRecordAtAll',
component: RecordTableEmptyStateNoGroupNoRecordAtAll,
decorators: [
(Story) => (
<RecordTableContextProvider
recordTableId="persons"
viewBarId="view-bar"
objectNameSingular="person"
>
<Story />
</RecordTableContextProvider>
),
ComponentDecorator,
MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator,
(Story) => (
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<RecordTableComponentInstance
recordTableId="persons"
onColumnsChange={() => {}}
>
<Story />
</RecordTableComponentInstance>
</SnackBarProviderScope>
),
ContextStoreDecorator,
SnackBarDecorator,
ObjectMetadataItemsDecorator,
I18nFrontDecorator,
],
parameters: {
recordTableObjectNameSingular: 'person',
msw: graphqlMocks,
},
};

View File

@ -1,35 +1,40 @@
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 { 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 { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = {
title:
'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateNoRecordFoundForFilter',
component: RecordTableEmptyStateNoRecordFoundForFilter,
decorators: [
(Story) => (
<RecordTableContextProvider
recordTableId="persons"
viewBarId="view-bar"
objectNameSingular="person"
>
<Story />
</RecordTableContextProvider>
),
ComponentDecorator,
MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator,
(Story) => (
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<RecordTableComponentInstance
recordTableId="persons"
onColumnsChange={() => {}}
>
<Story />
</RecordTableComponentInstance>
</SnackBarProviderScope>
),
ContextStoreDecorator,
SnackBarDecorator,
ObjectMetadataItemsDecorator,
I18nFrontDecorator,
],
parameters: {
recordTableObjectNameSingular: 'person',
msw: graphqlMocks,
},
};

View File

@ -1,34 +1,39 @@
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 { 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 { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateRemote',
component: RecordTableEmptyStateRemote,
decorators: [
(Story) => (
<RecordTableContextProvider
recordTableId="persons"
viewBarId="view-bar"
objectNameSingular="person"
>
<Story />
</RecordTableContextProvider>
),
ComponentDecorator,
MemoryRouterDecorator,
ObjectMetadataItemsDecorator,
RecordTableDecorator,
(Story) => (
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<RecordTableComponentInstance
recordTableId="persons"
onColumnsChange={() => {}}
>
<Story />
</RecordTableComponentInstance>
</SnackBarProviderScope>
),
ContextStoreDecorator,
SnackBarDecorator,
ObjectMetadataItemsDecorator,
I18nFrontDecorator,
],
parameters: {
recordTableObjectNameSingular: 'person',
msw: graphqlMocks,
},
};

View File

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

View File

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

View File

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