Feat/generic editable cell all types (#987)

* Added generic relation cell

* Deactivated debug

* Added default warning

* Put back display component

* Removed unused types

* wip

* Renamed to view field

* Use new view field structure to have chip working

* Finished

* Added a temp feature flag

* Added double text chip cell

* Ok

* Finished tables

* Fixed icon size

* Fixed bug on date field

* Use icon index

* Fix

* Fixed naming

* Fix

* removed file from merge

* Fixed tests

* Coverage
This commit is contained in:
Lucas Bordeau
2023-07-29 23:48:43 +02:00
committed by GitHub
parent dc18bc40b0
commit d9f6ae8663
77 changed files with 1730 additions and 326 deletions

View File

@ -1,34 +0,0 @@
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { ViewFieldDefinition } from '@/ui/table/types/ViewField';
import { useSetEntityTableData } from '../hooks/useSetEntityTableData';
import { defaultOrderBy } from '../queries';
export function GenericEntityTableData({
useGetRequest,
getRequestResultKey,
orderBy = defaultOrderBy,
whereFilters,
viewFields,
filterDefinitionArray,
}: {
useGetRequest: any;
getRequestResultKey: string;
orderBy?: any;
whereFilters?: any;
viewFields: ViewFieldDefinition<unknown>[];
filterDefinitionArray: FilterDefinition[];
}) {
const setEntityTableData = useSetEntityTableData();
useGetRequest({
variables: { orderBy, where: whereFilters },
onCompleted: (data: any) => {
const entities = data[getRequestResultKey] ?? [];
setEntityTableData(entities, viewFields, filterDefinitionArray);
},
});
return <></>;
}

View File

@ -1,52 +0,0 @@
import {
IconBriefcase,
IconBuildingSkyscraper,
IconMap,
} from '@tabler/icons-react';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
} from '@/ui/table/types/ViewField';
export const peopleViewFields: ViewFieldDefinition<unknown>[] = [
{
id: 'city',
columnLabel: 'City',
columnIcon: <IconMap size={16} />,
columnSize: 150,
type: 'text',
columnOrder: 1,
metadata: {
fieldName: 'city',
placeHolder: 'City',
},
} as ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'jobTitle',
columnLabel: 'Job title',
columnIcon: <IconBriefcase size={16} />,
columnSize: 150,
type: 'text',
columnOrder: 2,
metadata: {
fieldName: 'jobTitle',
placeHolder: 'Job title',
},
} as ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'company',
columnLabel: 'Company',
columnIcon: <IconBuildingSkyscraper size={16} />,
columnSize: 150,
type: 'relation',
relationType: Entity.Company,
columnOrder: 3,
metadata: {
fieldName: 'company',
relationType: Entity.Company,
},
} as ViewFieldDefinition<ViewFieldRelationMetadata>,
];

View File

@ -0,0 +1,122 @@
import {
IconBrandLinkedin,
IconBriefcase,
IconBuildingSkyscraper,
IconCalendarEvent,
IconMail,
IconMap,
IconPhone,
IconUser,
} from '@/ui/icon/index';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
ViewFieldMetadata,
ViewFieldPhoneMetadata,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
ViewFieldURLMetadata,
} from '@/ui/table/types/ViewField';
export const peopleViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
{
id: 'displayName',
columnLabel: 'People',
columnIcon: <IconUser />,
columnSize: 210,
columnOrder: 1,
metadata: {
type: 'double-text-chip',
firstValueFieldName: 'firstName',
secondValueFieldName: 'lastName',
firstValuePlaceholder: 'First name',
secondValuePlaceholder: 'Last name',
entityType: Entity.Person,
},
} satisfies ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>,
{
id: 'email',
columnLabel: 'Email',
columnIcon: <IconMail />,
columnSize: 150,
columnOrder: 2,
metadata: {
type: 'text',
fieldName: 'email',
placeHolder: 'Email',
},
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'company',
columnLabel: 'Company',
columnIcon: <IconBuildingSkyscraper />,
columnSize: 150,
columnOrder: 3,
metadata: {
type: 'relation',
fieldName: 'company',
relationType: Entity.Company,
},
} satisfies ViewFieldDefinition<ViewFieldRelationMetadata>,
{
id: 'phone',
columnLabel: 'Phone',
columnIcon: <IconPhone />,
columnSize: 150,
columnOrder: 4,
metadata: {
type: 'phone',
fieldName: 'phone',
placeHolder: 'Phone',
},
} satisfies ViewFieldDefinition<ViewFieldPhoneMetadata>,
{
id: 'createdAt',
columnLabel: 'Creation',
columnIcon: <IconCalendarEvent />,
columnSize: 150,
columnOrder: 5,
metadata: {
type: 'date',
fieldName: 'createdAt',
},
} satisfies ViewFieldDefinition<ViewFieldDateMetadata>,
{
id: 'city',
columnLabel: 'City',
columnIcon: <IconMap />,
columnSize: 150,
columnOrder: 6,
metadata: {
type: 'text',
fieldName: 'city',
placeHolder: 'City',
},
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'jobTitle',
columnLabel: 'Job title',
columnIcon: <IconBriefcase />,
columnSize: 150,
columnOrder: 7,
metadata: {
type: 'text',
fieldName: 'jobTitle',
placeHolder: 'Job title',
},
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'linkedin',
columnLabel: 'LinkedIn',
columnIcon: <IconBrandLinkedin />,
columnSize: 150,
columnOrder: 8,
metadata: {
type: 'url',
fieldName: 'linkedinUrl',
placeHolder: 'LinkedIn',
},
} satisfies ViewFieldDefinition<ViewFieldURLMetadata>,
];

View File

@ -1,63 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
import { viewFieldsState } from '@/ui/table/states/viewFieldsState';
import { ViewFieldDefinition } from '@/ui/table/types/ViewField';
import { availableFiltersScopedState } from '../../ui/filter-n-sort/states/availableFiltersScopedState';
import { useContextScopeId } from '../../ui/recoil-scope/hooks/useContextScopeId';
import { useResetTableRowSelection } from '../../ui/table/hooks/useResetTableRowSelection';
import { entityTableDimensionsState } from '../../ui/table/states/entityTableDimensionsState';
import { isFetchingEntityTableDataState } from '../../ui/table/states/isFetchingEntityTableDataState';
import { TableContext } from '../../ui/table/states/TableContext';
import { tableRowIdsState } from '../../ui/table/states/tableRowIdsState';
export function useSetEntityTableData() {
const resetTableRowSelection = useResetTableRowSelection();
const tableContextScopeId = useContextScopeId(TableContext);
return useRecoilCallback(
({ set, snapshot }) =>
<T extends { id: string }>(
newEntityArray: T[],
viewFields: ViewFieldDefinition<unknown>[],
filters: FilterDefinition[],
) => {
for (const entity of newEntityArray) {
const currentEntity = snapshot
.getLoadable(tableEntitiesFamilyState(entity.id))
.valueOrThrow();
if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) {
set(tableEntitiesFamilyState(entity.id), entity);
}
}
const entityIds = newEntityArray.map((entity) => entity.id);
set(tableRowIdsState, (currentRowIds) => {
if (JSON.stringify(currentRowIds) !== JSON.stringify(entityIds)) {
return entityIds;
}
return currentRowIds;
});
resetTableRowSelection();
set(entityTableDimensionsState, {
numberOfColumns: viewFields.length,
numberOfRows: entityIds.length,
});
set(availableFiltersScopedState(tableContextScopeId), filters);
set(viewFieldsState, viewFields);
set(isFetchingEntityTableDataState, false);
},
[],
);
}

View File

@ -1,80 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
import { viewFieldsState } from '@/ui/table/states/viewFieldsState';
import { isViewFieldChip } from '@/ui/table/types/guards/isViewFieldChip';
import { isViewFieldRelation } from '@/ui/table/types/guards/isViewFieldRelation';
import { isViewFieldText } from '@/ui/table/types/guards/isViewFieldText';
export function useUpdateEntityField() {
const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext);
const [updateEntity] = useUpdateEntityMutation();
const viewFields = useRecoilValue(viewFieldsState);
return function updatePeopleField(
currentEntityId: string,
viewFieldId: string,
newFieldValue: unknown,
) {
const viewField = viewFields.find(
(metadata) => metadata.id === viewFieldId,
);
if (!viewField) {
throw new Error(`View field not found for id ${viewFieldId}`);
}
// TODO: improve type narrowing here with validation maybe ? Also validate the newFieldValue with linked type guards
if (isViewFieldRelation(viewField)) {
const newSelectedEntity = newFieldValue as EntityForSelect | null;
const fieldName = viewField.metadata.fieldName;
if (!newSelectedEntity) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[fieldName]: {
disconnect: true,
},
},
},
});
} else {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[fieldName]: {
connect: { id: newSelectedEntity.id },
},
},
},
});
}
} else if (isViewFieldChip(viewField)) {
const newContent = newFieldValue as string;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.contentFieldName]: newContent },
},
});
} else if (isViewFieldText(viewField)) {
const newContent = newFieldValue as string;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
}
};
}

View File

@ -1,8 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import { defaultOrderBy } from '@/companies/queries';
import { GenericEntityTableData } from '@/people/components/GenericEntityTableData';
import { peopleViewFields } from '@/people/constants/peopleFieldMetadataArray';
import { peopleViewFields } from '@/people/constants/peopleViewFields';
import { PeopleSelectedSortType } from '@/people/queries';
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
@ -10,6 +9,7 @@ import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIn
import { IconList } from '@/ui/icon';
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
import { EntityTable } from '@/ui/table/components/EntityTableV2';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { TableContext } from '@/ui/table/states/TableContext';
import {
PersonOrderByWithRelationInput,