Skip to content

Commit

Permalink
Feat/generic editable cell chip (twentyhq#982)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lucasbordeau authored and AdityaPimpalkar committed Aug 3, 2023
1 parent dda3fa8 commit ec4fda0
Show file tree
Hide file tree
Showing 30 changed files with 416 additions and 143 deletions.
3 changes: 3 additions & 0 deletions front/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks';

import { SignInUp } from './pages/auth/SignInUp';

// TEMP FEATURE FLAG FOR VIEW FIELDS
export const ACTIVATE_VIEW_FIELDS = false;

export function App() {
return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IconBuildingSkyscraper } from '@tabler/icons-react';

import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import {
ViewFieldChipMetadata,
ViewFieldDefinition,
} from '@/ui/table/types/ViewField';

export const companyViewFields: ViewFieldDefinition<unknown>[] = [
{
columnLabel: 'Name',
columnIcon: <IconBuildingSkyscraper size={16} />,
columnSize: 150,
type: 'chip',
columnOrder: 1,
metadata: {
urlFieldName: 'domainName',
contentFieldName: 'name',
relationType: Entity.Company,
},
} as ViewFieldDefinition<ViewFieldChipMetadata>,
];
54 changes: 54 additions & 0 deletions front/src/modules/companies/table/components/CompanyTableV2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useCallback, useMemo, useState } from 'react';

import { companyViewFields } from '@/companies/constants/companyFieldMetadataArray';
import { CompaniesSelectedSortType, defaultOrderBy } from '@/companies/queries';
import { GenericEntityTableData } from '@/people/components/GenericEntityTableData';
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
import { IconList } from '@/ui/icon';
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
import { EntityTable } from '@/ui/table/components/EntityTableV2';
import { TableContext } from '@/ui/table/states/TableContext';
import {
CompanyOrderByWithRelationInput,
useGetCompaniesQuery,
useUpdateOneCompanyMutation,
} from '~/generated/graphql';
import { companiesFilters } from '~/pages/companies/companies-filters';
import { availableSorts } from '~/pages/companies/companies-sorts';

export function CompanyTable() {
const [orderBy, setOrderBy] =
useState<CompanyOrderByWithRelationInput[]>(defaultOrderBy);

const updateSorts = useCallback((sorts: Array<CompaniesSelectedSortType>) => {
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
}, []);

const filters = useRecoilScopedValue(filtersScopedState, TableContext);

const whereFilters = useMemo(() => {
return { AND: filters.map(turnFilterIntoWhereClause) };
}, [filters]) as any;

return (
<>
<GenericEntityTableData
getRequestResultKey="companies"
useGetRequest={useGetCompaniesQuery}
orderBy={orderBy}
whereFilters={whereFilters}
viewFields={companyViewFields}
filterDefinitionArray={companiesFilters}
/>
<EntityTable
viewName="All Companies"
viewIcon={<IconList size={16} />}
availableSorts={availableSorts}
onSortsUpdate={updateSorts}
useUpdateEntityMutation={useUpdateOneCompanyMutation}
/>
</>
);
}
11 changes: 7 additions & 4 deletions front/src/modules/people/components/GenericEntityTableData.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EntityFieldMetadata } from '@/ui/table/types/EntityFieldMetadata';
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';
Expand All @@ -8,13 +9,15 @@ export function GenericEntityTableData({
getRequestResultKey,
orderBy = defaultOrderBy,
whereFilters,
fieldMetadataArray,
viewFields,
filterDefinitionArray,
}: {
useGetRequest: any;
getRequestResultKey: string;
orderBy?: any;
whereFilters?: any;
fieldMetadataArray: EntityFieldMetadata[];
viewFields: ViewFieldDefinition<unknown>[];
filterDefinitionArray: FilterDefinition[];
}) {
const setEntityTableData = useSetEntityTableData();

Expand All @@ -23,7 +26,7 @@ export function GenericEntityTableData({
onCompleted: (data: any) => {
const entities = data[getRequestResultKey] ?? [];

setEntityTableData(entities, fieldMetadataArray);
setEntityTableData(entities, viewFields, filterDefinitionArray);
},
});

Expand Down
47 changes: 33 additions & 14 deletions front/src/modules/people/constants/peopleFieldMetadataArray.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,48 @@ import {
} from '@tabler/icons-react';

import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import { EntityFieldMetadata } from '@/ui/table/types/EntityFieldMetadata';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
} from '@/ui/table/types/ViewField';

export const peopleFieldMetadataArray: EntityFieldMetadata[] = [
export const peopleViewFields: ViewFieldDefinition<unknown>[] = [
{
fieldName: 'city',
label: 'City',
icon: <IconMap size={16} />,
id: 'city',
columnLabel: 'City',
columnIcon: <IconMap size={16} />,
columnSize: 150,
type: 'text',
},
columnOrder: 1,
metadata: {
fieldName: 'city',
placeHolder: 'City',
},
} as ViewFieldDefinition<ViewFieldTextMetadata>,
{
fieldName: 'jobTitle',
label: 'Job title',
icon: <IconBriefcase size={16} />,
id: 'jobTitle',
columnLabel: 'Job title',
columnIcon: <IconBriefcase size={16} />,
columnSize: 150,
type: 'text',
},
columnOrder: 2,
metadata: {
fieldName: 'jobTitle',
placeHolder: 'Job title',
},
} as ViewFieldDefinition<ViewFieldTextMetadata>,
{
fieldName: 'company',
label: 'Company',
icon: <IconBuildingSkyscraper size={16} />,
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>,
];
21 changes: 6 additions & 15 deletions front/src/modules/people/hooks/useSetEntityTableData.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useRecoilCallback } from 'recoil';

import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { entityFieldMetadataArrayState } from '@/ui/table/states/entityFieldMetadataArrayState';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
import { EntityFieldMetadata } from '@/ui/table/types/EntityFieldMetadata';
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';
Expand All @@ -22,7 +22,8 @@ export function useSetEntityTableData() {
({ set, snapshot }) =>
<T extends { id: string }>(
newEntityArray: T[],
entityFieldMetadataArray: EntityFieldMetadata[],
viewFields: ViewFieldDefinition<unknown>[],
filters: FilterDefinition[],
) => {
for (const entity of newEntityArray) {
const currentEntity = snapshot
Expand All @@ -47,23 +48,13 @@ export function useSetEntityTableData() {
resetTableRowSelection();

set(entityTableDimensionsState, {
numberOfColumns: entityFieldMetadataArray.length,
numberOfColumns: viewFields.length,
numberOfRows: entityIds.length,
});

const filters = entityFieldMetadataArray.map(
(fieldMetadata) =>
({
field: fieldMetadata.fieldName,
icon: fieldMetadata.filterIcon,
label: fieldMetadata.label,
type: fieldMetadata.type,
} as FilterDefinition),
);

set(availableFiltersScopedState(tableContextScopeId), filters);

set(entityFieldMetadataArrayState, entityFieldMetadataArray);
set(viewFieldsState, viewFields);

set(isFetchingEntityTableDataState, false);
},
Expand Down
39 changes: 27 additions & 12 deletions front/src/modules/people/hooks/useUpdateEntityField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,38 @@ import { useContext } from 'react';
import { useRecoilValue } from 'recoil';

import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
import { entityFieldMetadataArrayState } from '@/ui/table/states/entityFieldMetadataArrayState';
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 entityFieldMetadataArray = useRecoilValue(
entityFieldMetadataArrayState,
);
const viewFields = useRecoilValue(viewFieldsState);

return function updatePeopleField(
currentEntityId: string,
fieldName: string,
viewFieldId: string,
newFieldValue: unknown,
) {
const fieldMetadata = entityFieldMetadataArray.find(
(metadata) => metadata.fieldName === fieldName,
const viewField = viewFields.find(
(metadata) => metadata.id === viewFieldId,
);

if (!fieldMetadata) {
throw new Error(`Field metadata not found for field ${fieldName}`);
if (!viewField) {
throw new Error(`View field not found for id ${viewFieldId}`);
}

if (fieldMetadata.type === 'relation') {
// 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: {
Expand All @@ -53,11 +57,22 @@ export function useUpdateEntityField() {
},
});
}
} else {
} 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: { [fieldName]: newFieldValue },
data: { [viewField.metadata.fieldName]: newContent },
},
});
}
Expand Down
6 changes: 4 additions & 2 deletions front/src/modules/people/table/components/PeopleTableV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react';

import { defaultOrderBy } from '@/companies/queries';
import { GenericEntityTableData } from '@/people/components/GenericEntityTableData';
import { peopleFieldMetadataArray } from '@/people/constants/peopleFieldMetadataArray';
import { peopleViewFields } from '@/people/constants/peopleFieldMetadataArray';
import { PeopleSelectedSortType } from '@/people/queries';
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
Expand All @@ -16,6 +16,7 @@ import {
useGetPeopleQuery,
useUpdateOnePersonMutation,
} from '~/generated/graphql';
import { peopleFilters } from '~/pages/people/people-filters';
import { availableSorts } from '~/pages/people/people-sorts';

export function PeopleTable() {
Expand All @@ -39,7 +40,8 @@ export function PeopleTable() {
useGetRequest={useGetPeopleQuery}
orderBy={orderBy}
whereFilters={whereFilters}
fieldMetadataArray={peopleFieldMetadataArray}
viewFields={peopleViewFields}
filterDefinitionArray={peopleFilters}
/>
<EntityTable
viewName="All People"
Expand Down
6 changes: 3 additions & 3 deletions front/src/modules/ui/table/components/EntityTableCellV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { RecoilScope } from '../../recoil-scope/components/RecoilScope';
import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected';
import { ColumnIndexContext } from '../states/ColumnIndexContext';
import { contextMenuPositionState } from '../states/contextMenuPositionState';
import { EntityFieldMetadataContext } from '../states/EntityFieldMetadataContext';
import { ViewFieldContext } from '../states/ViewFieldContext';

export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
Expand All @@ -25,7 +25,7 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
});
}

const entityFieldMetadata = useContext(EntityFieldMetadataContext);
const entityFieldMetadata = useContext(ViewFieldContext);

if (!entityFieldMetadata) {
return null;
Expand All @@ -42,7 +42,7 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
maxWidth: entityFieldMetadata.columnSize,
}}
>
<GenericEditableCell entityFieldMetadata={entityFieldMetadata} />
<GenericEditableCell fieldDefinition={entityFieldMetadata} />
</td>
</ColumnIndexContext.Provider>
</RecoilScope>
Expand Down
Loading

0 comments on commit ec4fda0

Please sign in to comment.