Feat/pagination front (#2387)
* Finished renaming and scope * wip * WIP update * Ok * Cleaned * Finished infinite scroll * Clean * Fixed V1 tables * Fix post merge * Removed ScrollWrapper * Put back ScrollWrapper * Put back in the right place
This commit is contained in:
@ -45,6 +45,7 @@
|
|||||||
"react-helmet-async": "^1.3.0",
|
"react-helmet-async": "^1.3.0",
|
||||||
"react-hook-form": "^7.45.1",
|
"react-hook-form": "^7.45.1",
|
||||||
"react-hotkeys-hook": "^4.4.0",
|
"react-hotkeys-hook": "^4.4.0",
|
||||||
|
"react-intersection-observer": "^9.5.2",
|
||||||
"react-loading-skeleton": "^3.3.1",
|
"react-loading-skeleton": "^3.3.1",
|
||||||
"react-phone-number-input": "^3.3.4",
|
"react-phone-number-input": "^3.3.4",
|
||||||
"react-responsive": "^9.0.2",
|
"react-responsive": "^9.0.2",
|
||||||
|
|||||||
@ -4,9 +4,12 @@ import {
|
|||||||
OperationVariables,
|
OperationVariables,
|
||||||
useApolloClient,
|
useApolloClient,
|
||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
import { GET_COMPANIES } from '@/companies/graphql/queries/getCompanies';
|
import { GET_COMPANIES } from '@/companies/graphql/queries/getCompanies';
|
||||||
|
import { ObjectMetadataItem } from '@/metadata/types/ObjectMetadataItem';
|
||||||
|
import { generateFindManyCustomObjectsQuery } from '@/metadata/utils/generateFindManyCustomObjectsQuery';
|
||||||
import { GET_PEOPLE } from '@/people/graphql/queries/getPeople';
|
import { GET_PEOPLE } from '@/people/graphql/queries/getPeople';
|
||||||
import { GET_API_KEYS } from '@/settings/developers/graphql/queries/getApiKeys';
|
import { GET_API_KEYS } from '@/settings/developers/graphql/queries/getApiKeys';
|
||||||
import {
|
import {
|
||||||
@ -16,6 +19,7 @@ import {
|
|||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
import { optimisticEffectState } from '../states/optimisticEffectState';
|
import { optimisticEffectState } from '../states/optimisticEffectState';
|
||||||
|
import { OptimisticEffect } from '../types/internal/OptimisticEffect';
|
||||||
import { OptimisticEffectDefinition } from '../types/OptimisticEffectDefinition';
|
import { OptimisticEffectDefinition } from '../types/OptimisticEffectDefinition';
|
||||||
|
|
||||||
export const useOptimisticEffect = () => {
|
export const useOptimisticEffect = () => {
|
||||||
@ -28,7 +32,7 @@ export const useOptimisticEffect = () => {
|
|||||||
definition,
|
definition,
|
||||||
}: {
|
}: {
|
||||||
variables: OperationVariables;
|
variables: OperationVariables;
|
||||||
definition: OptimisticEffectDefinition<T>;
|
definition: OptimisticEffectDefinition;
|
||||||
}) => {
|
}) => {
|
||||||
const optimisticEffects = snapshot
|
const optimisticEffects = snapshot
|
||||||
.getLoadable(optimisticEffectState)
|
.getLoadable(optimisticEffectState)
|
||||||
@ -39,12 +43,47 @@ export const useOptimisticEffect = () => {
|
|||||||
newData,
|
newData,
|
||||||
query,
|
query,
|
||||||
variables,
|
variables,
|
||||||
|
isUsingFlexibleBackend,
|
||||||
|
objectMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
cache: ApolloCache<unknown>;
|
cache: ApolloCache<unknown>;
|
||||||
newData: unknown[];
|
newData: unknown;
|
||||||
variables: OperationVariables;
|
variables: OperationVariables;
|
||||||
query: DocumentNode;
|
query: DocumentNode;
|
||||||
|
isUsingFlexibleBackend?: boolean;
|
||||||
|
objectMetadataItem?: ObjectMetadataItem;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (isUsingFlexibleBackend && objectMetadataItem) {
|
||||||
|
const generatedQuery = generateFindManyCustomObjectsQuery({
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingData = cache.readQuery({
|
||||||
|
query: generatedQuery,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.writeQuery({
|
||||||
|
query: generatedQuery,
|
||||||
|
variables,
|
||||||
|
data: {
|
||||||
|
[objectMetadataItem.namePlural]: definition.resolver({
|
||||||
|
currentData: (existingData as any)?.[
|
||||||
|
objectMetadataItem.namePlural
|
||||||
|
],
|
||||||
|
newData,
|
||||||
|
variables,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const existingData = cache.readQuery({
|
const existingData = cache.readQuery({
|
||||||
query,
|
query,
|
||||||
variables,
|
variables,
|
||||||
@ -82,6 +121,7 @@ export const useOptimisticEffect = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query === GET_API_KEYS) {
|
if (query === GET_API_KEYS) {
|
||||||
cache.writeQuery({
|
cache.writeQuery({
|
||||||
query,
|
query,
|
||||||
@ -104,7 +144,9 @@ export const useOptimisticEffect = () => {
|
|||||||
typename: definition.typename,
|
typename: definition.typename,
|
||||||
query: definition.query,
|
query: definition.query,
|
||||||
writer: optimisticEffectWriter,
|
writer: optimisticEffectWriter,
|
||||||
};
|
objectMetadataItem: definition.objectMetadataItem,
|
||||||
|
isUsingFlexibleBackend: definition.isUsingFlexibleBackend,
|
||||||
|
} satisfies OptimisticEffect<T>;
|
||||||
|
|
||||||
set(optimisticEffectState, {
|
set(optimisticEffectState, {
|
||||||
...optimisticEffects,
|
...optimisticEffects,
|
||||||
@ -115,26 +157,31 @@ export const useOptimisticEffect = () => {
|
|||||||
|
|
||||||
const triggerOptimisticEffects = useRecoilCallback(
|
const triggerOptimisticEffects = useRecoilCallback(
|
||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
(typename: string, newData: any[]) => {
|
(typename: string, newData: unknown) => {
|
||||||
const optimisticEffects = snapshot
|
const optimisticEffects = snapshot
|
||||||
.getLoadable(optimisticEffectState)
|
.getLoadable(optimisticEffectState)
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
Object.values(optimisticEffects).forEach((optimisticEffect) => {
|
for (const optimisticEffect of Object.values(optimisticEffects)) {
|
||||||
// We need to update the typename when createObject type differs from listObject types
|
// We need to update the typename when createObject type differs from listObject types
|
||||||
// It is the case for apiKey, where the creation route returns an ApiKeyToken type
|
// It is the case for apiKey, where the creation route returns an ApiKeyToken type
|
||||||
const formattedNewData = newData.map((data) => {
|
const formattedNewData = isNonEmptyArray(newData)
|
||||||
return { ...data, __typename: typename };
|
? newData.map((data: any) => {
|
||||||
});
|
return { ...data, __typename: typename };
|
||||||
|
})
|
||||||
|
: newData;
|
||||||
|
|
||||||
if (optimisticEffect.typename === typename) {
|
if (optimisticEffect.typename === typename) {
|
||||||
optimisticEffect.writer({
|
optimisticEffect.writer({
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
query: optimisticEffect.query,
|
query: optimisticEffect.query ?? ({} as DocumentNode),
|
||||||
newData: formattedNewData,
|
newData: formattedNewData,
|
||||||
variables: optimisticEffect.variables,
|
variables: optimisticEffect.variables,
|
||||||
|
isUsingFlexibleBackend: optimisticEffect.isUsingFlexibleBackend,
|
||||||
|
objectMetadataItem: optimisticEffect.objectMetadataItem,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import { DocumentNode } from 'graphql';
|
import { DocumentNode } from 'graphql';
|
||||||
|
|
||||||
|
import { ObjectMetadataItem } from '@/metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
import { OptimisticEffectResolver } from './OptimisticEffectResolver';
|
import { OptimisticEffectResolver } from './OptimisticEffectResolver';
|
||||||
|
|
||||||
export type OptimisticEffectDefinition<T> = {
|
export type OptimisticEffectDefinition = {
|
||||||
key: string;
|
key: string;
|
||||||
query: DocumentNode;
|
query?: DocumentNode;
|
||||||
typename: string;
|
typename: string;
|
||||||
resolver: OptimisticEffectResolver<T>;
|
resolver: OptimisticEffectResolver;
|
||||||
|
objectMetadataItem?: ObjectMetadataItem;
|
||||||
|
isUsingFlexibleBackend?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { OperationVariables } from '@apollo/client';
|
import { OperationVariables } from '@apollo/client';
|
||||||
|
|
||||||
export type OptimisticEffectResolver<T> = ({
|
export type OptimisticEffectResolver = ({
|
||||||
currentData,
|
currentData,
|
||||||
newData,
|
newData,
|
||||||
variables,
|
variables,
|
||||||
}: {
|
}: {
|
||||||
currentData: T[];
|
currentData: any; //TODO: Change when decommissioning v1
|
||||||
newData: T[];
|
newData: any; //TODO: Change when decommissioning v1
|
||||||
variables: OperationVariables;
|
variables: OperationVariables;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { ApolloCache, DocumentNode, OperationVariables } from '@apollo/client';
|
import { ApolloCache, DocumentNode, OperationVariables } from '@apollo/client';
|
||||||
|
|
||||||
|
import { ObjectMetadataItem } from '@/metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
type OptimisticEffectWriter<T> = ({
|
type OptimisticEffectWriter<T> = ({
|
||||||
cache,
|
cache,
|
||||||
newData,
|
newData,
|
||||||
@ -8,14 +10,18 @@ type OptimisticEffectWriter<T> = ({
|
|||||||
}: {
|
}: {
|
||||||
cache: ApolloCache<T>;
|
cache: ApolloCache<T>;
|
||||||
query: DocumentNode;
|
query: DocumentNode;
|
||||||
newData: T[];
|
newData: T;
|
||||||
variables: OperationVariables;
|
variables: OperationVariables;
|
||||||
|
objectMetadataItem?: ObjectMetadataItem;
|
||||||
|
isUsingFlexibleBackend?: boolean;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
export type OptimisticEffect<T> = {
|
export type OptimisticEffect<T> = {
|
||||||
key: string;
|
key: string;
|
||||||
query: DocumentNode;
|
query?: DocumentNode;
|
||||||
typename: string;
|
typename: string;
|
||||||
variables: OperationVariables;
|
variables: OperationVariables;
|
||||||
writer: OptimisticEffectWriter<T>;
|
writer: OptimisticEffectWriter<T>;
|
||||||
|
objectMetadataItem?: ObjectMetadataItem;
|
||||||
|
isUsingFlexibleBackend?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { companiesAvailableFieldDefinitions } from '@/companies/constants/compan
|
|||||||
import { getCompaniesOptimisticEffectDefinition } from '@/companies/graphql/optimistic-effect-definitions/getCompaniesOptimisticEffectDefinition';
|
import { getCompaniesOptimisticEffectDefinition } from '@/companies/graphql/optimistic-effect-definitions/getCompaniesOptimisticEffectDefinition';
|
||||||
import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyTableContextMenuEntries';
|
import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyTableContextMenuEntries';
|
||||||
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
|
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
|
||||||
import { RecordTable } from '@/ui/object/record-table/components/RecordTable';
|
|
||||||
import { RecordTableEffect } from '@/ui/object/record-table/components/RecordTableEffect';
|
import { RecordTableEffect } from '@/ui/object/record-table/components/RecordTableEffect';
|
||||||
|
import { RecordTableV1 } from '@/ui/object/record-table/components/RecordTableV1';
|
||||||
import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId';
|
import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId';
|
||||||
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
|
||||||
import { TableOptionsDropdown } from '@/ui/object/record-table/options/components/TableOptionsDropdown';
|
import { TableOptionsDropdown } from '@/ui/object/record-table/options/components/TableOptionsDropdown';
|
||||||
@ -135,7 +135,7 @@ export const CompanyTable = () => {
|
|||||||
setContextMenuEntries={setContextMenuEntries}
|
setContextMenuEntries={setContextMenuEntries}
|
||||||
setActionBarEntries={setActionBarEntries}
|
setActionBarEntries={setActionBarEntries}
|
||||||
/>
|
/>
|
||||||
<RecordTable
|
<RecordTableV1
|
||||||
updateEntityMutation={({
|
updateEntityMutation={({
|
||||||
variables,
|
variables,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -32,12 +32,14 @@ export const RecordTableContainer = ({
|
|||||||
}: {
|
}: {
|
||||||
objectNamePlural: string;
|
objectNamePlural: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { columnDefinitions } = useFindOneObjectMetadataItem({
|
const { columnDefinitions, foundObjectMetadataItem } =
|
||||||
objectNamePlural,
|
useFindOneObjectMetadataItem({
|
||||||
});
|
objectNamePlural,
|
||||||
|
});
|
||||||
|
|
||||||
const { updateOneObject } = useUpdateOneObject({
|
const { updateOneObject } = useUpdateOneObject({
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
|
objectNameSingular: foundObjectMetadataItem?.nameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableScopeId = objectNamePlural ?? '';
|
const tableScopeId = objectNamePlural ?? '';
|
||||||
|
|||||||
@ -1,27 +1,25 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { turnFiltersIntoWhereClauseV2 } from '@/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2';
|
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
|
||||||
import { turnSortsIntoOrderByV2 } from '@/ui/object/object-sort-dropdown/utils/turnSortsIntoOrderByV2';
|
|
||||||
import { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/internal/useRecordTableScopedStates';
|
|
||||||
import { useView } from '@/views/hooks/useView';
|
import { useView } from '@/views/hooks/useView';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
|
|
||||||
import { useRecordTable } from '../../ui/object/record-table/hooks/useRecordTable';
|
|
||||||
import { useFindManyObjects } from '../hooks/useFindManyObjects';
|
|
||||||
import { useFindOneObjectMetadataItem } from '../hooks/useFindOneObjectMetadataItem';
|
import { useFindOneObjectMetadataItem } from '../hooks/useFindOneObjectMetadataItem';
|
||||||
|
import { useTableObjects } from '../hooks/useTableObjects';
|
||||||
|
|
||||||
export const RecordTableEffect = () => {
|
export const RecordTableEffect = () => {
|
||||||
const { scopeId } = useRecordTable();
|
const { scopeId: objectNamePlural, setAvailableTableColumns } =
|
||||||
|
useRecordTable();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
foundObjectMetadataItem,
|
|
||||||
columnDefinitions,
|
columnDefinitions,
|
||||||
filterDefinitions,
|
filterDefinitions,
|
||||||
sortDefinitions,
|
sortDefinitions,
|
||||||
|
foundObjectMetadataItem,
|
||||||
} = useFindOneObjectMetadataItem({
|
} = useFindOneObjectMetadataItem({
|
||||||
objectNamePlural: scopeId,
|
objectNamePlural,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setAvailableSortDefinitions,
|
setAvailableSortDefinitions,
|
||||||
setAvailableFilterDefinitions,
|
setAvailableFilterDefinitions,
|
||||||
@ -30,32 +28,7 @@ export const RecordTableEffect = () => {
|
|||||||
setViewObjectId,
|
setViewObjectId,
|
||||||
} = useView();
|
} = useView();
|
||||||
|
|
||||||
const { setRecordTableData, setAvailableTableColumns } = useRecordTable();
|
useTableObjects();
|
||||||
|
|
||||||
const { tableFiltersState, tableSortsState } = useRecordTableScopedStates();
|
|
||||||
|
|
||||||
const tableFilters = useRecoilValue(tableFiltersState);
|
|
||||||
const tableSorts = useRecoilValue(tableSortsState);
|
|
||||||
|
|
||||||
const { objects, loading } = useFindManyObjects({
|
|
||||||
objectNamePlural: scopeId,
|
|
||||||
filter: turnFiltersIntoWhereClauseV2(
|
|
||||||
tableFilters,
|
|
||||||
foundObjectMetadataItem?.fields ?? [],
|
|
||||||
),
|
|
||||||
orderBy: turnSortsIntoOrderByV2(
|
|
||||||
tableSorts,
|
|
||||||
foundObjectMetadataItem?.fields ?? [],
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loading) {
|
|
||||||
const entities = objects ?? [];
|
|
||||||
|
|
||||||
setRecordTableData(entities);
|
|
||||||
}
|
|
||||||
}, [objects, setRecordTableData, loading]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!foundObjectMetadataItem) {
|
if (!foundObjectMetadataItem) {
|
||||||
@ -70,7 +43,6 @@ export const RecordTableEffect = () => {
|
|||||||
|
|
||||||
setAvailableTableColumns(columnDefinitions);
|
setAvailableTableColumns(columnDefinitions);
|
||||||
}, [
|
}, [
|
||||||
setAvailableTableColumns,
|
|
||||||
setViewObjectId,
|
setViewObjectId,
|
||||||
setViewType,
|
setViewType,
|
||||||
columnDefinitions,
|
columnDefinitions,
|
||||||
@ -80,6 +52,7 @@ export const RecordTableEffect = () => {
|
|||||||
foundObjectMetadataItem,
|
foundObjectMetadataItem,
|
||||||
sortDefinitions,
|
sortDefinitions,
|
||||||
filterDefinitions,
|
filterDefinitions,
|
||||||
|
setAvailableTableColumns,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { produce } from 'immer';
|
||||||
|
|
||||||
|
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
|
||||||
|
import { ObjectMetadataItem } from '@/metadata/types/ObjectMetadataItem';
|
||||||
|
import { PaginatedObjectTypeResults } from '@/metadata/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);
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
|
||||||
|
|
||||||
|
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||||
import { Currency, FieldMetadataType } from '~/generated-metadata/graphql';
|
import { Currency, FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier';
|
import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier';
|
||||||
|
|
||||||
@ -23,10 +24,11 @@ const defaultFieldValues: Record<FieldMetadataType, unknown> = {
|
|||||||
export const useCreateOneObject = ({
|
export const useCreateOneObject = ({
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
}: Pick<ObjectMetadataItemIdentifier, 'objectNamePlural'>) => {
|
}: Pick<ObjectMetadataItemIdentifier, 'objectNamePlural'>) => {
|
||||||
|
const { triggerOptimisticEffects } = useOptimisticEffect();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
foundObjectMetadataItem,
|
foundObjectMetadataItem,
|
||||||
objectNotFoundInMetadata,
|
objectNotFoundInMetadata,
|
||||||
findManyQuery,
|
|
||||||
createOneMutation,
|
createOneMutation,
|
||||||
} = useFindOneObjectMetadataItem({
|
} = useFindOneObjectMetadataItem({
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
@ -36,8 +38,8 @@ export const useCreateOneObject = ({
|
|||||||
const [mutate] = useMutation(createOneMutation);
|
const [mutate] = useMutation(createOneMutation);
|
||||||
|
|
||||||
const createOneObject = foundObjectMetadataItem
|
const createOneObject = foundObjectMetadataItem
|
||||||
? (input: Record<string, unknown> = {}) => {
|
? async (input: Record<string, any>) => {
|
||||||
return mutate({
|
const createdObject = await mutate({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
...foundObjectMetadataItem.fields.reduce(
|
...foundObjectMetadataItem.fields.reduce(
|
||||||
@ -50,8 +52,14 @@ export const useCreateOneObject = ({
|
|||||||
...input,
|
...input,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
refetchQueries: [getOperationName(findManyQuery) ?? ''],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
triggerOptimisticEffects(
|
||||||
|
`${capitalize(foundObjectMetadataItem.nameSingular)}Edge`,
|
||||||
|
createdObject.data[
|
||||||
|
`create${capitalize(foundObjectMetadataItem.nameSingular)}`
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,22 @@
|
|||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { isNonEmptyArray } from '@apollo/client/utilities';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
|
||||||
import { logError } from '~/utils/logError';
|
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 { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier';
|
import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier';
|
||||||
import { PaginatedObjectType } from '../types/PaginatedObjectType';
|
import { PaginatedObjectType } from '../types/PaginatedObjectType';
|
||||||
import { PaginatedObjectTypeResults } from '../types/PaginatedObjectTypeResults';
|
import {
|
||||||
|
PaginatedObjectTypeEdge,
|
||||||
|
PaginatedObjectTypeResults,
|
||||||
|
} from '../types/PaginatedObjectTypeResults';
|
||||||
import { formatPagedObjectsToObjects } from '../utils/formatPagedObjectsToObjects';
|
import { formatPagedObjectsToObjects } from '../utils/formatPagedObjectsToObjects';
|
||||||
|
|
||||||
import { useFindOneObjectMetadataItem } from './useFindOneObjectMetadataItem';
|
import { useFindOneObjectMetadataItem } from './useFindOneObjectMetadataItem';
|
||||||
@ -27,6 +37,18 @@ export const useFindManyObjects = <
|
|||||||
onCompleted?: (data: PaginatedObjectTypeResults<ObjectType>) => void;
|
onCompleted?: (data: PaginatedObjectTypeResults<ObjectType>) => void;
|
||||||
skip?: boolean;
|
skip?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [lastCursor, setLastCursor] = useRecoilState(
|
||||||
|
cursorFamilyState(objectNamePlural),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [hasNextPage, setHasNextPage] = useRecoilState(
|
||||||
|
hasNextPageFamilyState(objectNamePlural),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [, setIsFetchingMoreObjects] = useRecoilState(
|
||||||
|
isFetchingMoreObjectsFamilyState(objectNamePlural),
|
||||||
|
);
|
||||||
|
|
||||||
const { foundObjectMetadataItem, objectNotFoundInMetadata, findManyQuery } =
|
const { foundObjectMetadataItem, objectNotFoundInMetadata, findManyQuery } =
|
||||||
useFindOneObjectMetadataItem({
|
useFindOneObjectMetadataItem({
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
@ -35,29 +57,107 @@ export const useFindManyObjects = <
|
|||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const { data, loading, error } = useQuery<PaginatedObjectType<ObjectType>>(
|
const { data, loading, error, fetchMore } = useQuery<
|
||||||
findManyQuery,
|
PaginatedObjectType<ObjectType>
|
||||||
{
|
>(findManyQuery, {
|
||||||
skip: skip || !foundObjectMetadataItem,
|
skip: skip || !foundObjectMetadataItem || !objectNamePlural,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter ?? {},
|
filter: filter ?? {},
|
||||||
orderBy: orderBy ?? {},
|
orderBy: orderBy ?? {},
|
||||||
},
|
},
|
||||||
onCompleted: (data) =>
|
onCompleted: (data) => {
|
||||||
objectNamePlural && onCompleted?.(data[objectNamePlural]),
|
if (objectNamePlural) {
|
||||||
onError: (error) => {
|
onCompleted?.(data[objectNamePlural]);
|
||||||
logError(
|
|
||||||
`useFindManyObjects for "${objectNamePlural}" error : ` + error,
|
if (objectNamePlural && data?.[objectNamePlural]) {
|
||||||
);
|
setLastCursor(data?.[objectNamePlural]?.pageInfo.endCursor);
|
||||||
|
setHasNextPage(data?.[objectNamePlural]?.pageInfo.hasNextPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
logError(`useFindManyObjects for "${objectNamePlural}" error : ` + error);
|
||||||
|
enqueueSnackBar(
|
||||||
|
`Error during useFindManyObjects 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(
|
enqueueSnackBar(
|
||||||
`Error during useFindManyObjects for "${objectNamePlural}", ${error.message}`,
|
`Error during fetchMoreObjects for "${objectNamePlural}", ${error}`,
|
||||||
{
|
{
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
} finally {
|
||||||
},
|
setIsFetchingMoreObjects(false);
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
objectNamePlural,
|
||||||
|
lastCursor,
|
||||||
|
fetchMore,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
foundObjectMetadataItem,
|
||||||
|
hasNextPage,
|
||||||
|
setIsFetchingMoreObjects,
|
||||||
|
enqueueSnackBar,
|
||||||
|
]);
|
||||||
|
|
||||||
const objects = useMemo(
|
const objects = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -75,5 +175,6 @@ export const useFindManyObjects = <
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
objectNotFoundInMetadata,
|
objectNotFoundInMetadata,
|
||||||
|
fetchMoreObjects,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export const useSetRecordTableData = () => {
|
|||||||
|
|
||||||
return useRecoilCallback(
|
return useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
<T extends { id: string }>(newEntityArray: T[]) => {
|
<T extends { id: string } & Record<string, any>>(newEntityArray: T[]) => {
|
||||||
for (const entity of newEntityArray) {
|
for (const entity of newEntityArray) {
|
||||||
const currentEntity = snapshot
|
const currentEntity = snapshot
|
||||||
.getLoadable(entityFieldsFamilyState(entity.id))
|
.getLoadable(entityFieldsFamilyState(entity.id))
|
||||||
|
|||||||
65
front/src/modules/metadata/hooks/useTableObjects.ts
Normal file
65
front/src/modules/metadata/hooks/useTableObjects.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||||
|
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 { useFindManyObjects } from './useFindManyObjects';
|
||||||
|
import { useFindOneObjectMetadataItem } from './useFindOneObjectMetadataItem';
|
||||||
|
|
||||||
|
export const useTableObjects = () => {
|
||||||
|
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 } = useFindManyObjects({
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
|
||||||
|
|
||||||
import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier';
|
import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier';
|
||||||
|
|
||||||
@ -12,7 +11,6 @@ export const useUpdateOneObject = ({
|
|||||||
const {
|
const {
|
||||||
foundObjectMetadataItem,
|
foundObjectMetadataItem,
|
||||||
objectNotFoundInMetadata,
|
objectNotFoundInMetadata,
|
||||||
findManyQuery,
|
|
||||||
updateOneMutation,
|
updateOneMutation,
|
||||||
} = useFindOneObjectMetadataItem({
|
} = useFindOneObjectMetadataItem({
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
@ -20,9 +18,7 @@ export const useUpdateOneObject = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: type this with a minimal type at least with Record<string, any>
|
// TODO: type this with a minimal type at least with Record<string, any>
|
||||||
const [mutate] = useMutation(updateOneMutation, {
|
const [mutate] = useMutation(updateOneMutation);
|
||||||
refetchQueries: [getOperationName(findManyQuery) ?? ''],
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateOneObject = foundObjectMetadataItem
|
const updateOneObject = foundObjectMetadataItem
|
||||||
? ({
|
? ({
|
||||||
|
|||||||
6
front/src/modules/metadata/states/cursorFamilyState.ts
Normal file
6
front/src/modules/metadata/states/cursorFamilyState.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { atomFamily } from 'recoil';
|
||||||
|
|
||||||
|
export const cursorFamilyState = atomFamily<string, string | undefined>({
|
||||||
|
key: 'cursorFamilyState',
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { atomFamily } from 'recoil';
|
||||||
|
|
||||||
|
export const fetchMoreObjectsFamilyState = atomFamily<
|
||||||
|
{ fetchMore: () => void },
|
||||||
|
string
|
||||||
|
>({
|
||||||
|
key: 'fetchMoreObjectsFamilyState',
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atomFamily } from 'recoil';
|
||||||
|
|
||||||
|
export const hasNextPageFamilyState = atomFamily<boolean, string | undefined>({
|
||||||
|
key: 'hasNextPageFamilyState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { atomFamily } from 'recoil';
|
||||||
|
|
||||||
|
export const isFetchingMoreObjectsFamilyState = atomFamily<
|
||||||
|
boolean,
|
||||||
|
string | undefined
|
||||||
|
>({
|
||||||
|
key: 'isFetchingMoreObjectsFamilyState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
@ -1,6 +1,14 @@
|
|||||||
export type PaginatedObjectTypeResults<ObjectType extends { id: string }> = {
|
export type PaginatedObjectTypeEdge<ObjectType extends { id: string }> = {
|
||||||
edges: {
|
node: ObjectType;
|
||||||
node: ObjectType;
|
cursor: string;
|
||||||
cursor: string;
|
};
|
||||||
}[];
|
|
||||||
|
export type PaginatedObjectTypeResults<ObjectType extends { id: string }> = {
|
||||||
|
__typename?: string;
|
||||||
|
edges: PaginatedObjectTypeEdge<ObjectType>[];
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: boolean;
|
||||||
|
startCursor: string;
|
||||||
|
endCursor: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { capitalize } from '~/utils/string/capitalize';
|
|||||||
|
|
||||||
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
import { mapFieldMetadataToGraphQLQuery } from './mapFieldMetadataToGraphQLQuery';
|
||||||
|
|
||||||
export const generateCreateOneObjectMutation = ({
|
export const generateCreateOneObjectMutation = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
@ -15,6 +17,9 @@ export const generateCreateOneObjectMutation = ({
|
|||||||
mutation CreateOne${capitalizedObjectName}($input: ${capitalizedObjectName}CreateInput!) {
|
mutation CreateOne${capitalizedObjectName}($input: ${capitalizedObjectName}CreateInput!) {
|
||||||
create${capitalizedObjectName}(data: $input) {
|
create${capitalizedObjectName}(data: $input) {
|
||||||
id
|
id
|
||||||
|
${objectMetadataItem.fields
|
||||||
|
.map(mapFieldMetadataToGraphQLQuery)
|
||||||
|
.join('\n')}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -8,18 +8,20 @@ import { mapFieldMetadataToGraphQLQuery } from './mapFieldMetadataToGraphQLQuery
|
|||||||
|
|
||||||
export const generateFindManyCustomObjectsQuery = ({
|
export const generateFindManyCustomObjectsQuery = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
_fromCursor,
|
|
||||||
}: {
|
}: {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
_fromCursor?: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
return gql`
|
return gql`
|
||||||
query FindMany${objectMetadataItem.namePlural}($filter: ${capitalize(
|
query FindMany${capitalize(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
)}($filter: ${capitalize(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
)}FilterInput, $orderBy: ${capitalize(
|
)}FilterInput, $orderBy: ${capitalize(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
)}OrderByInput) {
|
)}OrderByInput, $lastCursor: String) {
|
||||||
${objectMetadataItem.namePlural}(filter: $filter, orderBy: $orderBy){
|
${
|
||||||
|
objectMetadataItem.namePlural
|
||||||
|
}(filter: $filter, orderBy: $orderBy, first: 30, after: $lastCursor){
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
@ -29,6 +31,11 @@ export const generateFindManyCustomObjectsQuery = ({
|
|||||||
}
|
}
|
||||||
cursor
|
cursor
|
||||||
}
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -4,6 +4,16 @@ import { capitalize } from '~/utils/string/capitalize';
|
|||||||
|
|
||||||
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
import { mapFieldMetadataToGraphQLQuery } from './mapFieldMetadataToGraphQLQuery';
|
||||||
|
|
||||||
|
export const getUpdateOneObjectMutationGraphQLField = ({
|
||||||
|
objectNameSingular,
|
||||||
|
}: {
|
||||||
|
objectNameSingular: string;
|
||||||
|
}) => {
|
||||||
|
return `update${capitalize(objectNameSingular)}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const generateUpdateOneObjectMutation = ({
|
export const generateUpdateOneObjectMutation = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
@ -11,10 +21,18 @@ export const generateUpdateOneObjectMutation = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||||
|
|
||||||
|
const graphQLFieldForUpdateOneObjectMutation =
|
||||||
|
getUpdateOneObjectMutationGraphQLField({
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
return gql`
|
return gql`
|
||||||
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
|
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
|
||||||
update${capitalizedObjectName}(id: $idToUpdate, data: $input) {
|
${graphQLFieldForUpdateOneObjectMutation}(id: $idToUpdate, data: $input) {
|
||||||
id
|
id
|
||||||
|
${objectMetadataItem.fields
|
||||||
|
.map(mapFieldMetadataToGraphQLQuery)
|
||||||
|
.join('\n')}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { getPeopleOptimisticEffectDefinition } from '@/people/graphql/optimistic
|
|||||||
import { usePersonTableContextMenuEntries } from '@/people/hooks/usePersonTableContextMenuEntries';
|
import { usePersonTableContextMenuEntries } from '@/people/hooks/usePersonTableContextMenuEntries';
|
||||||
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
|
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
|
||||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||||
import { RecordTable } from '@/ui/object/record-table/components/RecordTable';
|
|
||||||
import { RecordTableEffect } from '@/ui/object/record-table/components/RecordTableEffect';
|
import { RecordTableEffect } from '@/ui/object/record-table/components/RecordTableEffect';
|
||||||
|
import { RecordTableV1 } from '@/ui/object/record-table/components/RecordTableV1';
|
||||||
import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId';
|
import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId';
|
||||||
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
|
||||||
import { TableOptionsDropdown } from '@/ui/object/record-table/options/components/TableOptionsDropdown';
|
import { TableOptionsDropdown } from '@/ui/object/record-table/options/components/TableOptionsDropdown';
|
||||||
@ -119,7 +119,7 @@ export const PersonTable = () => {
|
|||||||
setContextMenuEntries={setContextMenuEntries}
|
setContextMenuEntries={setContextMenuEntries}
|
||||||
setActionBarEntries={setActionBarEntries}
|
setActionBarEntries={setActionBarEntries}
|
||||||
/>
|
/>
|
||||||
<RecordTable
|
<RecordTableV1
|
||||||
updateEntityMutation={({
|
updateEntityMutation={({
|
||||||
variables,
|
variables,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -1,78 +1,72 @@
|
|||||||
import styled from '@emotion/styled';
|
import { useEffect } from 'react';
|
||||||
import { useVirtual } from '@tanstack/react-virtual';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
|
import { useFindOneObjectMetadataItem } from '@/metadata/hooks/useFindOneObjectMetadataItem';
|
||||||
|
import { useTableObjects } from '@/metadata/hooks/useTableObjects';
|
||||||
|
import { isFetchingMoreObjectsFamilyState } from '@/metadata/states/isFetchingMoreObjectsFamilyState';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { RowIdContext } from '../contexts/RowIdContext';
|
import { RowIdContext } from '../contexts/RowIdContext';
|
||||||
import { RowIndexContext } from '../contexts/RowIndexContext';
|
import { RowIndexContext } from '../contexts/RowIndexContext';
|
||||||
|
import { useRecordTable } from '../hooks/useRecordTable';
|
||||||
import { isFetchingRecordTableDataState } from '../states/isFetchingRecordTableDataState';
|
import { isFetchingRecordTableDataState } from '../states/isFetchingRecordTableDataState';
|
||||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||||
|
|
||||||
import { RecordTableRow } from './RecordTableRow';
|
import { RecordTableRow, StyledRow } from './RecordTableRow';
|
||||||
|
|
||||||
type SpaceProps = {
|
|
||||||
top?: number;
|
|
||||||
bottom?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledSpace = styled.td<SpaceProps>`
|
|
||||||
${({ top }) => top && `padding-top: ${top}px;`}
|
|
||||||
${({ bottom }) => bottom && `padding-bottom: ${bottom}px;`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const RecordTableBody = () => {
|
export const RecordTableBody = () => {
|
||||||
const scrollWrapperRef = useScrollWrapperScopedRef();
|
const { ref: lastTableRowRef, inView: lastTableRowIsVisible } = useInView();
|
||||||
|
|
||||||
const tableRowIds = useRecoilValue(tableRowIdsState);
|
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||||
|
|
||||||
|
const { scopeId: objectNamePlural } = useRecordTable();
|
||||||
|
|
||||||
|
const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({
|
||||||
|
objectNamePlural,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isFetchingMoreObjects] = useRecoilState(
|
||||||
|
isFetchingMoreObjectsFamilyState(foundObjectMetadataItem?.namePlural),
|
||||||
|
);
|
||||||
|
|
||||||
const isFetchingRecordTableData = useRecoilValue(
|
const isFetchingRecordTableData = useRecoilValue(
|
||||||
isFetchingRecordTableDataState,
|
isFetchingRecordTableDataState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rowVirtualizer = useVirtual({
|
const { fetchMoreObjects } = useTableObjects();
|
||||||
size: tableRowIds.length,
|
|
||||||
parentRef: scrollWrapperRef,
|
|
||||||
overscan: 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
const items = rowVirtualizer.virtualItems;
|
useEffect(() => {
|
||||||
const paddingTop = items.length > 0 ? items[0].start : 0;
|
if (lastTableRowIsVisible && isDefined(fetchMoreObjects)) {
|
||||||
const paddingBottom =
|
fetchMoreObjects();
|
||||||
items.length > 0
|
}
|
||||||
? rowVirtualizer.totalSize - items[items.length - 1].end
|
}, [lastTableRowIsVisible, fetchMoreObjects]);
|
||||||
: 0;
|
|
||||||
|
const lastRowId = tableRowIds[tableRowIds.length - 1];
|
||||||
|
|
||||||
if (isFetchingRecordTableData) {
|
if (isFetchingRecordTableData) {
|
||||||
return null;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
{paddingTop > 0 && (
|
{tableRowIds.map((rowId, rowIndex) => (
|
||||||
<tr>
|
<RowIdContext.Provider value={rowId} key={rowId}>
|
||||||
<StyledSpace top={paddingTop} />
|
<RowIndexContext.Provider value={rowIndex}>
|
||||||
</tr>
|
<RecordTableRow
|
||||||
)}
|
key={rowId}
|
||||||
{items.map((virtualItem) => {
|
ref={rowId === lastRowId ? lastTableRowRef : undefined}
|
||||||
const rowId = tableRowIds[virtualItem.index];
|
rowId={rowId}
|
||||||
|
/>
|
||||||
return (
|
</RowIndexContext.Provider>
|
||||||
<RowIdContext.Provider value={rowId} key={rowId}>
|
</RowIdContext.Provider>
|
||||||
<RowIndexContext.Provider value={virtualItem.index}>
|
))}
|
||||||
<RecordTableRow
|
{isFetchingMoreObjects && (
|
||||||
key={virtualItem.index}
|
<StyledRow selected={false}>
|
||||||
ref={virtualItem.measureRef}
|
<td style={{ height: 50 }} colSpan={1000}>
|
||||||
rowId={rowId}
|
Fetching more...
|
||||||
/>
|
</td>
|
||||||
</RowIndexContext.Provider>
|
</StyledRow>
|
||||||
</RowIdContext.Provider>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{paddingBottom > 0 && (
|
|
||||||
<tr>
|
|
||||||
<StyledSpace bottom={paddingBottom} />
|
|
||||||
</tr>
|
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { RowIdContext } from '../contexts/RowIdContext';
|
||||||
|
import { RowIndexContext } from '../contexts/RowIndexContext';
|
||||||
|
import { isFetchingRecordTableDataState } from '../states/isFetchingRecordTableDataState';
|
||||||
|
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||||
|
|
||||||
|
import { RecordTableRow } from './RecordTableRow';
|
||||||
|
|
||||||
|
export const RecordTableBodyV1 = () => {
|
||||||
|
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||||
|
|
||||||
|
const isFetchingRecordTableData = useRecoilValue(
|
||||||
|
isFetchingRecordTableDataState,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isFetchingRecordTableData) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{tableRowIds.map((rowId, rowIndex) => (
|
||||||
|
<RowIdContext.Provider value={rowId} key={rowId}>
|
||||||
|
<RowIndexContext.Provider value={rowIndex}>
|
||||||
|
<RecordTableRow key={rowId} rowId={rowId} />
|
||||||
|
</RowIndexContext.Provider>
|
||||||
|
</RowIdContext.Provider>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -26,8 +26,7 @@ export const RecordTableEffect = ({
|
|||||||
}: {
|
}: {
|
||||||
useGetRequest: typeof useGetCompaniesQuery | typeof useGetPeopleQuery;
|
useGetRequest: typeof useGetCompaniesQuery | typeof useGetPeopleQuery;
|
||||||
getRequestResultKey: string;
|
getRequestResultKey: string;
|
||||||
getRequestOptimisticEffectDefinition: OptimisticEffectDefinition<any>;
|
getRequestOptimisticEffectDefinition: OptimisticEffectDefinition;
|
||||||
|
|
||||||
filterDefinitionArray: FilterDefinition[];
|
filterDefinitionArray: FilterDefinition[];
|
||||||
sortDefinitionArray: SortDefinition[];
|
sortDefinitionArray: SortDefinition[];
|
||||||
setActionBarEntries?: () => void;
|
setActionBarEntries?: () => void;
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useCurrentRowSelected } from '../record-table-row/hooks/useCurrentRowSe
|
|||||||
import { CheckboxCell } from './CheckboxCell';
|
import { CheckboxCell } from './CheckboxCell';
|
||||||
import { RecordTableCell } from './RecordTableCell';
|
import { RecordTableCell } from './RecordTableCell';
|
||||||
|
|
||||||
const StyledRow = styled.tr<{ selected: boolean }>`
|
export const StyledRow = styled.tr<{ selected: boolean }>`
|
||||||
background: ${(props) =>
|
background: ${(props) =>
|
||||||
props.selected ? props.theme.accent.quaternary : 'none'};
|
props.selected ? props.theme.accent.quaternary : 'none'};
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -0,0 +1,139 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import {
|
||||||
|
useListenClickOutside,
|
||||||
|
useListenClickOutsideByClassName,
|
||||||
|
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
|
import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext';
|
||||||
|
import { useRecordTable } from '../hooks/useRecordTable';
|
||||||
|
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||||
|
|
||||||
|
import { RecordTableBodyV1 } from './RecordTableBodyV1';
|
||||||
|
import { RecordTableHeader } from './RecordTableHeader';
|
||||||
|
|
||||||
|
const StyledTable = styled.table`
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
border-spacing: 0;
|
||||||
|
margin-left: ${({ theme }) => theme.table.horizontalCellMargin};
|
||||||
|
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
|
||||||
|
table-layout: fixed;
|
||||||
|
|
||||||
|
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
|
||||||
|
|
||||||
|
th {
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-collapse: collapse;
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
:first-of-type {
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
:last-of-type {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-collapse: collapse;
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
:first-of-type {
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTableWithHeader = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTableContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type RecordTableV1Props = {
|
||||||
|
updateEntityMutation: (params: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordTableV1 = ({ updateEntityMutation }: RecordTableV1Props) => {
|
||||||
|
const tableBodyRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
leaveTableFocus,
|
||||||
|
setRowSelectedState,
|
||||||
|
resetTableRowSelection,
|
||||||
|
useMapKeyboardToSoftFocus,
|
||||||
|
} = useRecordTable();
|
||||||
|
|
||||||
|
useMapKeyboardToSoftFocus();
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: [tableBodyRef],
|
||||||
|
callback: () => {
|
||||||
|
leaveTableFocus();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
'escape',
|
||||||
|
() => {
|
||||||
|
resetTableRowSelection();
|
||||||
|
},
|
||||||
|
TableHotkeyScope.Table,
|
||||||
|
);
|
||||||
|
|
||||||
|
useListenClickOutsideByClassName({
|
||||||
|
classNames: ['entity-table-cell'],
|
||||||
|
excludeClassNames: ['action-bar', 'context-menu'],
|
||||||
|
callback: () => {
|
||||||
|
resetTableRowSelection();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EntityUpdateMutationContext.Provider value={updateEntityMutation}>
|
||||||
|
<StyledTableWithHeader>
|
||||||
|
<StyledTableContainer>
|
||||||
|
<div ref={tableBodyRef}>
|
||||||
|
<StyledTable className="entity-table-cell">
|
||||||
|
<RecordTableHeader />
|
||||||
|
<RecordTableBodyV1 />
|
||||||
|
</StyledTable>
|
||||||
|
<DragSelect
|
||||||
|
dragSelectable={tableBodyRef}
|
||||||
|
onDragSelectionStart={resetTableRowSelection}
|
||||||
|
onDragSelectionChange={setRowSelectedState}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyledTableContainer>
|
||||||
|
</StyledTableWithHeader>
|
||||||
|
</EntityUpdateMutationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -16416,6 +16416,11 @@ react-inspector@^6.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-6.0.2.tgz#aa3028803550cb6dbd7344816d5c80bf39d07e9d"
|
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-6.0.2.tgz#aa3028803550cb6dbd7344816d5c80bf39d07e9d"
|
||||||
integrity sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==
|
integrity sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==
|
||||||
|
|
||||||
|
react-intersection-observer@^9.5.2:
|
||||||
|
version "9.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.5.2.tgz#f68363a1ff292323c0808201b58134307a1626d0"
|
||||||
|
integrity sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==
|
||||||
|
|
||||||
react-is@18.1.0:
|
react-is@18.1.0:
|
||||||
version "18.1.0"
|
version "18.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
|
||||||
|
|||||||
Reference in New Issue
Block a user