Split components into object-metadata and object-record (#2425)

* Split components into object-metadata and object-record

* Fix seed
This commit is contained in:
Charles Bochet
2023-11-10 15:54:32 +01:00
committed by GitHub
parent 04c618284f
commit 54d7acd518
93 changed files with 209 additions and 266 deletions

View File

@ -0,0 +1,164 @@
import { useParams } from 'react-router-dom';
import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil';
import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { IconBuildingSkyscraper } from '@/ui/display/icon';
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
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 { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer';
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 { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState';
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 { useFindOneObjectRecord } from '../hooks/useFindOneObjectRecord';
import { useUpdateOneObjectRecord } from '../hooks/useUpdateOneObjectRecord';
export const RecordShowPage = () => {
const { objectNameSingular, objectMetadataId } = useParams<{
objectNameSingular: string;
objectMetadataId: string;
}>();
const { icons } = useLazyLoadIcons();
const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({
objectNameSingular,
});
const [, setEntityFields] = useRecoilState(
entityFieldsFamilyState(objectMetadataId ?? ''),
);
const { object } = useFindOneObjectRecord({
objectMetadataId: objectMetadataId,
objectNameSingular,
onCompleted: (data) => {
setEntityFields(data);
},
});
const useUpdateOneObjectMutation: () => [(params: any) => any, any] = () => {
const { updateOneObject } = useUpdateOneObjectRecord({
objectNameSingular,
});
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
updateOneObject?.({
idToUpdate: variables.where.id,
input: variables.data,
});
};
return [updateEntity, { loading: false }];
};
const handleFavoriteButtonClick = async () => {
//
};
if (!object) return <></>;
return (
<PageContainer>
<PageTitle title={object.name || 'No Name'} />
<PageHeader
title={object.name ?? ''}
hasBackButton
Icon={IconBuildingSkyscraper}
>
<PageFavoriteButton
isFavorite={false}
onClick={handleFavoriteButtonClick}
/>
<ShowPageAddButton
key="add"
entity={{
id: object.id,
type: ActivityTargetableEntityType.Company,
}}
/>
</PageHeader>
<PageBody>
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
<ShowPageContainer>
<ShowPageLeftContainer>
<ShowPageSummaryCard
id={object.id}
logoOrAvatar={''}
title={object.name ?? 'No name'}
date={object.createdAt ?? ''}
renderTitleEditComponent={() => <></>}
avatarType="squared"
/>
<PropertyBox extraPadding={true}>
{foundObjectMetadataItem &&
[...foundObjectMetadataItem.fields]
.sort((a, b) =>
DateTime.fromISO(a.createdAt)
.diff(DateTime.fromISO(b.createdAt))
.toMillis(),
)
.map((metadataField, index) => {
return (
<FieldContext.Provider
key={object.id + metadataField.id}
value={{
entityId: object.id,
recoilScopeId: object.id + metadataField.id,
fieldDefinition:
formatFieldMetadataItemAsColumnDefinition({
field: metadataField,
position: index,
objectMetadataItem: foundObjectMetadataItem,
icons,
}),
useUpdateEntityMutation: useUpdateOneObjectMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell />
</FieldContext.Provider>
);
})}
</PropertyBox>
</ShowPageLeftContainer>
<ShowPageRightContainer
entity={{
id: object.id,
type: ActivityTargetableEntityType.Company,
}}
timeline
tasks
notes
emails
/>
</ShowPageContainer>
</RecoilScope>
</PageBody>
</PageContainer>
);
};

View File

@ -0,0 +1,107 @@
import styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { RecordTable } from '@/ui/object/record-table/components/RecordTable';
import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { TableOptionsDropdown } from '@/ui/object/record-table/options/components/TableOptionsDropdown';
import { RecordTableScope } from '@/ui/object/record-table/scopes/RecordTableScope';
import { ViewBar } from '@/views/components/ViewBar';
import { useViewFields } from '@/views/hooks/internal/useViewFields';
import { useView } from '@/views/hooks/useView';
import { ViewScope } from '@/views/scopes/ViewScope';
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useUpdateOneObjectRecord } from '../hooks/useUpdateOneObjectRecord';
import { RecordTableEffect } from './RecordTableEffect';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
`;
export const RecordTableContainer = ({
objectNamePlural,
}: {
objectNamePlural: string;
}) => {
const { columnDefinitions, foundObjectMetadataItem } =
useFindOneObjectMetadataItem({
objectNamePlural,
});
const { updateOneObject } = useUpdateOneObjectRecord({
objectNamePlural,
objectNameSingular: foundObjectMetadataItem?.nameSingular,
});
const tableScopeId = objectNamePlural ?? '';
const viewScopeId = objectNamePlural ?? '';
const { persistViewFields } = useViewFields(viewScopeId);
const { setTableFilters, setTableSorts, setTableColumns } = useRecordTable({
recordTableScopeId: tableScopeId,
});
const { setEntityCountInCurrentView } = useView({ viewScopeId });
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
updateOneObject?.({
idToUpdate: variables.where.id,
input: variables.data,
});
};
return (
<ViewScope
viewScopeId={viewScopeId}
onViewFieldsChange={(viewFields) => {
setTableColumns(
mapViewFieldsToColumnDefinitions(viewFields, columnDefinitions),
);
}}
onViewFiltersChange={(viewFilters) => {
setTableFilters(mapViewFiltersToFilters(viewFilters));
}}
onViewSortsChange={(viewSorts) => {
setTableSorts(mapViewSortsToSorts(viewSorts));
}}
>
<StyledContainer>
<RecordTableScope
recordTableScopeId={tableScopeId}
onColumnsChange={useRecoilCallback(() => (columns) => {
persistViewFields(mapColumnDefinitionsToViewFields(columns));
})}
onEntityCountChange={(entityCount) => {
setEntityCountInCurrentView(entityCount);
}}
>
<ViewBar
optionsDropdownButton={<TableOptionsDropdown />}
optionsDropdownScopeId={TableOptionsDropdownId}
/>
<RecordTableEffect />
<RecordTable updateEntityMutation={updateEntity} />
</RecordTableScope>
</StyledContainer>
</ViewScope>
);
};

View File

@ -0,0 +1,57 @@
import { useEffect } from 'react';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { useView } from '@/views/hooks/useView';
import { ViewType } from '@/views/types/ViewType';
export const RecordTableEffect = () => {
const { scopeId: objectNamePlural, setAvailableTableColumns } =
useRecordTable();
const {
columnDefinitions,
filterDefinitions,
sortDefinitions,
foundObjectMetadataItem,
} = useFindOneObjectMetadataItem({
objectNamePlural,
});
const {
setAvailableSortDefinitions,
setAvailableFilterDefinitions,
setAvailableFieldDefinitions,
setViewType,
setViewObjectMetadataId,
} = useView();
useRecordTable();
useEffect(() => {
if (!foundObjectMetadataItem) {
return;
}
setViewObjectMetadataId?.(foundObjectMetadataItem.id);
setViewType?.(ViewType.Table);
setAvailableSortDefinitions?.(sortDefinitions);
setAvailableFilterDefinitions?.(filterDefinitions);
setAvailableFieldDefinitions?.(columnDefinitions);
setAvailableTableColumns(columnDefinitions);
}, [
setViewObjectMetadataId,
setViewType,
columnDefinitions,
setAvailableSortDefinitions,
setAvailableFilterDefinitions,
setAvailableFieldDefinitions,
foundObjectMetadataItem,
sortDefinitions,
filterDefinitions,
setAvailableTableColumns,
]);
return <></>;
};

View File

@ -0,0 +1,68 @@
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
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 { useCreateOneObjectRecord } from '../hooks/useCreateOneObjectRecord';
import { RecordTableContainer } from './RecordTableContainer';
const StyledTableContainer = styled.div`
display: flex;
width: 100%;
`;
export type RecordTablePageProps = Pick<
ObjectMetadataItemIdentifier,
'objectNamePlural'
>;
export const RecordTablePage = () => {
const objectNamePlural = useParams().objectNamePlural ?? '';
const { objectNotFoundInMetadata, loading } = useFindOneObjectMetadataItem({
objectNamePlural,
});
const navigate = useNavigate();
useEffect(() => {
if (!loading && objectNotFoundInMetadata) {
navigate('/');
}
}, [objectNotFoundInMetadata, loading, navigate]);
const { createOneObject } = useCreateOneObjectRecord({
objectNamePlural,
});
const handleAddButtonClick = async () => {
createOneObject?.({});
};
return (
<PageContainer>
<PageHeader title="Objects" Icon={IconBuildingSkyscraper}>
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
<PageAddButton onClick={handleAddButtonClick} />
</PageHeader>
<PageBody>
<StyledTableContainer>
<RecordTableContainer objectNamePlural={objectNamePlural} />
</StyledTableContainer>
<RecordTableActionBar />
<RecordTableContextMenu />
</PageBody>
</PageContainer>
);
};

View File

@ -0,0 +1,33 @@
import { produce } from 'immer';
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { PaginatedObjectTypeResults } from '@/object-record/types/PaginatedObjectTypeResults';
import { capitalize } from '~/utils/string/capitalize';
export const getRecordOptimisticEffectDefinition = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) =>
({
key: `record-create-optimistic-effect-definition-${objectMetadataItem.nameSingular}`,
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
resolver: ({
currentData,
newData,
}: {
currentData: unknown;
newData: unknown;
}) => {
const newRecordPaginatedCacheField = produce<
PaginatedObjectTypeResults<any>
>(currentData as PaginatedObjectTypeResults<any>, (draft) => {
draft.edges.unshift({ node: newData, cursor: '' });
});
return newRecordPaginatedCacheField;
},
isUsingFlexibleBackend: true,
objectMetadataItem,
} satisfies OptimisticEffectDefinition);

View File

@ -0,0 +1,69 @@
import { useMutation } from '@apollo/client';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { Currency, FieldMetadataType } from '~/generated-metadata/graphql';
import { capitalize } from '~/utils/string/capitalize';
const defaultFieldValues: Record<FieldMetadataType, unknown> = {
[FieldMetadataType.Money]: { amount: null, currency: Currency.Usd },
[FieldMetadataType.Boolean]: false,
[FieldMetadataType.Date]: null,
[FieldMetadataType.Email]: '',
[FieldMetadataType.Enum]: null,
[FieldMetadataType.Number]: null,
[FieldMetadataType.Relation]: null,
[FieldMetadataType.Phone]: '',
[FieldMetadataType.Text]: '',
[FieldMetadataType.Url]: { link: '', text: '' },
[FieldMetadataType.Uuid]: '',
};
export const useCreateOneObjectRecord = ({
objectNamePlural,
}: Pick<ObjectMetadataItemIdentifier, 'objectNamePlural'>) => {
const { triggerOptimisticEffects } = useOptimisticEffect();
const {
foundObjectMetadataItem,
objectNotFoundInMetadata,
createOneMutation,
} = useFindOneObjectMetadataItem({
objectNamePlural,
});
// TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(createOneMutation);
const createOneObject = foundObjectMetadataItem
? async (input: Record<string, any>) => {
const createdObject = await mutate({
variables: {
input: {
...foundObjectMetadataItem.fields.reduce(
(result, field) => ({
...result,
[field.name]: defaultFieldValues[field.type],
}),
{},
),
...input,
},
},
});
triggerOptimisticEffects(
`${capitalize(foundObjectMetadataItem.nameSingular)}Edge`,
createdObject.data[
`create${capitalize(foundObjectMetadataItem.nameSingular)}`
],
);
}
: undefined;
return {
createOneObject,
objectNotFoundInMetadata,
};
};

View File

@ -0,0 +1,39 @@
import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
export const useDeleteOneObjectRecord = ({
objectNamePlural,
}: Pick<ObjectMetadataItemIdentifier, 'objectNamePlural'>) => {
const {
foundObjectMetadataItem,
objectNotFoundInMetadata,
findManyQuery,
deleteOneMutation,
} = useFindOneObjectMetadataItem({
objectNamePlural,
});
// TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(deleteOneMutation);
const deleteOneObject = foundObjectMetadataItem
? (input: Record<string, any>) => {
return mutate({
variables: {
input: {
...input,
},
},
refetchQueries: [getOperationName(findManyQuery) ?? ''],
});
}
: undefined;
return {
deleteOneObject,
objectNotFoundInMetadata,
};
};

View File

@ -0,0 +1,181 @@
import { useCallback, useMemo } from 'react';
import { useQuery } from '@apollo/client';
import { isNonEmptyArray } from '@apollo/client/utilities';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState } from 'recoil';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
import { logError } from '~/utils/logError';
import { capitalize } from '~/utils/string/capitalize';
import { cursorFamilyState } from '../states/cursorFamilyState';
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
import { isFetchingMoreObjectsFamilyState } from '../states/isFetchingMoreObjectsFamilyState';
import { PaginatedObjectType } from '../types/PaginatedObjectType';
import {
PaginatedObjectTypeEdge,
PaginatedObjectTypeResults,
} from '../types/PaginatedObjectTypeResults';
import { formatPagedObjectsToObjects } from '../utils/formatPagedObjectsToObjects';
// TODO: test with a wrong name
// TODO: add zod to validate that we have at least id on each object
export const useFindManyObjectRecords = <
ObjectType extends { id: string } & Record<string, any>,
>({
objectNamePlural,
filter,
orderBy,
onCompleted,
skip,
}: Pick<ObjectMetadataItemIdentifier, 'objectNamePlural'> & {
filter?: any;
orderBy?: any;
onCompleted?: (data: PaginatedObjectTypeResults<ObjectType>) => void;
skip?: boolean;
}) => {
const [lastCursor, setLastCursor] = useRecoilState(
cursorFamilyState(objectNamePlural),
);
const [hasNextPage, setHasNextPage] = useRecoilState(
hasNextPageFamilyState(objectNamePlural),
);
const [, setIsFetchingMoreObjects] = useRecoilState(
isFetchingMoreObjectsFamilyState(objectNamePlural),
);
const { foundObjectMetadataItem, objectNotFoundInMetadata, findManyQuery } =
useFindOneObjectMetadataItem({
objectNamePlural,
skip,
});
const { enqueueSnackBar } = useSnackBar();
const { data, loading, error, fetchMore } = useQuery<
PaginatedObjectType<ObjectType>
>(findManyQuery, {
skip: skip || !foundObjectMetadataItem || !objectNamePlural,
variables: {
filter: filter ?? {},
orderBy: orderBy ?? {},
},
onCompleted: (data) => {
if (objectNamePlural) {
onCompleted?.(data[objectNamePlural]);
if (objectNamePlural && data?.[objectNamePlural]) {
setLastCursor(data?.[objectNamePlural]?.pageInfo.endCursor);
setHasNextPage(data?.[objectNamePlural]?.pageInfo.hasNextPage);
}
}
},
onError: (error) => {
logError(
`useFindManyObjectRecords for "${objectNamePlural}" error : ` + error,
);
enqueueSnackBar(
`Error during useFindManyObjectRecords for "${objectNamePlural}", ${error.message}`,
{
variant: 'error',
},
);
},
});
const fetchMoreObjects = useCallback(async () => {
if (objectNamePlural && hasNextPage) {
setIsFetchingMoreObjects(true);
try {
await fetchMore({
variables: {
filter: filter ?? {},
orderBy: orderBy ?? {},
lastCursor: isNonEmptyString(lastCursor) ? lastCursor : undefined,
},
updateQuery: (prev, { fetchMoreResult }) => {
const uniqueByCursor = (
a: PaginatedObjectTypeEdge<ObjectType>[],
) => {
const seenCursors = new Set();
return a.filter((item) => {
const currentCursor = item.cursor;
return seenCursors.has(currentCursor)
? false
: seenCursors.add(currentCursor);
});
};
const previousEdges = prev?.[objectNamePlural]?.edges;
const nextEdges = fetchMoreResult?.[objectNamePlural]?.edges;
let newEdges: any[] = [];
if (isNonEmptyArray(previousEdges) && isNonEmptyArray(nextEdges)) {
newEdges = uniqueByCursor([
...prev?.[objectNamePlural]?.edges,
...fetchMoreResult?.[objectNamePlural]?.edges,
]);
}
return Object.assign({}, prev, {
[objectNamePlural]: {
__typename: `${capitalize(
foundObjectMetadataItem?.nameSingular ?? '',
)}Connection`,
edges: newEdges,
pageInfo: fetchMoreResult?.[objectNamePlural].pageInfo,
},
} as PaginatedObjectType<ObjectType>);
},
});
} catch (error) {
logError(`fetchMoreObjects for "${objectNamePlural}" error : ` + error);
enqueueSnackBar(
`Error during fetchMoreObjects for "${objectNamePlural}", ${error}`,
{
variant: 'error',
},
);
} finally {
setIsFetchingMoreObjects(false);
}
}
}, [
objectNamePlural,
lastCursor,
fetchMore,
filter,
orderBy,
foundObjectMetadataItem,
hasNextPage,
setIsFetchingMoreObjects,
enqueueSnackBar,
]);
const objects = useMemo(
() =>
objectNamePlural
? formatPagedObjectsToObjects({
pagedObjects: data,
objectNamePlural,
})
: [],
[data, objectNamePlural],
);
return {
objects,
loading,
error,
objectNotFoundInMetadata,
fetchMoreObjects,
};
};

View File

@ -0,0 +1,45 @@
import { useQuery } from '@apollo/client';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
export const useFindOneObjectRecord = <
ObjectType extends { id: string } & Record<string, any>,
>({
objectNameSingular,
objectMetadataId,
onCompleted,
}: Pick<ObjectMetadataItemIdentifier, 'objectNameSingular'> & {
objectMetadataId: string | undefined;
onCompleted?: (data: ObjectType) => void;
}) => {
const { foundObjectMetadataItem, objectNotFoundInMetadata, findOneQuery } =
useFindOneObjectMetadataItem({
objectNameSingular,
});
const { data, loading, error } = useQuery<
{ [nameSingular: string]: ObjectType },
{ objectMetadataId: string }
>(findOneQuery, {
skip: !foundObjectMetadataItem || !objectMetadataId,
variables: {
objectMetadataId: objectMetadataId ?? '',
},
onCompleted: (data) => {
if (onCompleted && objectNameSingular) {
onCompleted(data[objectNameSingular]);
}
},
});
const object =
objectNameSingular && data ? data[objectNameSingular] : undefined;
return {
object,
loading,
error,
objectNotFoundInMetadata,
};
};

View File

@ -0,0 +1,65 @@
import { useRecoilValue } from 'recoil';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { turnFiltersIntoWhereClauseV2 } from '@/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2';
import { turnSortsIntoOrderByV2 } from '@/ui/object/object-sort-dropdown/utils/turnSortsIntoOrderByV2';
import { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/internal/useRecordTableScopedStates';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { getRecordOptimisticEffectDefinition } from '../graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition';
import { useFindManyObjectRecords } from './useFindManyObjectRecords';
export const useObjectRecordTable = () => {
const { scopeId: objectNamePlural } = useRecordTable();
const { registerOptimisticEffect } = useOptimisticEffect();
const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({
objectNamePlural,
});
const { setRecordTableData } = useRecordTable();
const { tableFiltersState, tableSortsState } = useRecordTableScopedStates();
const tableFilters = useRecoilValue(tableFiltersState);
const tableSorts = useRecoilValue(tableSortsState);
const filter = turnFiltersIntoWhereClauseV2(
tableFilters,
foundObjectMetadataItem?.fields ?? [],
);
const orderBy = turnSortsIntoOrderByV2(
tableSorts,
foundObjectMetadataItem?.fields ?? [],
);
const { objects, loading, fetchMoreObjects } = useFindManyObjectRecords({
objectNamePlural,
filter,
orderBy,
onCompleted: (data) => {
const entities = data.edges.map((edge) => edge.node) ?? [];
setRecordTableData(entities);
if (foundObjectMetadataItem) {
registerOptimisticEffect({
variables: { orderBy, filter },
definition: getRecordOptimisticEffectDefinition({
objectMetadataItem: foundObjectMetadataItem,
}),
});
}
},
});
return {
objects,
loading,
fetchMoreObjects,
};
};

View File

@ -0,0 +1,46 @@
import { useRecoilCallback } from 'recoil';
import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState';
import { numberOfTableRowsState } from '@/ui/object/record-table/states/numberOfTableRowsState';
import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState';
import { useView } from '@/views/hooks/useView';
export const useSetRecordTableData = () => {
const { resetTableRowSelection } = useRecordTable();
const { setEntityCountInCurrentView } = useView();
return useRecoilCallback(
({ set, snapshot }) =>
<T extends { id: string } & Record<string, any>>(newEntityArray: T[]) => {
for (const entity of newEntityArray) {
const currentEntity = snapshot
.getLoadable(entityFieldsFamilyState(entity.id))
.valueOrThrow();
if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) {
set(entityFieldsFamilyState(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(numberOfTableRowsState, entityIds.length);
setEntityCountInCurrentView(entityIds.length);
set(isFetchingRecordTableDataState, false);
},
[resetTableRowSelection, setEntityCountInCurrentView],
);
};

View File

@ -0,0 +1,45 @@
import { useMutation } from '@apollo/client';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
export const useUpdateOneObjectRecord = ({
objectNamePlural,
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
const {
foundObjectMetadataItem,
objectNotFoundInMetadata,
updateOneMutation,
} = useFindOneObjectMetadataItem({
objectNamePlural,
objectNameSingular,
});
// TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(updateOneMutation);
const updateOneObject = foundObjectMetadataItem
? ({
idToUpdate,
input,
}: {
idToUpdate: string;
input: Record<string, any>;
}) => {
return mutate({
variables: {
idToUpdate: idToUpdate,
input: {
...input,
},
},
});
}
: undefined;
return {
updateOneObject,
objectNotFoundInMetadata,
};
};

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const cursorFamilyState = atomFamily<string, string | undefined>({
key: 'cursorFamilyState',
default: '',
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const hasNextPageFamilyState = atomFamily<boolean, string | undefined>({
key: 'hasNextPageFamilyState',
default: false,
});

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const isFetchingMoreObjectsFamilyState = atomFamily<
boolean,
string | undefined
>({
key: 'isFetchingMoreObjectsFamilyState',
default: false,
});

View File

@ -0,0 +1,5 @@
import { PaginatedObjectTypeResults } from './PaginatedObjectTypeResults';
export type PaginatedObjectType<ObjectType extends { id: string }> = {
[objectNamePlural: string]: PaginatedObjectTypeResults<ObjectType>;
};

View File

@ -0,0 +1,14 @@
export type PaginatedObjectTypeEdge<ObjectType extends { id: string }> = {
node: ObjectType;
cursor: string;
};
export type PaginatedObjectTypeResults<ObjectType extends { id: string }> = {
__typename?: string;
edges: PaginatedObjectTypeEdge<ObjectType>[];
pageInfo: {
hasNextPage: boolean;
startCursor: string;
endCursor: string;
};
};

View File

@ -0,0 +1,24 @@
export const formatPagedObjectsToObjects = <
ObjectType extends { id: string } & Record<string, any>,
ObjectTypeQuery extends {
[objectNamePlural: string]: {
edges: ObjectEdge[];
};
},
ObjectEdge extends {
node: ObjectType;
},
>({
pagedObjects,
objectNamePlural,
}: {
pagedObjects: ObjectTypeQuery | undefined;
objectNamePlural: string;
}) => {
const formattedObjects: ObjectType[] =
pagedObjects?.[objectNamePlural].edges.map((objectEdge: ObjectEdge) => ({
...objectEdge.node,
})) ?? [];
return formattedObjects;
};

View File

@ -0,0 +1,24 @@
import { gql } from '@apollo/client';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { capitalize } from '~/utils/string/capitalize';
export const generateCreateOneObjectMutation = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
return gql`
mutation CreateOne${capitalizedObjectName}($input: ${capitalizedObjectName}CreateInput!) {
create${capitalizedObjectName}(data: $input) {
id
${objectMetadataItem.fields
.map(mapFieldMetadataToGraphQLQuery)
.join('\n')}
}
}
`;
};

View File

@ -0,0 +1,20 @@
import { gql } from '@apollo/client';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const generateDeleteOneObjectMutation = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
return gql`
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
delete${capitalizedObjectName}(id: $idToDelete) {
id
}
}
`;
};

View File

@ -0,0 +1,40 @@
import { gql } from '@apollo/client';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { capitalize } from '~/utils/string/capitalize';
export const generateFindManyCustomObjectsQuery = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
return gql`
query FindMany${capitalize(
objectMetadataItem.namePlural,
)}($filter: ${capitalize(
objectMetadataItem.nameSingular,
)}FilterInput, $orderBy: ${capitalize(
objectMetadataItem.nameSingular,
)}OrderByInput, $lastCursor: String) {
${
objectMetadataItem.namePlural
}(filter: $filter, orderBy: $orderBy, first: 30, after: $lastCursor){
edges {
node {
id
${objectMetadataItem.fields
.map(mapFieldMetadataToGraphQLQuery)
.join('\n')}
}
cursor
}
pageInfo {
hasNextPage
startCursor
endCursor
}
}
}
`;
};

View File

@ -0,0 +1,25 @@
import { gql } from '@apollo/client';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
export const generateFindOneCustomObjectQuery = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
return gql`
query FindOne${objectMetadataItem.nameSingular}($objectMetadataId: UUID!) {
${objectMetadataItem.nameSingular}(filter: {
id: {
eq: $objectMetadataId
}
}){
id
${objectMetadataItem.fields
.map(mapFieldMetadataToGraphQLQuery)
.join('\n')}
}
}
`;
};

View File

@ -0,0 +1,37 @@
import { gql } from '@apollo/client';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { capitalize } from '~/utils/string/capitalize';
export const getUpdateOneObjectMutationGraphQLField = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
return `update${capitalize(objectNameSingular)}`;
};
export const generateUpdateOneObjectMutation = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const graphQLFieldForUpdateOneObjectMutation =
getUpdateOneObjectMutationGraphQLField({
objectNameSingular: objectMetadataItem.nameSingular,
});
return gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
${graphQLFieldForUpdateOneObjectMutation}(id: $idToUpdate, data: $input) {
id
${objectMetadataItem.fields
.map(mapFieldMetadataToGraphQLQuery)
.join('\n')}
}
}
`;
};