mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 15:35:36 +03:00
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:
parent
f2cc385710
commit
a2a5ab488c
@ -125,6 +125,7 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
filename: `${objectNameSingular}.csv`,
|
||||
objectNameSingular,
|
||||
recordIndexId,
|
||||
viewType,
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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 };
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 = {
|
||||
|
@ -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';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user