Removing Prisma and Grapql-nestjs-prisma resolvers (#2574)

* Some cleaning

* Fix seeds

* Fix all sign in, sign up flow and apiKey optimistic rendering

* Fix
This commit is contained in:
Charles Bochet
2023-11-19 18:25:47 +01:00
committed by GitHub
parent 18dac1a2b6
commit f5e1d7825a
616 changed files with 2220 additions and 23073 deletions

View File

@ -9,7 +9,9 @@ import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord';
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
@ -52,6 +54,7 @@ type Form = z.infer<typeof validationSchema>;
export const CreateProfile = () => {
const navigate = useNavigate();
const onboardingStatus = useOnboardingStatus();
const { enqueueSnackBar } = useSnackBar();
@ -61,7 +64,7 @@ export const CreateProfile = () => {
const { updateOneObject, objectNotFoundInMetadata } =
useUpdateOneObjectRecord<WorkspaceMember>({
objectNameSingular: 'workspaceMemberV2',
objectNameSingular: 'workspaceMember',
});
// Form
@ -139,6 +142,10 @@ export const CreateProfile = () => {
[onSubmit],
);
if (onboardingStatus !== OnboardingStatus.OngoingProfileCreation) {
return null;
}
return (
<>
<Title>Create profile</Title>

View File

@ -8,7 +8,9 @@ import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { H2Title } from '@/ui/display/typography/components/H2Title';
@ -43,6 +45,7 @@ export const CreateWorkspace = () => {
const navigate = useNavigate();
const { enqueueSnackBar } = useSnackBar();
const onboardingStatus = useOnboardingStatus();
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const [updateWorkspace] = useUpdateWorkspaceMutation();
@ -66,7 +69,7 @@ export const CreateWorkspace = () => {
try {
const result = await updateWorkspace({
variables: {
data: {
input: {
displayName: data.name,
},
},
@ -101,6 +104,10 @@ export const CreateWorkspace = () => {
[onSubmit],
);
if (onboardingStatus !== OnboardingStatus.OngoingWorkspaceCreation) {
return null;
}
return (
<>
<Title>Create your workspace</Title>

View File

@ -1,75 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { v4 } from 'uuid';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { CompanyTable } from '@/companies/table/components/CompanyTable';
import { SEARCH_COMPANY_QUERY } from '@/search/graphql/queries/searchCompanyQuery';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { IconBuildingSkyscraper } from '@/ui/display/icon';
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';
import { PageHeader } from '@/ui/layout/page/PageHeader';
import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect';
import { RecordTableActionBar } from '@/ui/object/record-table/action-bar/components/RecordTableActionBar';
import { RecordTableContextMenu } from '@/ui/object/record-table/context-menu/components/RecordTableContextMenu';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { useUpsertTableRowId } from '@/ui/object/record-table/hooks/useUpsertTableRowId';
import { useInsertOneCompanyMutation } from '~/generated/graphql';
const StyledTableContainer = styled.div`
display: flex;
width: 100%;
`;
export const Companies = () => {
const [insertCompany] = useInsertOneCompanyMutation();
const { upsertRecordTableItem } = useRecordTable({
recordTableScopeId: 'companies',
});
const upsertTableRowIds = useUpsertTableRowId();
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular: 'company',
});
const handleAddButtonClick = async () => {
const newCompanyId: string = v4();
await insertCompany({
variables: {
data: {
id: newCompanyId,
name: '',
domainName: '',
address: '',
},
},
update: (_cache, { data }) => {
if (data?.createOneCompany) {
upsertTableRowIds(data?.createOneCompany.id);
upsertRecordTableItem(data?.createOneCompany);
triggerOptimisticEffects('Company', [data?.createOneCompany]);
}
},
refetchQueries: [getOperationName(SEARCH_COMPANY_QUERY) ?? ''],
});
};
return (
<SpreadsheetImportProvider>
<PageContainer>
<PageHeader title="Companies" Icon={IconBuildingSkyscraper}>
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
<PageAddButton onClick={handleAddButtonClick} />
</PageHeader>
<PageBody>
<StyledTableContainer>
<CompanyTable />
</StyledTableContainer>
<RecordTableActionBar />
<RecordTableContextMenu />
</PageBody>
</PageContainer>
</SpreadsheetImportProvider>
);
};

View File

@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import { CompanyTableMockMode } from '@/companies/table/components/CompanyTableMockMode';
import { IconBuildingSkyscraper } from '@/ui/display/icon';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';
@ -16,9 +15,7 @@ export const CompaniesMockMode = () => {
<PageContainer>
<PageHeader title="Companies" Icon={IconBuildingSkyscraper} />
<PageBody>
<StyledTableContainer>
<CompanyTableMockMode />
</StyledTableContainer>
<StyledTableContainer></StyledTableContainer>
</PageBody>
</PageContainer>
);

View File

@ -1,129 +0,0 @@
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { CompanyTeam } from '@/companies/components/CompanyTeam';
import { useCompanyQuery } from '@/companies/hooks/useCompanyQuery';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { AppPath } from '@/types/AppPath';
import { IconBuildingSkyscraper } from '@/ui/display/icon';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';
import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton';
import { PageHeader } from '@/ui/layout/page/PageHeader';
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
import { FieldContext } from '@/ui/object/field/contexts/FieldContext';
import { RecordInlineCell } from '@/ui/object/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/ui/object/record-inline-cell/property-box/components/PropertyBox';
import { InlineCellHotkeyScope } from '@/ui/object/record-inline-cell/types/InlineCellHotkeyScope';
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useUpdateOneCompanyMutation } from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
import { CompanyNameEditableField } from '../../modules/companies/editable-field/components/CompanyNameEditableField';
import { ShowPageContainer } from '../../modules/ui/layout/page/ShowPageContainer';
import { companyShowFieldDefinitions } from './constants/companyShowFieldDefinitions';
export const CompanyShow = () => {
const companyId = useParams().companyId ?? '';
const { createFavorite, deleteFavorite } = useFavorites({
objectNamePlural: 'companies',
});
const navigate = useNavigate();
const { data, loading } = useCompanyQuery(companyId);
const company = data?.findUniqueCompany;
useEffect(() => {
if (!loading && !company) {
navigate(AppPath.NotFound);
}
}, [loading, company, navigate]);
if (!company) return <></>;
const isFavorite =
company.Favorite && company.Favorite?.length > 0 ? true : false;
const handleFavoriteButtonClick = async () => {
if (isFavorite) deleteFavorite(companyId);
else createFavorite('company', companyId);
};
return (
<PageContainer>
<PageTitle title={company.name || 'No Name'} />
<PageHeader
title={company.name ?? ''}
hasBackButton
Icon={IconBuildingSkyscraper}
>
<PageFavoriteButton
isFavorite={isFavorite}
onClick={handleFavoriteButtonClick}
/>
<ShowPageAddButton
key="add"
entity={{
id: company.id,
type: 'Company',
}}
/>
</PageHeader>
<PageBody>
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
<ShowPageContainer>
<ShowPageLeftContainer>
<ShowPageSummaryCard
id={company.id}
logoOrAvatar={getLogoUrlFromDomainName(
company.domainName ?? '',
)}
title={company.name ?? 'No name'}
date={company.createdAt ?? ''}
renderTitleEditComponent={() => (
<CompanyNameEditableField company={company} />
)}
avatarType="squared"
/>
<PropertyBox extraPadding={true}>
{companyShowFieldDefinitions.map((fieldDefinition) => {
return (
<FieldContext.Provider
key={company.id + fieldDefinition.fieldMetadataId}
value={{
entityId: company.id,
recoilScopeId:
company.id + fieldDefinition.fieldMetadataId,
fieldDefinition,
useUpdateEntityMutation: useUpdateOneCompanyMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell />
</FieldContext.Provider>
);
})}
</PropertyBox>
<CompanyTeam company={company}></CompanyTeam>
</ShowPageLeftContainer>
<ShowPageRightContainer
entity={{
id: company.id,
type: 'Company',
}}
timeline
tasks
notes
emails
/>
</ShowPageContainer>
</RecoilScope>
</PageBody>
</PageContainer>
);
};

View File

@ -1,62 +0,0 @@
import { expect } from '@storybook/jest';
import { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedCompaniesData } from '~/testing/mock-data/companies';
import { sleep } from '~/testing/sleep';
import { Companies } from '../Companies';
import { Story } from './Companies.stories';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Companies/Add',
component: Companies,
decorators: [PageDecorator],
args: { routePath: AppPath.CompaniesPage },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export const AddNewCompany: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedCompaniesData[0].name,
{},
{ timeout: 3000 },
);
});
const rowsBeforeAdd = canvas.getAllByRole('row');
await step('Click on add button', async () => {
const addButton = canvas.getByRole('button', { name: 'Add' });
await userEvent.click(addButton);
});
await sleep(1000);
await step('Check an empty row has been added', async () => {
const rowsAfterAdd = canvas.getAllByRole('row');
const firstRow = rowsAfterAdd[1];
const cells = within(firstRow).getAllByRole('cell');
expect(cells[1].textContent).toBe('');
expect(rowsAfterAdd).toHaveLength(rowsBeforeAdd.length + 1);
});
},
};

View File

@ -1,119 +0,0 @@
import { expect } from '@storybook/jest';
import { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedCompaniesData } from '~/testing/mock-data/companies';
import { sleep } from '~/testing/sleep';
import { Companies } from '../Companies';
import { Story } from './Companies.stories';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Companies/FilterBy',
component: Companies,
decorators: [PageDecorator],
args: { routePath: AppPath.CompaniesPage },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export const FilterByName: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedCompaniesData[0].name,
{},
{ timeout: 3000 },
);
});
await step('Click on filter button', async () => {
const filterButton = canvas.getByText('Filter');
await userEvent.click(filterButton);
});
await step('Select name filter', async () => {
const nameFilterButton = canvas.getByTestId('select-filter-0');
await userEvent.click(nameFilterButton);
const nameInput = canvas.getByPlaceholderText('Name');
await userEvent.type(nameInput, 'Air', { delay: 200 });
const nameFilter = canvas.getAllByText(
(_, element) => !!element?.textContent?.includes('Name: Air'),
);
expect(nameFilter).not.toHaveLength(0);
});
await sleep(1000);
await step('Check filtered rows', async () => {
expect(canvas.getByText('Airbnb')).toBeVisible();
expect(canvas.getByText('Aircall')).toBeVisible();
expect(canvas.queryByText('Qonto')).toBeNull();
});
},
};
export const FilterByAccountOwner: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedCompaniesData[0].name,
{},
{ timeout: 3000 },
);
});
await step('Click on filter button', async () => {
const filterButton = canvas.getByText('Filter');
await userEvent.click(filterButton);
});
await step('Select account owner filter', async () => {
const accountOwnerFilterButton = canvas.getByTestId('select-filter-5');
await userEvent.click(accountOwnerFilterButton);
const accountOwnerNameInput =
canvas.getByPlaceholderText('Account owner');
await userEvent.type(accountOwnerNameInput, 'Char', { delay: 200 });
const charlesChip = await canvas.findByRole(
'listitem',
{
name: (_, element) =>
!!element?.textContent?.includes('Charles Test'),
},
{ timeout: 1000 },
);
await userEvent.click(charlesChip);
const accountOwnerFilter = canvas.getAllByText(
(_, element) =>
!!element?.textContent?.includes('Account owner: Charles Test'),
);
expect(accountOwnerFilter).not.toHaveLength(0);
});
await sleep(1000);
await step('Check filtered rows', async () => {
expect(canvas.getByText('Airbnb')).toBeVisible();
expect(canvas.queryByText('Qonto')).toBeNull();
});
},
};

View File

@ -1,76 +0,0 @@
import { expect } from '@storybook/jest';
import { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedCompaniesData } from '~/testing/mock-data/companies';
import { sleep } from '~/testing/sleep';
import { Companies } from '../Companies';
import { Story } from './Companies.stories';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Companies/SortBy',
component: Companies,
decorators: [PageDecorator],
args: { routePath: AppPath.CompaniesPage },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
const sortedCompanyNames = [...mockedCompaniesData]
.map(({ name }) => name)
.sort((a, b) => a.localeCompare(b));
export const SortByName: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedCompaniesData[0].name,
{},
{ timeout: 3000 },
);
});
await step('Click on sort button', async () => {
const sortButton = canvas.getByRole('button', { name: 'Sort' });
await userEvent.click(sortButton);
});
await step('Select sort by name', async () => {
const nameSortButton = canvas.getByTestId('select-sort-0');
await userEvent.click(nameSortButton);
await canvas.findByTestId('remove-icon-name', {}, { timeout: 3000 });
});
await sleep(1000);
await step('Check rows are sorted by name', async () => {
const nameCells = canvas.getAllByText(
(_, element) =>
sortedCompanyNames.some((name) =>
element?.textContent?.includes(name),
),
{ selector: '[data-testid="editable-cell-display-mode"]' },
);
expect(nameCells).toHaveLength(sortedCompanyNames.length);
sortedCompanyNames.forEach((name, index) =>
expect(nameCells[index]).toHaveTextContent(name),
);
});
},
};

View File

@ -1,26 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { Companies } from '../Companies';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Companies',
component: Companies,
decorators: [PageDecorator],
args: { routePath: AppPath.CompaniesPage },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof Companies>;
export const Default: Story = {};

View File

@ -1,171 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { fireEvent, within } from '@storybook/testing-library';
import { graphql } from 'msw';
import { UPDATE_ONE_COMPANY } from '@/companies/graphql/mutations/updateOneCompany';
import { GET_COMPANY } from '@/companies/graphql/queries/getCompany';
import { AppPath } from '@/types/AppPath';
import { ObjectFilterDropdownScope } from '@/ui/object/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedCompaniesData } from '~/testing/mock-data/companies';
import { CompanyShow } from '../CompanyShow';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Companies/Company',
component: CompanyShow,
decorators: [PageDecorator],
args: {
routePath: AppPath.CompanyShowPage,
routeParams: { ':companyId': mockedCompaniesData[0].id },
},
parameters: {
msw: [
...graphqlMocks,
graphql.query(getOperationName(GET_COMPANY) ?? '', (req, res, ctx) => {
return res(
ctx.data({
findUniqueCompany: mockedCompaniesData[0],
}),
);
}),
],
},
};
export default meta;
export type Story = StoryObj<typeof CompanyShow>;
export const Default: Story = {};
export const EditNoteByAddButton: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const firstNoteTitle = await canvas.findByText('My very first note');
await firstNoteTitle.click();
expect(
await canvas.findByDisplayValue('My very first note'),
).toBeInTheDocument();
const workspaceName = await canvas.findByText('Twenty');
await fireEvent.click(workspaceName);
expect(await canvas.queryByDisplayValue('My very first note')).toBeNull();
const addDropdown = await canvas.findByTestId('add-showpage-button');
await addDropdown.click();
const noteButton = await canvas.findByText('Note');
await noteButton.click();
expect(
await canvas.findByDisplayValue('My very first note'),
).toBeInTheDocument();
},
parameters: {
msw: [
...meta.parameters?.msw,
graphql.mutation(
getOperationName(UPDATE_ONE_COMPANY) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
updateOneCompany: [mockedCompaniesData[0]],
}),
);
},
),
],
},
};
export const NoteTab: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const noteTab = await canvas.findByTestId('tab-notes');
await noteTab.click();
expect(await canvas.findByText('My very first note')).toBeInTheDocument();
const workspaceName = await canvas.findByText('Twenty');
await fireEvent.click(workspaceName);
expect(await canvas.queryByDisplayValue('My very first note')).toBeNull();
const addButton = await canvas.findByText('Add note');
await addButton.click();
const noteButton = await canvas.findByText('Note');
await noteButton.click();
expect(await canvas.findByText('My very first note')).toBeInTheDocument();
},
parameters: {
msw: [
...meta.parameters?.msw,
graphql.mutation(
getOperationName(UPDATE_ONE_COMPANY) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
updateOneCompany: [mockedCompaniesData[0]],
}),
);
},
),
],
},
};
export const TaskTab: Story = {
decorators: [
(Story) => (
<ObjectFilterDropdownScope filterScopeId="tasks-filter-scope">
<Story />
</ObjectFilterDropdownScope>
),
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const taskTab = await canvas.findByTestId('tab-tasks');
await taskTab.click();
expect(await canvas.findByText('My very first task')).toBeInTheDocument();
const workspaceName = await canvas.findByText('Twenty');
await fireEvent.click(workspaceName);
expect(await canvas.queryByDisplayValue('My very first task')).toBeNull();
const addButton = await canvas.findByText('Add task');
await addButton.click();
const taskButton = await canvas.findByText('Task');
await taskButton.click();
expect(await canvas.findByText('My very first task')).toBeInTheDocument();
},
parameters: {
msw: [
...meta.parameters?.msw,
graphql.mutation(
getOperationName(UPDATE_ONE_COMPANY) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
updateOneCompany: [mockedCompaniesData[0]],
}),
);
},
),
],
},
};

View File

@ -1,99 +0,0 @@
import {
IconBrandX,
IconCalendar,
IconLink,
IconMap,
IconTarget,
IconUserCircle,
IconUsers,
} from '@/ui/display/icon';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { FieldDefinition } from '@/ui/object/field/types/FieldDefinition';
import {
FieldBooleanMetadata,
FieldDateMetadata,
FieldMetadata,
FieldNumberMetadata,
FieldRelationMetadata,
FieldTextMetadata,
FieldURLMetadata,
} from '@/ui/object/field/types/FieldMetadata';
import { User } from '~/generated/graphql';
export const companyShowFieldDefinitions: FieldDefinition<FieldMetadata>[] = [
{
fieldMetadataId: 'domainName',
label: 'Domain name',
Icon: IconLink,
type: 'URL',
metadata: {
fieldName: 'domainName',
placeHolder: 'URL',
},
} satisfies FieldDefinition<FieldURLMetadata>,
{
fieldMetadataId: 'accountOwner',
label: 'Account owner',
Icon: IconUserCircle,
type: 'RELATION',
metadata: {
fieldName: 'accountOwner',
relationType: Entity.User,
},
entityChipDisplayMapper: (dataObject: User) => {
return {
name: dataObject?.displayName,
pictureUrl: dataObject?.avatarUrl ?? undefined,
avatarType: 'rounded',
};
},
} satisfies FieldDefinition<FieldRelationMetadata>,
{
fieldMetadataId: 'employees',
label: 'Employees',
Icon: IconUsers,
type: 'NUMBER',
metadata: {
fieldName: 'employees',
placeHolder: 'Employees',
},
} satisfies FieldDefinition<FieldNumberMetadata>,
{
fieldMetadataId: 'address',
label: 'Address',
Icon: IconMap,
type: 'TEXT',
metadata: {
fieldName: 'address',
placeHolder: 'Address',
},
} satisfies FieldDefinition<FieldTextMetadata>,
{
fieldMetadataId: 'idealCustomerProfile',
label: 'ICP',
Icon: IconTarget,
type: 'BOOLEAN',
metadata: {
fieldName: 'idealCustomerProfile',
},
} satisfies FieldDefinition<FieldBooleanMetadata>,
{
fieldMetadataId: 'xUrl',
label: 'Twitter',
Icon: IconBrandX,
type: 'URL',
metadata: {
fieldName: 'xUrl',
placeHolder: 'X',
},
} satisfies FieldDefinition<FieldURLMetadata>,
{
fieldMetadataId: 'createdAt',
label: 'Created at',
Icon: IconCalendar,
type: 'DATE',
metadata: {
fieldName: 'createdAt',
},
} satisfies FieldDefinition<FieldDateMetadata>,
];

View File

@ -1,52 +0,0 @@
import {
IconBuildingSkyscraper,
IconCalendarEvent,
IconLink,
IconMap,
IconUser,
IconUsers,
} from '@/ui/display/icon/index';
import { FilterDefinitionByEntity } from '@/ui/object/object-filter-dropdown/types/FilterDefinitionByEntity';
import { FilterDropdownUserSearchSelect } from '@/users/components/FilterDropdownUserSearchSelect';
import { Company } from '~/generated/graphql';
export const companyTableFilterDefinitions: FilterDefinitionByEntity<Company>[] =
[
{
fieldMetadataId: 'name',
label: 'Name',
Icon: IconBuildingSkyscraper,
type: 'TEXT',
},
{
fieldMetadataId: 'employees',
label: 'Employees',
Icon: IconUsers,
type: 'NUMBER',
},
{
fieldMetadataId: 'domainName',
label: 'URL',
Icon: IconLink,
type: 'TEXT',
},
{
fieldMetadataId: 'address',
label: 'Address',
Icon: IconMap,
type: 'TEXT',
},
{
fieldMetadataId: 'createdAt',
label: 'Created at',
Icon: IconCalendarEvent,
type: 'DATE',
},
{
fieldMetadataId: 'accountOwnerId',
label: 'Account owner',
Icon: IconUser,
type: 'ENTITY',
entitySelectComponent: <FilterDropdownUserSearchSelect />,
},
];

View File

@ -1,36 +0,0 @@
import {
IconBuildingSkyscraper,
IconCalendarEvent,
IconLink,
IconMap,
IconUsers,
} from '@/ui/display/icon/index';
import { SortDefinition } from '@/ui/object/object-sort-dropdown/types/SortDefinition';
export const companyTableSortDefinitions: SortDefinition[] = [
{
fieldMetadataId: 'name',
label: 'Name',
Icon: IconBuildingSkyscraper,
},
{
fieldMetadataId: 'employees',
label: 'Employees',
Icon: IconUsers,
},
{
fieldMetadataId: 'domainName',
label: 'Url',
Icon: IconLink,
},
{
fieldMetadataId: 'address',
label: 'Address',
Icon: IconMap,
},
{
fieldMetadataId: 'createdAt',
label: 'Creation',
Icon: IconCalendarEvent,
},
];

View File

@ -28,7 +28,7 @@ export const Opportunities = () => {
const { updateOneObject: updateOnePipelineStep } =
useUpdateOneObjectRecord<PipelineStep>({
objectNameSingular: 'pipelineStepV2',
objectNameSingular: 'pipelineStep',
});
const handleEditColumnTitle = (
@ -46,7 +46,7 @@ export const Opportunities = () => {
};
const opportunitiesV2MetadataId = useFindOneObjectMetadataItem({
objectNameSingular: 'opportunityV2',
objectNameSingular: 'opportunity',
}).foundObjectMetadataItem?.id;
const { setViewObjectMetadataId } = useView({

View File

@ -1,40 +1,34 @@
import { FilterDropdownCompanySearchSelect } from '@/companies/components/FilterDropdownCompanySearchSelect';
import {
IconBuildingSkyscraper,
IconCalendarEvent,
IconCurrencyDollar,
IconUser,
} from '@/ui/display/icon/index';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { FilterDefinitionByEntity } from '@/ui/object/object-filter-dropdown/types/FilterDefinitionByEntity';
import { PipelineProgress } from '~/generated/graphql';
import { FilterDropdownPeopleSearchSelect } from '../../../modules/people/components/FilterDropdownPeopleSearchSelect';
export const opportunityBoardFilterDefinitions: FilterDefinitionByEntity<PipelineProgress>[] =
export const opportunityBoardFilterDefinitions: FilterDefinitionByEntity<Opportunity>[] =
[
{
fieldMetadataId: 'amount',
label: 'Amount',
Icon: IconCurrencyDollar,
iconName: 'IconCurrencyDollar',
type: 'NUMBER',
},
{
fieldMetadataId: 'closeDate',
label: 'Close date',
Icon: IconCalendarEvent,
iconName: 'IconCalendarEvent',
type: 'DATE',
},
{
fieldMetadataId: 'companyId',
label: 'Company',
Icon: IconBuildingSkyscraper,
iconName: 'IconBuildingSkyscraper',
type: 'ENTITY',
entitySelectComponent: <FilterDropdownCompanySearchSelect />,
},
{
fieldMetadataId: 'pointOfContactId',
label: 'Point of contact',
Icon: IconUser,
iconName: 'IconUser',
type: 'ENTITY',
entitySelectComponent: <FilterDropdownPeopleSearchSelect />,
},

View File

@ -1,71 +0,0 @@
import styled from '@emotion/styled';
import { v4 } from 'uuid';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { PersonTable } from '@/people/table/components/PersonTable';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { IconUser } from '@/ui/display/icon';
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';
import { PageHeader } from '@/ui/layout/page/PageHeader';
import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect';
import { RecordTableActionBar } from '@/ui/object/record-table/action-bar/components/RecordTableActionBar';
import { RecordTableContextMenu } from '@/ui/object/record-table/context-menu/components/RecordTableContextMenu';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { useUpsertTableRowId } from '@/ui/object/record-table/hooks/useUpsertTableRowId';
import { useInsertOnePersonMutation } from '~/generated/graphql';
const StyledTableContainer = styled.div`
display: flex;
width: 100%;
`;
export const People = () => {
const [insertOnePerson] = useInsertOnePersonMutation();
const { upsertRecordTableItem } = useRecordTable({
recordTableScopeId: 'people',
});
const upsertTableRowIds = useUpsertTableRowId();
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular: 'Person',
});
const handleAddButtonClick = async () => {
const newPersonId: string = v4();
await insertOnePerson({
variables: {
data: {
id: newPersonId,
firstName: '',
lastName: '',
},
},
update: (_cache, { data }) => {
if (data?.createOnePerson) {
upsertTableRowIds(data?.createOnePerson.id);
upsertRecordTableItem(data?.createOnePerson);
triggerOptimisticEffects('Person', [data?.createOnePerson]);
}
},
});
};
return (
<SpreadsheetImportProvider>
<PageContainer>
<PageHeader title="People" Icon={IconUser}>
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
<PageAddButton onClick={handleAddButtonClick} />
</PageHeader>
<PageBody>
<StyledTableContainer>
<PersonTable />
</StyledTableContainer>
<RecordTableActionBar />
<RecordTableContextMenu />
</PageBody>
</PageContainer>
</SpreadsheetImportProvider>
);
};

View File

@ -1,161 +0,0 @@
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { getOperationName } from '@apollo/client/utilities';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { GET_PERSON } from '@/people/graphql/queries/getPerson';
import { usePersonQuery } from '@/people/hooks/usePersonQuery';
import { AppPath } from '@/types/AppPath';
import { IconUser } from '@/ui/display/icon';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';
import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton';
import { PageHeader } from '@/ui/layout/page/PageHeader';
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
import { FieldContext } from '@/ui/object/field/contexts/FieldContext';
import { RecordInlineCell } from '@/ui/object/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/ui/object/record-inline-cell/property-box/components/PropertyBox';
import { InlineCellHotkeyScope } from '@/ui/object/record-inline-cell/types/InlineCellHotkeyScope';
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import {
useUpdateOnePersonMutation,
useUploadPersonPictureMutation,
} from '~/generated/graphql';
import { PeopleFullNameEditableField } from '../../modules/people/editable-field/components/PeopleFullNameEditableField';
import { ShowPageContainer } from '../../modules/ui/layout/page/ShowPageContainer';
import { personShowFieldDefinition } from './constants/personShowFieldDefinitions';
export const PersonShow = () => {
const personId = useParams().personId ?? '';
const { createFavorite, deleteFavorite } = useFavorites({
objectNamePlural: 'peopleV2',
});
const navigate = useNavigate();
const { data, loading } = usePersonQuery(personId);
const person = data?.findUniquePerson;
const [uploadPicture] = useUploadPersonPictureMutation();
useEffect(() => {
if (!loading && !person) {
navigate(AppPath.NotFound);
}
}, [loading, person, navigate]);
if (!person) return <></>;
const isFavorite =
person.Favorite && person.Favorite?.length > 0 ? true : false;
const onUploadPicture = async (file: File) => {
if (!file || !person?.id) {
return;
}
await uploadPicture({
variables: {
file,
id: person.id,
},
refetchQueries: [getOperationName(GET_PERSON) ?? ''],
});
};
const handleFavoriteButtonClick = async () => {
if (isFavorite) deleteFavorite(person.id);
else createFavorite('person', person.id);
};
return (
<PageContainer>
<PageTitle title={person.displayName || 'No Name'} />
<PageHeader title={person.firstName ?? ''} Icon={IconUser} hasBackButton>
<PageFavoriteButton
isFavorite={isFavorite}
onClick={handleFavoriteButtonClick}
/>
<ShowPageAddButton
key="add"
entity={{
id: person.id,
type: 'Person',
relatedEntities: person.company?.id
? [
{
id: person.company?.id,
type: 'Company',
},
]
: undefined,
}}
/>
</PageHeader>
<PageBody>
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
<ShowPageContainer>
<ShowPageLeftContainer>
<ShowPageSummaryCard
id={person.id}
title={person.displayName ?? 'No name'}
logoOrAvatar={person.avatarUrl ?? undefined}
date={person.createdAt ?? ''}
renderTitleEditComponent={() =>
person ? (
<PeopleFullNameEditableField people={person} />
) : (
<></>
)
}
onUploadPicture={onUploadPicture}
avatarType="rounded"
/>
<PropertyBox extraPadding={true}>
{personShowFieldDefinition.map((fieldDefinition) => {
return (
<FieldContext.Provider
value={{
entityId: person.id,
recoilScopeId: person.id + fieldDefinition.label,
fieldDefinition,
useUpdateEntityMutation: useUpdateOnePersonMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
key={person.id + fieldDefinition.label}
>
<RecordInlineCell />
</FieldContext.Provider>
);
})}
</PropertyBox>
</ShowPageLeftContainer>
<ShowPageRightContainer
entity={{
id: person.id ?? '',
type: 'Person',
relatedEntities: person.company?.id
? [
{
id: person.company?.id,
type: 'Company',
},
]
: undefined,
}}
timeline
tasks
notes
emails
/>
</ShowPageContainer>
</RecoilScope>
</PageBody>
</PageContainer>
);
};

View File

@ -1,62 +0,0 @@
import { expect } from '@storybook/jest';
import { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { sleep } from '~/testing/sleep';
import { People } from '../People';
import { Story } from './People.stories';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People/Add',
component: People,
decorators: [PageDecorator],
args: { routePath: AppPath.PeoplePage },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export const AddNewPerson: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedPeopleData[0].displayName,
{},
{ timeout: 3000 },
);
});
const rowsBeforeAdd = canvas.getAllByRole('row');
await step('Click on add button', async () => {
const addButton = canvas.getByRole('button', { name: 'Add' });
await userEvent.click(addButton);
});
await sleep(1000);
await step('Check an empty row has been added', async () => {
const rowsAfterAdd = canvas.getAllByRole('row');
const firstRow = rowsAfterAdd[1];
const cells = within(firstRow).getAllByRole('cell');
expect(cells[1].textContent).toBe('');
expect(rowsAfterAdd).toHaveLength(rowsBeforeAdd.length + 1);
});
},
};

View File

@ -1,113 +0,0 @@
import { expect } from '@storybook/jest';
import { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { sleep } from '~/testing/sleep';
import { People } from '../People';
import { Story } from './People.stories';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People/FilterBy',
component: People,
decorators: [PageDecorator],
args: { routePath: AppPath.PeoplePage },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export const Email: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedPeopleData[0].displayName,
{},
{ timeout: 3000 },
);
});
await step('Click on filter button', async () => {
const filterButton = canvas.getByText('Filter');
await userEvent.click(filterButton);
});
await step('Select email filter', async () => {
const emailFilterButton = canvas.getByTestId('select-filter-2');
await userEvent.click(emailFilterButton);
const emailInput = canvas.getByPlaceholderText('Email');
await userEvent.type(emailInput, 'al', { delay: 200 });
const emailFilter = canvas.getAllByText(
(_, element) => !!element?.textContent?.includes('Email: al'),
);
expect(emailFilter).not.toHaveLength(0);
});
await sleep(1000);
await step('Check filtered rows', async () => {
expect(canvas.getByText('Alexandre Prot')).toBeVisible();
expect(canvas.queryByText('John Doe')).toBeNull();
});
},
};
export const CompanyName: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedPeopleData[0].displayName,
{},
{ timeout: 3000 },
);
});
await step('Click on filter button', async () => {
const filterButton = canvas.getByText('Filter');
await userEvent.click(filterButton);
});
await step('Select company filter', async () => {
const companyFilterButton = canvas.getByTestId('select-filter-3');
await userEvent.click(companyFilterButton);
const companyNameInput = canvas.getByPlaceholderText('Company');
await userEvent.type(companyNameInput, 'Qon', { delay: 200 });
const qontoChip = await canvas.findByRole(
'listitem',
{ name: (_, element) => !!element?.textContent?.includes('Qonto') },
{ timeout: 1000 },
);
await userEvent.click(qontoChip);
const companyFilter = canvas.getAllByText(
(_, element) => !!element?.textContent?.includes('Company: Qonto'),
);
expect(companyFilter).not.toHaveLength(0);
});
await sleep(1000);
await step('Check filtered rows', async () => {
expect(canvas.getByText('Alexandre Prot')).toBeVisible();
expect(canvas.queryByText('John Doe')).toBeNull();
});
},
};

View File

@ -1,199 +0,0 @@
import { expect } from '@storybook/jest';
import { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import assert from 'assert';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { sleep } from '~/testing/sleep';
import { People } from '../People';
import { Story } from './People.stories';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People/Input',
component: People,
decorators: [PageDecorator],
args: { routePath: AppPath.PeoplePage },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export const InteractWithManyRows: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const firstRowEmailCell = await canvas.findByText(
mockedPeopleData[0].email,
{},
{ timeout: 3000 },
);
assert(firstRowEmailCell.parentElement);
const secondRowEmailCell = canvas.getByText(mockedPeopleData[1].email);
assert(secondRowEmailCell.parentElement);
expect(
canvas.queryByTestId('editable-cell-edit-mode-container'),
).toBeNull();
await userEvent.click(firstRowEmailCell.parentElement);
expect(
canvas.getByTestId('editable-cell-edit-mode-container'),
).toBeVisible();
await userEvent.click(secondRowEmailCell.parentElement);
await sleep(25);
expect(
canvas.queryByTestId('editable-cell-edit-mode-container'),
).toBeNull();
await userEvent.click(secondRowEmailCell.parentElement);
expect(
canvas.getByTestId('editable-cell-edit-mode-container'),
).toBeVisible();
},
};
export const CheckCheckboxes: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedPeopleData[0].displayName,
{},
{ timeout: 3000 },
);
});
const [, firstRowCheckbox, secondRowCheckbox] =
canvas.getAllByRole<HTMLInputElement>('checkbox');
await step('Select first row', async () => {
assert(firstRowCheckbox.parentElement);
await userEvent.click(firstRowCheckbox.parentElement);
await sleep(25);
expect(firstRowCheckbox).toBeChecked();
});
await step('Select second row', async () => {
await userEvent.click(secondRowCheckbox);
await sleep(25);
expect(secondRowCheckbox).toBeChecked();
});
await step('Unselect second row', async () => {
assert(secondRowCheckbox.parentElement);
await userEvent.click(secondRowCheckbox.parentElement);
await sleep(25);
expect(secondRowCheckbox).not.toBeChecked();
});
},
};
export const EditRelation: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Click on third row company cell', async () => {
const thirdRowCompanyCell = await canvas.findByText(
mockedPeopleData[2].company.name,
{},
{ timeout: 3000 },
);
await userEvent.click(thirdRowCompanyCell);
});
await step('Type "Air" in relation picker', async () => {
const relationSearchInput = canvas.getByPlaceholderText('Search');
await userEvent.type(relationSearchInput, 'Air', { delay: 200 });
});
await step('Select "Airbnb"', async () => {
const airbnbChip = await canvas.findByRole('listitem', {
name: (_, element) => !!element?.textContent?.includes('Airbnb'),
});
await userEvent.click(airbnbChip);
});
await step('Check if Airbnb is in the table', async () => {
expect(
await canvas.findByText('Airbnb', {}, { timeout: 3000 }),
).toBeVisible();
});
},
};
export const SelectRelationWithKeys: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Click on first row company cell', async () => {
const firstRowCompanyCell = await canvas.findByText(
mockedPeopleData[0].company.name,
{},
{ timeout: 3000 },
);
await userEvent.click(firstRowCompanyCell);
});
const relationSearchInput = canvas.getByPlaceholderText('Search');
await step('Type "Air" in relation picker', async () => {
await userEvent.type(relationSearchInput, 'Air', { delay: 200 });
});
await step('Select "Aircall"', async () => {
await userEvent.keyboard('{arrowdown}');
await sleep(50);
await userEvent.keyboard('{arrowup}');
await sleep(50);
await userEvent.keyboard('{arrowdown}');
await sleep(50);
await userEvent.keyboard('{arrowdown}');
await sleep(50);
await userEvent.keyboard('{arrowdown}');
await sleep(50);
await userEvent.keyboard('{enter}');
});
await step('Check if Aircall is in the table', async () => {
expect(
await canvas.findByText('Aircall', {}, { timeout: 3000 }),
).toBeVisible();
});
},
};

View File

@ -1,124 +0,0 @@
import { expect } from '@storybook/jest';
import { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { sleep } from '~/testing/sleep';
import { People } from '../People';
import { Story } from './People.stories';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People/SortBy',
component: People,
decorators: [PageDecorator],
args: { routePath: AppPath.PeoplePage },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
const peopleEmails = mockedPeopleData.map(({ email }) => email);
const sortedPeopleEmails = [...peopleEmails].sort((a, b) => a.localeCompare(b));
export const Email: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedPeopleData[0].displayName,
{},
{ timeout: 1000 },
);
});
await step('Click on sort button', async () => {
const sortButton = canvas.getByRole('button', { name: 'Sort' });
await userEvent.click(sortButton);
});
await step('Select sort by email', async () => {
const emailSortButton = canvas.getByTestId('select-sort-2');
await userEvent.click(emailSortButton);
await canvas.findByTestId('remove-icon-email', {}, { timeout: 3000 });
});
await sleep(1000);
await step('Check rows are sorted by email', async () => {
const emailCells = canvas.getAllByText(
(_, element) =>
sortedPeopleEmails.some((email) =>
element?.textContent?.includes(email),
),
{ selector: '[data-testid="editable-cell-display-mode"]' },
);
expect(emailCells).toHaveLength(sortedPeopleEmails.length);
sortedPeopleEmails.forEach((email, index) =>
expect(emailCells[index]).toHaveTextContent(email),
);
});
},
};
export const Reset: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Wait for rows to appear', async () => {
await canvas.findByText(
mockedPeopleData[0].displayName,
{},
{ timeout: 3000 },
);
});
await step('Click on sort button', async () => {
const sortButton = canvas.getByRole('button', { name: 'Sort' });
await userEvent.click(sortButton);
});
await step('Select sort by email', async () => {
const emailSortButton = canvas.getByTestId('select-sort-2');
await userEvent.click(emailSortButton);
expect(
await canvas.findByTestId('remove-icon-email'),
).toBeInTheDocument();
});
await step('Click on reset button', async () => {
const resetButton = canvas.getByRole('button', { name: 'Reset' });
await userEvent.click(resetButton);
expect(canvas.queryByTestId('remove-icon-email')).toBeNull();
});
await step('Check rows are in initial order', async () => {
const emailCells = canvas.getAllByText(
(_, element) =>
peopleEmails.some((email) => element?.textContent?.includes(email)),
{ selector: '[data-testid="editable-cell-display-mode"]' },
);
expect(emailCells).toHaveLength(peopleEmails.length);
peopleEmails.forEach((email, index) =>
expect(emailCells[index]).toHaveTextContent(email),
);
});
},
};

View File

@ -1,26 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { People } from '../People';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People',
component: People,
decorators: [PageDecorator],
args: { routePath: AppPath.PeoplePage },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof People>;
export const Default: Story = {};

View File

@ -1,30 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { AppPath } from '@/types/AppPath';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { PersonShow } from '../PersonShow';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People/Person',
component: PersonShow,
decorators: [PageDecorator],
args: {
routePath: AppPath.PersonShowPage,
routeParams: { ':personId': mockedPeopleData[0].id },
},
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof PersonShow>;
export const Default: Story = {};

View File

@ -1,111 +0,0 @@
import {
IconBrandLinkedin,
IconBrandX,
IconBriefcase,
IconBuildingSkyscraper,
IconCalendar,
IconMail,
IconMap,
IconPhone,
} from '@/ui/display/icon';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { FieldDefinition } from '@/ui/object/field/types/FieldDefinition';
import {
FieldDateMetadata,
FieldMetadata,
FieldPhoneMetadata,
FieldRelationMetadata,
FieldTextMetadata,
FieldURLMetadata,
} from '@/ui/object/field/types/FieldMetadata';
import { Company } from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
export const personShowFieldDefinition: FieldDefinition<FieldMetadata>[] = [
{
fieldMetadataId: 'email',
label: 'Email',
Icon: IconMail,
type: 'TEXT',
metadata: {
fieldName: 'email',
placeHolder: 'Email',
},
} satisfies FieldDefinition<FieldTextMetadata>,
{
fieldMetadataId: 'company',
label: 'Company',
Icon: IconBuildingSkyscraper,
type: 'RELATION',
metadata: {
fieldName: 'company',
relationType: Entity.Company,
},
entityChipDisplayMapper: (dataObject: Company) => {
return {
name: dataObject?.name,
pictureUrl: getLogoUrlFromDomainName(dataObject?.domainName),
avatarType: 'squared',
};
},
} satisfies FieldDefinition<FieldRelationMetadata>,
{
fieldMetadataId: 'phone',
label: 'Phone',
Icon: IconPhone,
type: 'PHONE',
metadata: {
fieldName: 'phone',
placeHolder: 'Phone',
},
} satisfies FieldDefinition<FieldPhoneMetadata>,
{
fieldMetadataId: 'jobTitle',
label: 'Job Title',
Icon: IconBriefcase,
type: 'TEXT',
metadata: {
fieldName: 'jobTitle',
placeHolder: 'Job Title',
},
} satisfies FieldDefinition<FieldTextMetadata>,
{
fieldMetadataId: 'city',
label: 'City',
Icon: IconMap,
type: 'TEXT',
metadata: {
fieldName: 'city',
placeHolder: 'City',
},
} satisfies FieldDefinition<FieldTextMetadata>,
{
fieldMetadataId: 'linkedinUrl',
label: 'Linkedin URL',
Icon: IconBrandLinkedin,
type: 'URL',
metadata: {
fieldName: 'linkedinUrl',
placeHolder: 'Linkedin URL',
},
} satisfies FieldDefinition<FieldURLMetadata>,
{
fieldMetadataId: 'xUrl',
label: 'X URL',
Icon: IconBrandX,
type: 'URL',
metadata: {
fieldName: 'xUrl',
placeHolder: 'X URL',
},
} satisfies FieldDefinition<FieldURLMetadata>,
{
fieldMetadataId: 'createdAt',
label: 'Created at',
Icon: IconCalendar,
type: 'DATE',
metadata: {
fieldName: 'createdAt',
},
} satisfies FieldDefinition<FieldDateMetadata>,
];

View File

@ -1,59 +0,0 @@
import { FilterDropdownCompanySearchSelect } from '@/companies/components/FilterDropdownCompanySearchSelect';
import {
IconBuildingSkyscraper,
IconCalendarEvent,
IconMail,
IconMap,
IconPhone,
IconUser,
} from '@/ui/display/icon/index';
import { FilterDefinitionByEntity } from '@/ui/object/object-filter-dropdown/types/FilterDefinitionByEntity';
import { Person } from '~/generated/graphql';
export const personTableFilterDefinitions: FilterDefinitionByEntity<Person>[] =
[
{
fieldMetadataId: 'firstName',
label: 'First name',
Icon: IconUser,
type: 'TEXT',
},
{
fieldMetadataId: 'lastName',
label: 'Last name',
Icon: IconUser,
type: 'TEXT',
},
{
fieldMetadataId: 'email',
label: 'Email',
Icon: IconMail,
type: 'TEXT',
},
{
fieldMetadataId: 'companyId',
label: 'Company',
Icon: IconBuildingSkyscraper,
type: 'ENTITY',
// TODO: replace this with a component that selects the dropdown to use based on the entity type
entitySelectComponent: <FilterDropdownCompanySearchSelect />,
},
{
fieldMetadataId: 'phone',
label: 'Phone',
Icon: IconPhone,
type: 'TEXT',
},
{
fieldMetadataId: 'createdAt',
label: 'Created at',
Icon: IconCalendarEvent,
type: 'DATE',
},
{
fieldMetadataId: 'city',
label: 'City',
Icon: IconMap,
type: 'TEXT',
},
];

View File

@ -1,51 +0,0 @@
import {
IconBuildingSkyscraper,
IconCalendarEvent,
IconMail,
IconMap,
IconPhone,
IconUser,
} from '@/ui/display/icon/index';
import { SortDefinition } from '@/ui/object/object-sort-dropdown/types/SortDefinition';
import { SortDirection } from '@/ui/object/object-sort-dropdown/types/SortDirection';
export const personTableSortDefinitions: SortDefinition[] = [
{
fieldMetadataId: 'fullname',
label: 'People',
Icon: IconUser,
getOrderByTemplate: (direction: SortDirection) => [
{ firstName: direction },
{ lastName: direction },
],
},
{
fieldMetadataId: 'company_name',
label: 'Company',
Icon: IconBuildingSkyscraper,
getOrderByTemplate: (direction: SortDirection) => [
{ company: { name: direction } },
],
},
{
fieldMetadataId: 'email',
label: 'Email',
Icon: IconMail,
},
{
fieldMetadataId: 'phone',
label: 'Phone',
Icon: IconPhone,
},
{
fieldMetadataId: 'createdAt',
label: 'Created at',
Icon: IconCalendarEvent,
},
{
fieldMetadataId: 'city',
label: 'City',
Icon: IconMap,
},
];

View File

@ -36,11 +36,11 @@ export const SettingsWorkspaceMembers = () => {
const { objects: workspaceMembers } =
useFindManyObjectRecords<WorkspaceMember>({
objectNamePlural: 'workspaceMembersV2',
objectNamePlural: 'workspaceMembers',
});
const { deleteOneObject: deleteOneWorkspaceMember } =
useDeleteOneObjectRecord<WorkspaceMember>({
objectNameSingular: 'workspaceMemberV2',
objectNameSingular: 'workspaceMember',
});
const currentWorkspace = useRecoilValue(currentWorkspaceState);

View File

@ -19,7 +19,6 @@ import { H2Title } from '@/ui/display/typography/components/H2Title';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { ViewType } from '@/views/types/ViewType';
export const SettingsNewObject = () => {
const navigate = useNavigate();
@ -33,7 +32,7 @@ export const SettingsNewObject = () => {
} = useObjectMetadataItemForSettings();
const { createOneObject: createOneView } = useCreateOneObjectRecord({
objectNameSingular: 'viewV2',
objectNameSingular: 'view',
});
const [
@ -79,12 +78,6 @@ export const SettingsNewObject = () => {
icon: customFormValues.icon,
});
await createOneView?.({
objectMetadataId: createdObject.data?.createOneObject.id,
type: ViewType.Table,
name: `All ${customFormValues.labelPlural}`,
});
navigate(
createdObject.data?.createOneObject.isActive
? `/settings/objects/${getObjectSlug(

View File

@ -54,10 +54,10 @@ export const SettingsObjectDetail = () => {
if (!activeObjectMetadataItem) return null;
const activeMetadataFields = activeObjectMetadataItem.fields.filter(
(metadataField) => metadataField.isActive,
(metadataField) => metadataField.isActive && !metadataField.isSystem,
);
const disabledMetadataFields = activeObjectMetadataItem.fields.filter(
(metadataField) => !metadataField.isActive,
(metadataField) => !metadataField.isActive && !metadataField.isSystem,
);
const handleDisable = async () => {

View File

@ -54,7 +54,7 @@ export const SettingsObjectNewFieldStep2 = () => {
initForm({
relation: {
field: { icon: activeObjectMetadataItem.icon },
objectMetadataId: findObjectMetadataItemByNamePlural('peopleV2')?.id,
objectMetadataId: findObjectMetadataItemByNamePlural('people')?.id,
},
});
}, [
@ -69,11 +69,11 @@ export const SettingsObjectNewFieldStep2 = () => {
const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]);
const { createOneObject: createOneViewField } = useCreateOneObjectRecord({
objectNameSingular: 'viewFieldV2',
objectNameSingular: 'viewField',
});
useFindManyObjectRecords({
objectNamePlural: 'viewsV2',
objectNamePlural: 'views',
filter: {
type: { eq: ViewType.Table },
objectMetadataId: { eq: activeObjectMetadataItem?.id },
@ -88,7 +88,7 @@ export const SettingsObjectNewFieldStep2 = () => {
});
useFindManyObjectRecords({
objectNamePlural: 'viewsV2',
objectNamePlural: 'views',
skip: !formValues.relation?.objectMetadataId,
filter: {
type: { eq: ViewType.Table },

View File

@ -13,6 +13,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
import { useGeneratedApiKeys } from '@/settings/developers/hooks/useGeneratedApiKeys';
import { generatedApiKeyFamilyState } from '@/settings/developers/states/generatedApiKeyFamilyState';
import { ApiKey } from '@/settings/developers/types/ApiKey';
import { computeNewExpirationDate } from '@/settings/developers/utils/compute-new-expiration-date';
import { formatExpiration } from '@/settings/developers/utils/format-expiration';
import { IconRepeat, IconSettings, IconTrash } from '@/ui/display/icon';
@ -22,7 +23,7 @@ import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { ApiKey, useGenerateOneApiKeyTokenMutation } from '~/generated/graphql';
import { useGenerateApiKeyTokenMutation } from '~/generated/graphql';
const StyledInfo = styled.span`
color: ${({ theme }) => theme.font.color.light};
@ -48,18 +49,18 @@ export const SettingsDevelopersApiKeyDetail = () => {
);
const { performOptimisticEvict } = useOptimisticEvict();
const [generateOneApiKeyToken] = useGenerateOneApiKeyTokenMutation();
const [generateOneApiKeyToken] = useGenerateApiKeyTokenMutation();
const { createOneObject: createOneApiKey } = useCreateOneObjectRecord<ApiKey>(
{
objectNameSingular: 'apiKeyV2',
objectNameSingular: 'apiKey',
},
);
const { updateOneObject: updateApiKey } = useUpdateOneObjectRecord<ApiKey>({
objectNameSingular: 'apiKeyV2',
objectNameSingular: 'apiKey',
});
const { object: apiKeyData } = useFindOneObjectRecord({
objectNameSingular: 'apiKeyV2',
objectNameSingular: 'apiKey',
objectRecordId: apiKeyId,
});
@ -68,7 +69,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
idToUpdate: apiKeyId,
input: { revokedAt: DateTime.now().toString() },
});
performOptimisticEvict('ApiKeyV2', 'id', apiKeyId);
performOptimisticEvict('ApiKey', 'id', apiKeyId);
if (redirect) {
navigate('/settings/developers/api-keys');
}
@ -89,16 +90,13 @@ export const SettingsDevelopersApiKeyDetail = () => {
const tokenData = await generateOneApiKeyToken({
variables: {
data: {
id: newApiKey.id,
expiresAt: newApiKey.expiresAt,
name: newApiKey.name, // TODO update typing to remove useless name param here
},
apiKeyId: newApiKey.id,
expiresAt: newApiKey?.expiresAt,
},
});
return {
id: newApiKey.id,
token: tokenData.data?.generateApiKeyV2Token.token,
token: tokenData.data?.generateApiKeyToken.token,
};
};

View File

@ -45,14 +45,14 @@ export const SettingsDevelopersApiKeys = () => {
const [apiKeys, setApiKeys] = useState<Array<ApiFieldItem>>([]);
const { registerOptimisticEffect } = useOptimisticEffect({
objectNameSingular: 'apiKeyV2',
objectNameSingular: 'apiKey',
});
const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({
objectNameSingular: 'apiKeyV2',
objectNameSingular: 'apiKey',
});
const filter = { revokedAt: { eq: null } };
useFindManyObjectRecords({
objectNamePlural: 'apiKeysV2',
objectNamePlural: 'apiKeys',
filter,
onCompleted: (data) => {
setApiKeys(
@ -66,7 +66,7 @@ export const SettingsDevelopersApiKeys = () => {
);
if (foundObjectMetadataItem) {
registerOptimisticEffect({
variables: { filter },
variables: { filter, first: 30, orderBy: {} },
definition: getRecordOptimisticEffectDefinition({
objectMetadataItem: foundObjectMetadataItem,
}),

View File

@ -8,6 +8,7 @@ import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderCon
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { ExpirationDates } from '@/settings/developers/constants/expirationDates';
import { useGeneratedApiKeys } from '@/settings/developers/hooks/useGeneratedApiKeys';
import { ApiKey } from '@/settings/developers/types/ApiKey';
import { IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Select } from '@/ui/input/components/Select';
@ -15,10 +16,10 @@ import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { ApiKey, useGenerateOneApiKeyTokenMutation } from '~/generated/graphql';
import { useGenerateApiKeyTokenMutation } from '~/generated/graphql';
export const SettingsDevelopersApiKeysNew = () => {
const [generateOneApiKeyToken] = useGenerateOneApiKeyTokenMutation();
const [generateOneApiKeyToken] = useGenerateApiKeyTokenMutation();
const navigate = useNavigate();
const setGeneratedApi = useGeneratedApiKeys();
const [formValues, setFormValues] = useState<{
@ -30,14 +31,12 @@ export const SettingsDevelopersApiKeysNew = () => {
});
const { createOneObject: createOneApiKey } = useCreateOneObjectRecord<ApiKey>(
{
objectNameSingular: 'apiKeyV2',
},
{ objectNameSingular: 'apiKey' },
);
const onSave = async () => {
const expiresAt = formValues.expirationDate
? DateTime.now().plus({ days: formValues.expirationDate }).toString()
: null;
const expiresAt = DateTime.now()
.plus({ days: formValues.expirationDate ?? 30 })
.toString();
const newApiKey = await createOneApiKey?.({
name: formValues.name,
expiresAt,
@ -49,15 +48,12 @@ export const SettingsDevelopersApiKeysNew = () => {
const tokenData = await generateOneApiKeyToken({
variables: {
data: {
id: newApiKey.id,
expiresAt: newApiKey.expiresAt,
name: newApiKey.name, // TODO update typing to remove useless name param here
},
apiKeyId: newApiKey.id,
expiresAt: expiresAt,
},
});
if (tokenData.data?.generateApiKeyV2Token) {
setGeneratedApi(newApiKey.id, tokenData.data.generateApiKeyV2Token.token);
if (tokenData.data?.generateApiKeyToken) {
setGeneratedApi(newApiKey.id, tokenData.data.generateApiKeyToken.token);
navigate(`/settings/developers/api-keys/${newApiKey.id}`);
}
};

View File

@ -1,13 +1,13 @@
import { IconUser, IconUserCircle } from '@/ui/display/icon';
import { Activity } from '@/activities/types/Activity';
import { IconUserCircle } from '@/ui/display/icon';
import { FilterDefinitionByEntity } from '@/ui/object/object-filter-dropdown/types/FilterDefinitionByEntity';
import { FilterDropdownUserSearchSelect } from '@/users/components/FilterDropdownUserSearchSelect';
import { Activity } from '~/generated/graphql';
export const tasksFilterDefinitions: FilterDefinitionByEntity<Activity>[] = [
{
fieldMetadataId: 'assigneeId',
label: 'Assignee',
Icon: IconUser,
iconName: 'IconUser',
type: 'ENTITY',
entitySelectComponent: <FilterDropdownUserSearchSelect />,
selectAllLabel: 'All assignees',