Feat/metadata with datatable v2 (#2110)

* Reworked metadata creation

* Wip

* Fix from PR

* Removed consolelog

* Post merge

* Fixed seeds

* Wip

* Added dynamic routing

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-10-18 19:41:02 +02:00
committed by GitHub
parent 830dfc4d99
commit c590300bf1
14 changed files with 189 additions and 75 deletions

View File

@ -59,15 +59,7 @@ export const App = () => {
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} /> <Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
<Route path={AppPath.OpportunitiesPage} element={<Opportunities />} /> <Route path={AppPath.OpportunitiesPage} element={<Opportunities />} />
<Route <Route path={AppPath.ObjectTablePage} element={<ObjectTablePage />} />
path={AppPath.ObjectTablePage}
element={
<ObjectTablePage
objectNamePlural="suppliers"
objectNameSingular="supplier"
/>
}
/>
<Route <Route
path={AppPath.SettingsCatchAll} path={AppPath.SettingsCatchAll}

View File

@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount'; import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { Favorites } from '@/favorites/components/Favorites'; import { Favorites } from '@/favorites/components/Favorites';
import { MetadataObjectNavItems } from '@/metadata/components/MetadataObjectNavItems';
import { SettingsNavbar } from '@/settings/components/SettingsNavbar'; import { SettingsNavbar } from '@/settings/components/SettingsNavbar';
import { import {
IconBell, IconBell,
@ -89,6 +90,7 @@ export const AppNavbar = () => {
Icon={IconTargetArrow} Icon={IconTargetArrow}
active={currentPath === '/opportunities'} active={currentPath === '/opportunities'}
/> />
<MetadataObjectNavItems />
</MainNavbar> </MainNavbar>
) : ( ) : (
<SettingsNavbar /> <SettingsNavbar />

View File

@ -0,0 +1,29 @@
import { IconBuildingSkyscraper } from '@/ui/display/icon';
import NavItem from '@/ui/navigation/navbar/components/NavItem';
import { useGetClientConfigQuery } from '~/generated/graphql';
import { capitalize } from '~/utils/string/capitalize';
import { useFindManyMetadataObjects } from '../hooks/useFindManyMetadataObjects';
export const MetadataObjectNavItems = () => {
const { data } = useGetClientConfigQuery();
const { metadataObjects } = useFindManyMetadataObjects();
const isFlexibleBackendEnabled = data?.clientConfig?.flexibleBackendEnabled;
if (!isFlexibleBackendEnabled) return <></>;
return (
<>
{metadataObjects.map((metadataObject) => (
<NavItem
key={metadataObject.id}
label={capitalize(metadataObject.namePlural)}
to={`/objects/${metadataObject.namePlural}`}
Icon={IconBuildingSkyscraper}
/>
))}
</>
);
};

View File

@ -12,25 +12,24 @@ import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilSco
import { useFindManyObjects } from '../hooks/useFindManyObjects'; import { useFindManyObjects } from '../hooks/useFindManyObjects';
import { useSetObjectDataTableData } from '../hooks/useSetDataTableData'; import { useSetObjectDataTableData } from '../hooks/useSetDataTableData';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
export type ObjectDataTableEffectProps = MetadataObjectIdentifier;
export const ObjectDataTableEffect = ({ export const ObjectDataTableEffect = ({
objectNameSingular,
objectNamePlural, objectNamePlural,
}: { }: ObjectDataTableEffectProps) => {
objectNamePlural: string;
objectNameSingular: string;
}) => {
const setDataTableData = useSetObjectDataTableData(); const setDataTableData = useSetObjectDataTableData();
const { objects } = useFindManyObjects({ const { objects } = useFindManyObjects({
objectNamePlural: objectNamePlural, objectNamePlural,
}); });
useEffect(() => { useEffect(() => {
const entities = objects ?? []; const entities = objects ?? [];
setDataTableData(entities); setDataTableData(entities);
}, [objects, objectNameSingular, setDataTableData]); }, [objects, setDataTableData]);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext); const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext);

View File

@ -1,27 +1,43 @@
import { suppliersAvailableColumnDefinitions } from '@/companies/constants/companiesAvailableColumnDefinitions'; import { suppliersAvailableColumnDefinitions } from '@/companies/constants/companiesAvailableColumnDefinitions';
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
import { DataTable } from '@/ui/data/data-table/components/DataTable'; import { DataTable } from '@/ui/data/data-table/components/DataTable';
import { TableContext } from '@/ui/data/data-table/contexts/TableContext'; import { TableContext } from '@/ui/data/data-table/contexts/TableContext';
import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext'; import { ViewBarContext } from '@/ui/data/view-bar/contexts/ViewBarContext';
import { useTableViews } from '@/views/hooks/useTableViews'; import { useTableViews } from '@/views/hooks/useTableViews';
import { useUpdateOneObject } from '../hooks/useUpdateOneObject';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { ObjectDataTableEffect } from './ObjectDataTableEffect'; import { ObjectDataTableEffect } from './ObjectDataTableEffect';
export const ObjectTable = ({ export type ObjectTableProps = MetadataObjectIdentifier;
objectNamePlural,
objectNameSingular, export const ObjectTable = ({ objectNamePlural }: ObjectTableProps) => {
}: {
objectNameSingular: string;
objectNamePlural: string;
}) => {
const { createView, deleteView, submitCurrentView, updateView } = const { createView, deleteView, submitCurrentView, updateView } =
useTableViews({ useTableViews({
objectId: 'company', objectId: 'company',
columnDefinitions: suppliersAvailableColumnDefinitions, columnDefinitions: suppliersAvailableColumnDefinitions,
}); });
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport(); const { updateOneObject } = useUpdateOneObject({
objectNamePlural,
});
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
updateOneObject?.({
idToUpdate: variables.where.id,
input: variables.data,
});
};
return ( return (
<TableContext.Provider <TableContext.Provider
@ -31,26 +47,18 @@ export const ObjectTable = ({
}, },
}} }}
> >
<ObjectDataTableEffect <ObjectDataTableEffect objectNamePlural={objectNamePlural} />
objectNamePlural={objectNamePlural}
objectNameSingular={objectNameSingular}
/>
<ViewBarContext.Provider <ViewBarContext.Provider
value={{ value={{
defaultViewName: '???', defaultViewName: `All ${objectNamePlural}`,
onCurrentViewSubmit: submitCurrentView, onCurrentViewSubmit: submitCurrentView,
onViewCreate: createView, onViewCreate: createView,
onViewEdit: updateView, onViewEdit: updateView,
onViewRemove: deleteView, onViewRemove: deleteView,
onImport: openCompanySpreadsheetImport,
ViewBarRecoilScopeContext: TableRecoilScopeContext, ViewBarRecoilScopeContext: TableRecoilScopeContext,
}} }}
> >
<DataTable <DataTable updateEntityMutation={updateEntity} />
updateEntityMutation={() => {
//
}}
/>
</ViewBarContext.Provider> </ViewBarContext.Provider>
</TableContext.Provider> </TableContext.Provider>
); );

View File

@ -1,19 +1,17 @@
import { gql, useMutation } from '@apollo/client'; import { gql, useMutation } from '@apollo/client';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { generateCreateOneObjectMutation } from '../utils/generateCreateOneObjectMutation'; import { generateCreateOneObjectMutation } from '../utils/generateCreateOneObjectMutation';
import { useFindManyMetadataObjects } from './useFindManyMetadataObjects'; import { useFindOneMetadataObject } from './useFindOneMetadataObject';
export const useCreateOneObject = ({ export const useCreateOneObject = ({
objectNamePlural, objectNamePlural,
}: { }: MetadataObjectIdentifier) => {
objectNamePlural: string; const { foundMetadataObject, objectNotFoundInMetadata } =
}) => { useFindOneMetadataObject({
const { metadataObjects } = useFindManyMetadataObjects(); objectNamePlural,
});
const foundMetadataObject = metadataObjects.find(
(object) => object.namePlural === objectNamePlural,
);
const generatedMutation = foundMetadataObject const generatedMutation = foundMetadataObject
? generateCreateOneObjectMutation({ ? generateCreateOneObjectMutation({
@ -25,6 +23,7 @@ export const useCreateOneObject = ({
} }
`; `;
// TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(generatedMutation); const [mutate] = useMutation(generatedMutation);
const createOneObject = foundMetadataObject const createOneObject = foundMetadataObject
@ -39,9 +38,6 @@ export const useCreateOneObject = ({
} }
: undefined; : undefined;
const objectNotFoundInMetadata =
metadataObjects.length > 0 && !foundMetadataObject;
return { return {
createOneObject, createOneObject,
objectNotFoundInMetadata, objectNotFoundInMetadata,

View File

@ -1,11 +1,12 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { gql, useQuery } from '@apollo/client'; import { gql, useQuery } from '@apollo/client';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { PaginatedObjectType } from '../types/PaginatedObjectType'; import { PaginatedObjectType } from '../types/PaginatedObjectType';
import { formatPagedObjectsToObjects } from '../utils/formatPagedObjectsToObjects'; import { formatPagedObjectsToObjects } from '../utils/formatPagedObjectsToObjects';
import { generateFindManyCustomObjectsQuery } from '../utils/generateFindManyCustomObjectsQuery'; import { generateFindManyCustomObjectsQuery } from '../utils/generateFindManyCustomObjectsQuery';
import { useFindManyMetadataObjects } from './useFindManyMetadataObjects'; import { useFindOneMetadataObject } from './useFindOneMetadataObject';
// TODO: test with a wrong name // TODO: test with a wrong name
// TODO: add zod to validate that we have at least id on each object // TODO: add zod to validate that we have at least id on each object
@ -13,14 +14,11 @@ export const useFindManyObjects = <
ObjectType extends { id: string } & Record<string, any>, ObjectType extends { id: string } & Record<string, any>,
>({ >({
objectNamePlural, objectNamePlural,
}: { }: MetadataObjectIdentifier) => {
objectNamePlural: string; const { foundMetadataObject, objectNotFoundInMetadata } =
}) => { useFindOneMetadataObject({
const { metadataObjects } = useFindManyMetadataObjects(); objectNamePlural,
});
const foundMetadataObject = metadataObjects.find(
(object) => object.namePlural === objectNamePlural,
);
const generatedQuery = foundMetadataObject const generatedQuery = foundMetadataObject
? generateFindManyCustomObjectsQuery({ ? generateFindManyCustomObjectsQuery({
@ -48,9 +46,6 @@ export const useFindManyObjects = <
[data, objectNamePlural], [data, objectNamePlural],
); );
const objectNotFoundInMetadata =
metadataObjects.length > 0 && !foundMetadataObject;
return { return {
objects, objects,
loading, loading,

View File

@ -0,0 +1,21 @@
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { useFindManyMetadataObjects } from './useFindManyMetadataObjects';
export const useFindOneMetadataObject = ({
objectNamePlural,
}: MetadataObjectIdentifier) => {
const { metadataObjects } = useFindManyMetadataObjects();
const foundMetadataObject = metadataObjects.find(
(object) => object.namePlural === objectNamePlural,
);
const objectNotFoundInMetadata =
metadataObjects.length > 0 && !foundMetadataObject;
return {
foundMetadataObject,
objectNotFoundInMetadata,
};
};

View File

@ -16,8 +16,7 @@ import { useFindManyMetadataObjects } from './useFindManyMetadataObjects';
export const useUpdateOneMetadataObject = () => { export const useUpdateOneMetadataObject = () => {
const apolloClientMetadata = useApolloMetadataClient(); const apolloClientMetadata = useApolloMetadataClient();
const { getMetadataObjectsFromCache: queryMetadataObjects } = const { getMetadataObjectsFromCache } = useFindManyMetadataObjects();
useFindManyMetadataObjects();
const [mutate] = useMutation< const [mutate] = useMutation<
UpdateOneMetadataObjectMutation, UpdateOneMetadataObjectMutation,
@ -38,7 +37,7 @@ export const useUpdateOneMetadataObject = () => {
> >
>; >;
}) => { }) => {
const metadataObjects = queryMetadataObjects(); const metadataObjects = getMetadataObjectsFromCache();
const foundMetadataObject = metadataObjects.find( const foundMetadataObject = metadataObjects.find(
(metadataObject) => metadataObject.id === idToUpdate, (metadataObject) => metadataObject.id === idToUpdate,

View File

@ -0,0 +1,52 @@
import { gql, useMutation } from '@apollo/client';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { generateUpdateOneObjectMutation } from '../utils/generateUpdateOneObjectMutation';
import { useFindOneMetadataObject } from './useFindOneMetadataObject';
export const useUpdateOneObject = ({
objectNamePlural,
}: MetadataObjectIdentifier) => {
const { foundMetadataObject, objectNotFoundInMetadata } =
useFindOneMetadataObject({
objectNamePlural,
});
const generatedMutation = foundMetadataObject
? generateUpdateOneObjectMutation({
metadataObject: foundMetadataObject,
})
: gql`
mutation EmptyMutation {
empty
}
`;
// TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(generatedMutation);
const updateOneObject = foundMetadataObject
? ({
idToUpdate,
input,
}: {
idToUpdate: string;
input: Record<string, any>;
}) => {
return mutate({
variables: {
idToUpdate: idToUpdate,
input: {
...input,
},
},
});
}
: undefined;
return {
updateOneObject,
objectNotFoundInMetadata,
};
};

View File

@ -0,0 +1,3 @@
export type MetadataObjectIdentifier = {
objectNamePlural: string;
};

View File

@ -0,0 +1,21 @@
import { gql } from '@apollo/client';
import { capitalize } from '~/utils/string/capitalize';
import { MetadataObject } from '../types/MetadataObject';
export const generateUpdateOneObjectMutation = ({
metadataObject,
}: {
metadataObject: MetadataObject;
}) => {
const capitalizedObjectName = capitalize(metadataObject.nameSingular);
return gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
updateOne${capitalizedObjectName}(id: $idToUpdate, data: $input) {
id
}
}
`;
};

View File

@ -17,7 +17,7 @@ export enum AppPath {
PersonShowPage = '/person/:personId', PersonShowPage = '/person/:personId',
TasksPage = '/tasks', TasksPage = '/tasks',
OpportunitiesPage = '/opportunities', OpportunitiesPage = '/opportunities',
ObjectTablePage = '/:objectName', ObjectTablePage = '/objects/:objectNamePlural',
SettingsCatchAll = `/settings/*`, SettingsCatchAll = `/settings/*`,

View File

@ -1,6 +1,8 @@
import { useParams } from 'react-router-dom';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ObjectTable } from '@/metadata/components/ObjectTable'; import { ObjectTable } from '@/metadata/components/ObjectTable';
import { MetadataObjectIdentifier } from '@/metadata/types/MetadataObjectIdentifier';
import { DataTableActionBar } from '@/ui/data/data-table/action-bar/components/DataTableActionBar'; import { DataTableActionBar } from '@/ui/data/data-table/action-bar/components/DataTableActionBar';
import { DataTableContextMenu } from '@/ui/data/data-table/context-menu/components/DataTableContextMenu'; import { DataTableContextMenu } from '@/ui/data/data-table/context-menu/components/DataTableContextMenu';
import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
@ -17,13 +19,11 @@ const StyledTableContainer = styled.div`
width: 100%; width: 100%;
`; `;
export const ObjectTablePage = ({ export type ObjectTablePageProps = MetadataObjectIdentifier;
objectNamePlural,
objectNameSingular, export const ObjectTablePage = () => {
}: { const objectNamePlural = useParams().objectNamePlural ?? '';
objectNameSingular: string;
objectNamePlural: string;
}) => {
const handleAddButtonClick = async () => { const handleAddButtonClick = async () => {
// //
}; };
@ -40,10 +40,7 @@ export const ObjectTablePage = ({
CustomRecoilScopeContext={TableRecoilScopeContext} CustomRecoilScopeContext={TableRecoilScopeContext}
> >
<StyledTableContainer> <StyledTableContainer>
<ObjectTable <ObjectTable objectNamePlural={objectNamePlural} />
objectNamePlural={objectNamePlural}
objectNameSingular={objectNameSingular}
/>
</StyledTableContainer> </StyledTableContainer>
<DataTableActionBar /> <DataTableActionBar />
<DataTableContextMenu /> <DataTableContextMenu />