When exporting a kanban we should export the kanban's main field (#6444)

This PR was created by [GitStart](https://gitstart.com/) to address the
requirements from this ticket:
[TWNTY-6046](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-6046).
This ticket was imported from:
[TWNTY-6046](https://github.com/twentyhq/twenty/issues/6046)

 --- 

### Description

- We are getting the `kanbanFieldMetadataNameState` , get the column
data, and if there is data and the use is on the Kanban view we add the
data to the result

### Refs

#6046

### Demo

<https://jam.dev/c/96f16211-40e4-4b49-a6f5-88f0692fb47a>

Fixes #6046

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
gitstart-app[bot] 2024-08-09 10:23:06 +02:00 committed by GitHub
parent f2cc385710
commit a2a5ab488c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 416 additions and 3 deletions

View File

@ -125,6 +125,7 @@ export const RecordIndexOptionsDropdownContent = ({
filename: `${objectNameSingular}.csv`,
objectNameSingular,
recordIndexId,
viewType,
});
const location = useLocation();

View File

@ -0,0 +1,379 @@
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { act, renderHook, waitFor } from '@testing-library/react';
import { percentage, sleep, useTableData } from '../useTableData';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { ViewType } from '@/views/types/ViewType';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import gql from 'graphql-tag';
import { ReactNode } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { RecoilRoot, useRecoilValue } from 'recoil';
const defaultResponseData = {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
totalCount: 1,
};
const mockPerson = {
__typename: 'Person',
updatedAt: '2021-08-03T19:20:06.000Z',
myCustomObjectId: '123',
whatsapp: '123',
linkedinLink: {
primaryLinkUrl: 'https://www.linkedin.com',
primaryLinkLabel: 'linkedin',
secondaryLinks: ['https://www.linkedin.com'],
},
name: {
firstName: 'firstName',
lastName: 'lastName',
},
email: 'email',
position: 'position',
createdBy: {
source: 'source',
workspaceMemberId: '1',
name: 'name',
},
avatarUrl: 'avatarUrl',
jobTitle: 'jobTitle',
xLink: {
primaryLinkUrl: 'https://www.linkedin.com',
primaryLinkLabel: 'linkedin',
secondaryLinks: ['https://www.linkedin.com'],
},
performanceRating: 1,
createdAt: '2021-08-03T19:20:06.000Z',
phone: 'phone',
id: '123',
city: 'city',
companyId: '1',
intro: 'intro',
workPrefereance: 'workPrefereance',
};
const mocks: MockedResponse[] = [
{
request: {
query: gql`
query FindManyPeople(
$filter: PersonFilterInput
$orderBy: [PersonOrderByInput]
$lastCursor: String
$limit: Int
) {
people(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges {
node {
__typename
updatedAt
myCustomObjectId
whatsapp
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
email
position
createdBy {
source
workspaceMemberId
name
}
avatarUrl
jobTitle
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
performanceRating
createdAt
phone
id
city
companyId
intro
workPrefereance
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`,
variables: {
filter: undefined,
limit: 30,
orderBy: [{ position: 'AscNullsFirst' }],
},
},
result: jest.fn(() => ({
data: {
people: {
...defaultResponseData,
edges: [
{
node: mockPerson,
cursor: '1',
},
],
},
},
})),
},
];
const Wrapper = ({ children }: { children: ReactNode }) => (
<Router>
<RecoilRoot>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<MockedProvider addTypename={false} mocks={mocks}>
{children}
</MockedProvider>
</SnackBarProviderScope>
</RecoilRoot>
</Router>
);
const graphqlEmptyResponse = [
{
...mocks[0],
result: jest.fn(() => ({
data: {
people: {
...defaultResponseData,
edges: [],
},
},
})),
},
];
const WrapperWithEmptyResponse = ({ children }: { children: ReactNode }) => (
<Router>
<RecoilRoot>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<MockedProvider addTypename={false} mocks={graphqlEmptyResponse}>
{children}
</MockedProvider>
</SnackBarProviderScope>
</RecoilRoot>
</Router>
);
describe('useTableData', () => {
const recordIndexId = 'people';
const objectNameSingular = 'person';
describe('data fetching', () => {
it('should handle no records', async () => {
const callback = jest.fn();
const { result } = renderHook(
() =>
useTableData({
recordIndexId,
objectNameSingular,
callback,
delayMs: 0,
viewType: ViewType.Kanban,
}),
{ wrapper: WrapperWithEmptyResponse },
);
await act(async () => {
result.current.getTableData();
});
await waitFor(() => {
expect(callback).toHaveBeenCalledWith([], []);
});
});
it('should call the callback function with fetched data', async () => {
const callback = jest.fn();
const { result } = renderHook(
() =>
useTableData({
recordIndexId,
objectNameSingular,
callback,
delayMs: 0,
}),
{ wrapper: Wrapper },
);
await act(async () => {
result.current.getTableData();
});
await waitFor(() => {
expect(callback).toHaveBeenCalledWith([mockPerson], []);
});
});
it('should call the callback function with kanban field included as column if view type is kanban', async () => {
const callback = jest.fn();
const { result } = renderHook(
() => {
const kanbanFieldNameState = extractComponentState(
recordBoardKanbanFieldMetadataNameComponentState,
recordIndexId,
);
return {
tableData: useTableData({
recordIndexId,
objectNameSingular,
callback,
pageSize: 30,
maximumRequests: 100,
delayMs: 0,
viewType: ViewType.Kanban,
}),
setKanbanFieldName: useRecordBoard(recordIndexId),
kanbanFieldName: useRecoilValue(kanbanFieldNameState),
kanbanData: useRecordIndexOptionsForBoard({
objectNameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
}),
};
},
{
wrapper: Wrapper,
},
);
await act(async () => {
result.current.setKanbanFieldName.setKanbanFieldMetadataName(
result.current.kanbanData.hiddenBoardFields[0].metadata.fieldName,
);
});
await act(async () => {
result.current.tableData.getTableData();
});
await waitFor(async () => {
expect(callback).toHaveBeenCalledWith(
[mockPerson],
[
{
defaultValue: 'now',
editButtonIcon: undefined,
fieldMetadataId: '102963b7-3e77-4293-a1e6-1ab59a02b663',
iconName: 'IconCalendarClock',
isFilterable: true,
isLabelIdentifier: false,
isSortable: true,
isVisible: false,
label: 'Last update',
labelWidth: undefined,
metadata: {
fieldName: 'updatedAt',
isNullable: false,
objectMetadataNameSingular: 'person',
options: null,
placeHolder: 'Last update',
relationFieldMetadataId: undefined,
relationObjectMetadataNamePlural: '',
relationObjectMetadataNameSingular: '',
relationType: undefined,
targetFieldMetadataName: '',
},
position: 0,
showLabel: undefined,
size: 100,
type: 'DATE_TIME',
},
],
);
});
});
it('should not call the callback function with kanban field included as column if view type is table', async () => {
const callback = jest.fn();
const { result } = renderHook(
() => {
const kanbanFieldNameState = extractComponentState(
recordBoardKanbanFieldMetadataNameComponentState,
recordIndexId,
);
return {
tableData: useTableData({
recordIndexId,
objectNameSingular,
callback,
pageSize: 30,
maximumRequests: 100,
delayMs: 0,
viewType: ViewType.Table,
}),
setKanbanFieldName: useRecordBoard(recordIndexId),
kanbanFieldName: useRecoilValue(kanbanFieldNameState),
kanbanData: useRecordIndexOptionsForBoard({
objectNameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
}),
};
},
{
wrapper: Wrapper,
},
);
await act(async () => {
result.current.setKanbanFieldName.setKanbanFieldMetadataName(
result.current.kanbanData.hiddenBoardFields[0].metadata.fieldName,
);
});
await act(async () => {
result.current.tableData.getTableData();
});
await waitFor(async () => {
expect(callback).toHaveBeenCalledWith([mockPerson], []);
});
});
});
describe('utils', () => {
it('should correctly calculate percentage', () => {
expect(percentage(50, 200)).toBe(25);
expect(percentage(1, 3)).toBe(33);
});
it('should resolve sleep after given time', async () => {
jest.useFakeTimers();
const sleepPromise = sleep(1000);
jest.advanceTimersByTime(1000);
await expect(sleepPromise).resolves.toBeUndefined();
});
});
});

View File

@ -143,6 +143,7 @@ export const useExportTableData = ({
objectNameSingular,
pageSize = 30,
recordIndexId,
viewType,
}: UseExportTableDataOptions) => {
const { processRecordsForCSVExport } =
useProcessRecordsForCSVExport(objectNameSingular);
@ -164,6 +165,7 @@ export const useExportTableData = ({
pageSize,
recordIndexId,
callback: downloadCsv,
viewType,
});
return { progress, download };

View File

@ -8,6 +8,9 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from '~/utils/isDefined';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { ViewType } from '@/views/types/ViewType';
import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable';
export const sleep = (ms: number) =>
@ -27,6 +30,7 @@ export type UseTableDataOptions = {
rows: ObjectRecord[],
columns: ColumnDefinition<FieldMetadata>[],
) => void | Promise<void>;
viewType?: ViewType;
};
type ExportProgress = {
@ -42,6 +46,7 @@ export const useTableData = ({
pageSize = 30,
recordIndexId,
callback,
viewType = ViewType.Table,
}: UseTableDataOptions) => {
const [isDownloading, setIsDownloading] = useState(false);
const [inflight, setInflight] = useState(false);
@ -58,6 +63,17 @@ export const useTableData = ({
hasUserSelectedAllRowsState,
} = useRecordTableStates(recordIndexId);
const { hiddenBoardFields } = useRecordIndexOptionsForBoard({
objectNameSingular,
recordBoardId: recordIndexId,
viewBarId: recordIndexId,
});
const { kanbanFieldMetadataNameState } = useRecordBoardStates(recordIndexId);
const kanbanFieldMetadataName = useRecoilValue(kanbanFieldMetadataNameState);
const hiddenKanbanFieldColumn = hiddenBoardFields.find(
(column) => column.metadata.fieldName === kanbanFieldMetadataName,
);
const columns = useRecoilValue(visibleTableColumnsSelector());
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
@ -165,7 +181,14 @@ export const useTableData = ({
});
};
const res = callback(records, columns);
const finalColumns = [
...columns,
...(hiddenKanbanFieldColumn && viewType === ViewType.Kanban
? [hiddenKanbanFieldColumn]
: []),
];
const res = callback(records, finalColumns);
if (res instanceof Promise) {
res.then(complete);
@ -189,6 +212,8 @@ export const useTableData = ({
loading,
callback,
previousRecordCount,
hiddenKanbanFieldColumn,
viewType,
]);
return {

View File

@ -1,9 +1,10 @@
import { CaptchaDriverType } from '~/generated/graphql';
import { ClientConfig } from '~/generated-metadata/graphql';
import { CaptchaDriverType } from '~/generated/graphql';
export const mockedClientConfig: ClientConfig = {
signInPrefilled: true,
signUpDisabled: false,
chromeExtensionId: 'MOCKED_EXTENSION_ID',
debugMode: false,
authProviders: {
google: true,

View File

@ -6,6 +6,8 @@ import {
User,
Workspace,
WorkspaceActivationStatus,
WorkspaceMemberDateFormatEnum,
WorkspaceMemberTimeFormatEnum,
} from '~/generated/graphql';
type MockedUser = Pick<
@ -85,6 +87,9 @@ export const mockedWorkspaceMemberData: WorkspaceMember = {
updatedAt: '2023-04-26T10:23:42.33625+00:00',
userId: '2603c1f9-0172-4ea6-986c-eeaccdf7f4cf',
userEmail: 'charles@test.com',
dateFormat: WorkspaceMemberDateFormatEnum.DayFirst,
timeFormat: WorkspaceMemberTimeFormatEnum.Hour_24,
timeZone: 'America/New_York',
};
export const mockedUserData: MockedUser = {

View File

@ -18,9 +18,9 @@ import {
SaveOptions,
UpdateResult,
} from 'typeorm';
import { PickKeysByType } from 'typeorm/common/PickKeysByType';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { PickKeysByType } from 'typeorm/common/PickKeysByType';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';