Split components into object-metadata and object-record (#2425)
* Split components into object-metadata and object-record * Fix seed
This commit is contained in:
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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],
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user