Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,73 @@
import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
import { capitalize } from '~/utils/string/capitalize';
export const useCreateManyRecords = <T>({
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const { objectMetadataItem, createManyRecordsMutation } =
useObjectMetadataItem({
objectNameSingular,
});
const { generateEmptyRecord } = useGenerateEmptyRecord({
objectMetadataItem,
});
const apolloClient = useApolloClient();
const createManyRecords = async (data: Record<string, any>[]) => {
const withIds = data.map((record) => ({
...record,
id: (record.id as string) ?? v4(),
}));
withIds.forEach((record) => {
triggerOptimisticEffects(
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
generateEmptyRecord({ id: record.id }),
);
});
const createdObjects = await apolloClient.mutate({
mutation: createManyRecordsMutation,
variables: {
data: withIds,
},
optimisticResponse: {
[`create${capitalize(objectMetadataItem.namePlural)}`]: withIds.map(
(record) => generateEmptyRecord({ id: record.id }),
),
},
});
if (!createdObjects.data) {
return null;
}
const createdRecords =
(createdObjects.data[
`create${capitalize(objectMetadataItem.namePlural)}`
] as T[]) ?? [];
createdRecords.forEach((record) => {
triggerOptimisticEffects(
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
record,
);
});
return createdRecords;
};
return { createManyRecords };
};

View File

@ -0,0 +1,83 @@
import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { v4 } from 'uuid';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
import { capitalize } from '~/utils/string/capitalize';
type useCreateOneRecordProps = {
objectNameSingular: string;
refetchFindManyQuery?: boolean;
};
export const useCreateOneRecord = <T>({
objectNameSingular,
refetchFindManyQuery = false,
}: useCreateOneRecordProps) => {
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const { objectMetadataItem, createOneRecordMutation, findManyRecordsQuery } =
useObjectMetadataItem({
objectNameSingular,
});
// TODO: type this with a minimal type at least with Record<string, any>
const apolloClient = useApolloClient();
const { generateEmptyRecord } = useGenerateEmptyRecord({
objectMetadataItem,
});
const createOneRecord = async (input: Record<string, any>) => {
const recordId = v4();
const generatedEmptyRecord = generateEmptyRecord({
id: recordId,
...input,
});
if (generatedEmptyRecord) {
triggerOptimisticEffects(
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
generatedEmptyRecord,
);
}
const createdObject = await apolloClient.mutate({
mutation: createOneRecordMutation,
variables: {
input: { id: recordId, ...input },
},
optimisticResponse: {
[`create${capitalize(objectMetadataItem.nameSingular)}`]:
generateEmptyRecord({ id: recordId, ...input }),
},
refetchQueries: refetchFindManyQuery
? [getOperationName(findManyRecordsQuery) ?? '']
: [],
});
if (!createdObject.data) {
return null;
}
triggerOptimisticEffects(
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
createdObject.data[
`create${capitalize(objectMetadataItem.nameSingular)}`
],
);
return createdObject.data[
`create${capitalize(objectMetadataItem.nameSingular)}`
] as T;
};
return {
createOneRecord,
};
};

View File

@ -0,0 +1,73 @@
import { useCallback } from 'react';
import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
type useDeleteOneRecordProps = {
objectNameSingular: string;
refetchFindManyQuery?: boolean;
};
export const useDeleteOneRecord = <T>({
objectNameSingular,
refetchFindManyQuery = false,
}: useDeleteOneRecordProps) => {
const { performOptimisticEvict } = useOptimisticEvict();
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const { objectMetadataItem, deleteOneRecordMutation, findManyRecordsQuery } =
useObjectMetadataItem({
objectNameSingular,
});
const apolloClient = useApolloClient();
const deleteOneRecord = useCallback(
async (idToDelete: string) => {
triggerOptimisticEffects(
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
undefined,
[idToDelete],
);
performOptimisticEvict(
capitalize(objectMetadataItem.nameSingular),
'id',
idToDelete,
);
const deletedRecord = await apolloClient.mutate({
mutation: deleteOneRecordMutation,
variables: {
idToDelete,
},
refetchQueries: refetchFindManyQuery
? [getOperationName(findManyRecordsQuery) ?? '']
: [],
});
return deletedRecord.data[
`create${capitalize(objectMetadataItem.nameSingular)}`
] as T;
},
[
triggerOptimisticEffects,
objectMetadataItem.nameSingular,
performOptimisticEvict,
apolloClient,
deleteOneRecordMutation,
refetchFindManyQuery,
findManyRecordsQuery,
],
);
return {
deleteOneRecord,
};
};

View File

@ -0,0 +1,81 @@
import { ReactNode } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
export const useFieldContext = ({
objectNameSingular,
fieldMetadataName,
objectRecordId,
fieldPosition,
forceRefetch,
}: {
objectNameSingular: string;
objectRecordId: string;
fieldMetadataName: string;
fieldPosition: number;
forceRefetch?: boolean;
}) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const fieldMetadataItem = objectMetadataItem?.fields.find(
(field) => field.name === fieldMetadataName,
);
const useUpdateOneObjectMutation: () => [(params: any) => any, any] = () => {
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,
});
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
updateOneRecord?.({
idToUpdate: variables.where.id,
input: variables.data,
forceRefetch,
});
};
return [updateEntity, { loading: false }];
};
const FieldContextProvider =
fieldMetadataItem && objectMetadataItem
? ({ children }: { children: ReactNode }) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
entityId: objectRecordId,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: fieldPosition,
objectMetadataItem,
}),
useUpdateEntityMutation: useUpdateOneObjectMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
{children}
</FieldContext.Provider>
)
: undefined;
return {
FieldContextProvider,
};
};

View File

@ -0,0 +1,227 @@
import { useCallback, useMemo } from 'react';
import { useQuery } from '@apollo/client';
import { isNonEmptyArray } from '@apollo/client/utilities';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { getRecordOptimisticEffectDefinition } from '@/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition';
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { logError } from '~/utils/logError';
import { capitalize } from '~/utils/string/capitalize';
import { cursorFamilyState } from '../states/cursorFamilyState';
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState';
import { PaginatedRecordType } from '../types/PaginatedRecordType';
import {
PaginatedRecordTypeEdge,
PaginatedRecordTypeResults,
} from '../types/PaginatedRecordTypeResults';
import { mapPaginatedRecordsToRecords } from '../utils/mapPaginatedRecordsToRecords';
export const useFindManyRecords = <
RecordType extends { id: string } & Record<string, any>,
>({
objectNameSingular,
filter,
orderBy,
limit = DEFAULT_SEARCH_REQUEST_LIMIT,
onCompleted,
skip,
}: ObjectMetadataItemIdentifier & {
filter?: any;
orderBy?: OrderByField;
limit?: number;
onCompleted?: (data: PaginatedRecordTypeResults<RecordType>) => void;
skip?: boolean;
}) => {
const findManyQueryStateIdentifier =
objectNameSingular +
JSON.stringify(filter) +
JSON.stringify(orderBy) +
limit;
const [lastCursor, setLastCursor] = useRecoilState(
cursorFamilyState(findManyQueryStateIdentifier),
);
const [hasNextPage, setHasNextPage] = useRecoilState(
hasNextPageFamilyState(findManyQueryStateIdentifier),
);
const setIsFetchingMoreObjects = useSetRecoilState(
isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier),
);
const { objectMetadataItem, findManyRecordsQuery } = useObjectMetadataItem({
objectNameSingular,
});
const { registerOptimisticEffect } = useOptimisticEffect({
objectNameSingular,
});
const { enqueueSnackBar } = useSnackBar();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { data, loading, error, fetchMore } = useQuery<
PaginatedRecordType<RecordType>
>(findManyRecordsQuery, {
skip: skip || !objectMetadataItem || !currentWorkspace,
variables: {
filter: filter ?? {},
limit: limit,
orderBy: orderBy ?? {},
},
onCompleted: (data) => {
if (objectMetadataItem) {
registerOptimisticEffect({
variables: {
filter: filter ?? {},
orderBy: orderBy ?? {},
limit: limit,
},
definition: getRecordOptimisticEffectDefinition({
objectMetadataItem,
}),
});
}
onCompleted?.(data[objectMetadataItem.namePlural]);
if (data?.[objectMetadataItem.namePlural]) {
setLastCursor(
data?.[objectMetadataItem.namePlural]?.pageInfo.endCursor,
);
setHasNextPage(
data?.[objectMetadataItem.namePlural]?.pageInfo.hasNextPage,
);
}
},
onError: (error) => {
logError(
`useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` +
error,
);
enqueueSnackBar(
`Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`,
{
variant: 'error',
},
);
},
});
const fetchMoreRecords = useCallback(async () => {
if (hasNextPage) {
setIsFetchingMoreObjects(true);
try {
await fetchMore({
variables: {
filter: filter ?? {},
orderBy: orderBy ?? {},
lastCursor: isNonEmptyString(lastCursor) ? lastCursor : undefined,
},
updateQuery: (prev, { fetchMoreResult }) => {
const previousEdges = prev?.[objectMetadataItem.namePlural]?.edges;
const nextEdges =
fetchMoreResult?.[objectMetadataItem.namePlural]?.edges;
let newEdges: PaginatedRecordTypeEdge<RecordType>[] = [];
if (isNonEmptyArray(previousEdges) && isNonEmptyArray(nextEdges)) {
newEdges = filterUniqueRecordEdgesByCursor([
...prev?.[objectMetadataItem.namePlural]?.edges,
...fetchMoreResult?.[objectMetadataItem.namePlural]?.edges,
]);
}
if (data?.[objectMetadataItem.namePlural]) {
setLastCursor(
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo
.endCursor,
);
setHasNextPage(
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo
.hasNextPage,
);
}
onCompleted?.({
__typename: `${capitalize(
objectMetadataItem.nameSingular,
)}Connection`,
edges: newEdges,
pageInfo:
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
});
return Object.assign({}, prev, {
[objectMetadataItem.namePlural]: {
__typename: `${capitalize(
objectMetadataItem.nameSingular,
)}Connection`,
edges: newEdges,
pageInfo:
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
},
} as PaginatedRecordType<RecordType>);
},
});
} catch (error) {
logError(
`fetchMoreObjects for "${objectMetadataItem.namePlural}" error : ` +
error,
);
enqueueSnackBar(
`Error during fetchMoreObjects for "${objectMetadataItem.namePlural}", ${error}`,
{
variant: 'error',
},
);
} finally {
setIsFetchingMoreObjects(false);
}
}
}, [
hasNextPage,
setIsFetchingMoreObjects,
fetchMore,
filter,
orderBy,
lastCursor,
objectMetadataItem.namePlural,
objectMetadataItem.nameSingular,
onCompleted,
data,
setLastCursor,
setHasNextPage,
enqueueSnackBar,
]);
const records = useMemo(
() =>
mapPaginatedRecordsToRecords({
pagedRecords: data,
objectNamePlural: objectMetadataItem.namePlural,
}),
[data, objectMetadataItem],
);
return {
objectMetadataItem,
records: records as RecordType[],
loading,
error,
fetchMoreRecords,
queryStateIdentifier: findManyQueryStateIdentifier,
};
};

View File

@ -0,0 +1,49 @@
import { useQuery } from '@apollo/client';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
export const useFindOneRecord = <
ObjectType extends { id: string } & Record<string, any>,
>({
objectNameSingular,
objectRecordId,
onCompleted,
depth,
skip,
}: ObjectMetadataItemIdentifier & {
objectRecordId: string | undefined;
onCompleted?: (data: ObjectType) => void;
skip?: boolean;
depth?: number;
}) => {
const { objectMetadataItem, findOneRecordQuery } = useObjectMetadataItem(
{
objectNameSingular,
},
depth,
);
const { data, loading, error } = useQuery<
{ [nameSingular: string]: ObjectType },
{ objectRecordId: string }
>(findOneRecordQuery, {
skip: !objectMetadataItem || !objectRecordId || skip,
variables: {
objectRecordId: objectRecordId ?? '',
},
onCompleted: (data) => {
if (onCompleted) {
onCompleted(data[objectNameSingular]);
}
},
});
const record = data ? data[objectNameSingular] : undefined;
return {
record,
loading,
error,
};
};

View File

@ -0,0 +1,30 @@
import { gql } from '@apollo/client';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const useGenerateCreateManyRecordMutation = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
if (!objectMetadataItem) {
return EMPTY_MUTATION;
}
return gql`
mutation Create${capitalize(
objectMetadataItem.namePlural,
)}($data: [${capitalize(objectMetadataItem.nameSingular)}CreateInput!]!) {
create${capitalize(objectMetadataItem.namePlural)}(data: $data) {
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field))
.join('\n')}
}
}`;
};

View File

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

View File

@ -0,0 +1,169 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const useGenerateEmptyRecord = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
// Todo fix typing once we generate the return base on Metadata
const generateEmptyRecord = <T>(input: Partial<T> & { id: string }) => {
// Todo replace this by runtime typing
const validatedInput = input as { id: string } & { [key: string]: any };
if (objectMetadataItem.nameSingular === 'company') {
return {
id: validatedInput.id,
domainName: '',
accountOwnerId: null,
createdAt: new Date().toISOString(),
address: '',
people: [
{
edges: [],
__typename: 'PersonConnection',
},
],
xLink: {
label: '',
url: '',
__typename: 'Link',
},
attachments: {
edges: [],
__typename: 'AttachmentConnection',
},
activityTargets: {
edges: [],
__typename: 'ActivityTargetConnection',
},
idealCustomerProfile: null,
annualRecurringRevenue: {
amountMicros: null,
currencyCode: null,
__typename: 'Currency',
},
updatedAt: new Date().toISOString(),
employees: null,
accountOwner: null,
name: '',
linkedinLink: {
label: '',
url: '',
__typename: 'Link',
},
favorites: {
edges: [],
__typename: 'FavoriteConnection',
},
opportunities: {
edges: [],
__typename: 'OpportunityConnection',
},
__typename: 'Company',
} as T;
}
if (objectMetadataItem.nameSingular === 'person') {
return {
id: validatedInput.id,
activityTargets: {
edges: [],
__typename: 'ActivityTargetConnection',
},
opportunities: {
edges: [],
__typename: 'OpportunityConnection',
},
companyId: null,
favorites: {
edges: [],
__typename: 'FavoriteConnection',
},
phone: '',
company: null,
xLink: {
label: '',
url: '',
__typename: 'Link',
},
jobTitle: '',
pointOfContactForOpportunities: {
edges: [],
__typename: 'OpportunityConnection',
},
email: '',
attachments: {
edges: [],
__typename: 'AttachmentConnection',
},
name: {
firstName: '',
lastName: '',
__typename: 'FullName',
},
avatarUrl: '',
updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
city: '',
linkedinLink: {
label: '',
url: '',
__typename: 'Link',
},
__typename: 'Person',
} as T;
}
if (objectMetadataItem.nameSingular === 'opportunity') {
return {
id: validatedInput.id,
pipelineStepId: validatedInput.pipelineStepId,
closeDate: null,
updatedAt: new Date().toISOString(),
pipelineStep: null,
probability: '0',
pointOfContactId: null,
personId: null,
amount: {
amountMicros: null,
currencyCode: null,
__typename: 'Currency',
},
createdAt: new Date().toISOString(),
pointOfContact: null,
person: null,
company: null,
companyId: validatedInput.companyId,
__typename: 'Opportunity',
} as T;
}
if (objectMetadataItem.nameSingular === 'opportunity') {
return {
id: validatedInput.id,
pipelineStepId: validatedInput.pipelineStepId,
closeDate: null,
updatedAt: new Date().toISOString(),
pipelineStep: null,
probability: '0',
pointOfContactId: null,
personId: null,
amount: {
amountMicros: null,
currencyCode: null,
__typename: 'Currency',
},
createdAt: new Date().toISOString(),
pointOfContact: null,
person: null,
company: null,
companyId: validatedInput.companyId,
__typename: 'Opportunity',
} as T;
}
};
return {
generateEmptyRecord: generateEmptyRecord,
};
};

View File

@ -0,0 +1,49 @@
import { gql } from '@apollo/client';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const useGenerateFindManyRecordsQuery = ({
objectMetadataItem,
depth,
}: {
objectMetadataItem: ObjectMetadataItem;
depth?: number;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
if (!objectMetadataItem) {
return EMPTY_QUERY;
}
return gql`
query FindMany${capitalize(
objectMetadataItem.namePlural,
)}($filter: ${capitalize(
objectMetadataItem.nameSingular,
)}FilterInput, $orderBy: ${capitalize(
objectMetadataItem.nameSingular,
)}OrderByInput, $lastCursor: String, $limit: Float = 30) {
${
objectMetadataItem.namePlural
}(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){
edges {
node {
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field, depth))
.join('\n')}
}
cursor
}
pageInfo {
hasNextPage
startCursor
endCursor
}
}
}
`;
};

View File

@ -0,0 +1,34 @@
import { gql } from '@apollo/client';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const useGenerateFindOneRecordQuery = ({
objectMetadataItem,
depth,
}: {
objectMetadataItem: ObjectMetadataItem;
depth?: number;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
if (!objectMetadataItem) {
return EMPTY_QUERY;
}
return gql`
query FindOne${objectMetadataItem.nameSingular}($objectRecordId: UUID!) {
${objectMetadataItem.nameSingular}(filter: {
id: {
eq: $objectRecordId
}
}){
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field, depth))
.join('\n')}
}
}
`;
};

View File

@ -0,0 +1,44 @@
import { gql } from '@apollo/client';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const getUpdateOneRecordMutationGraphQLField = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
return `update${capitalize(objectNameSingular)}`;
};
export const useGenerateUpdateOneRecordMutation = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
if (!objectMetadataItem) {
return EMPTY_MUTATION;
}
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const graphQLFieldForUpdateOneRecordMutation =
getUpdateOneRecordMutationGraphQLField({
objectNameSingular: objectMetadataItem.nameSingular,
});
return gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
${graphQLFieldForUpdateOneRecordMutation}(id: $idToUpdate, data: $input) {
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field))
.join('\n')}
}
}
`;
};

View File

@ -0,0 +1,43 @@
import { gql, useApolloClient } from '@apollo/client';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const useGetRecordFromCache = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
const apolloClient = useApolloClient();
return (recordId: string) => {
if (!objectMetadataItem) {
return EMPTY_MUTATION;
}
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const cacheReadFragment = gql`
fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} {
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field))
.join('\n')}
}
`;
const cache = apolloClient.cache;
const cachedRecordId = cache.identify({
__typename: capitalize(objectMetadataItem.nameSingular),
id: recordId,
});
return cache.readFragment({
id: cachedRecordId,
fragment: cacheReadFragment,
});
};
};

View File

@ -0,0 +1,31 @@
import { useApolloClient } from '@apollo/client';
import { Modifiers } from '@apollo/client/cache';
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const useModifyRecordFromCache = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const apolloClient = useApolloClient();
return (recordId: string, fieldModifiers: Modifiers) => {
if (!objectMetadataItem) {
return EMPTY_MUTATION;
}
const cache = apolloClient.cache;
const cachedRecordId = cache.identify({
__typename: capitalize(objectMetadataItem.nameSingular),
id: recordId,
});
cache.modify({
id: cachedRecordId,
fields: fieldModifiers,
});
};
};

View File

@ -0,0 +1,104 @@
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { Company } from '@/companies/types/Company';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { turnFiltersIntoWhereClause } from '@/object-record/object-filter-dropdown/utils/turnFiltersIntoWhereClause';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { useFindManyRecords } from './useFindManyRecords';
export const useObjectRecordBoard = () => {
const objectNameSingular = 'opportunity';
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
{
objectNameSingular,
},
);
const {
isBoardLoadedState,
boardFiltersState,
boardSortsState,
savedCompaniesState,
savedOpportunitiesState,
savedPipelineStepsState,
} = useRecordBoardScopedStates();
const setIsBoardLoaded = useSetRecoilState(isBoardLoadedState);
const boardFilters = useRecoilValue(boardFiltersState);
const boardSorts = useRecoilValue(boardSortsState);
const setSavedCompanies = useSetRecoilState(savedCompaniesState);
const [savedOpportunities] = useRecoilState(savedOpportunitiesState);
const [savedPipelineSteps, setSavedPipelineSteps] = useRecoilState(
savedPipelineStepsState,
);
const filter = turnFiltersIntoWhereClause(
boardFilters,
foundObjectMetadataItem?.fields ?? [],
);
const orderBy = turnSortsIntoOrderBy(
boardSorts,
foundObjectMetadataItem?.fields ?? [],
);
useFindManyRecords({
objectNameSingular: 'pipelineStep',
filter: {},
onCompleted: useCallback(
(data: PaginatedRecordTypeResults<PipelineStep>) => {
setSavedPipelineSteps(data.edges.map((edge) => edge.node));
},
[setSavedPipelineSteps],
),
});
const {
records: opportunities,
loading,
fetchMoreRecords: fetchMoreOpportunities,
} = useFindManyRecords<Opportunity>({
skip: !savedPipelineSteps.length,
objectNameSingular: 'opportunity',
filter: filter,
orderBy: orderBy as any, // TODO: finish typing
onCompleted: useCallback(() => {
setIsBoardLoaded(true);
}, [setIsBoardLoaded]),
});
const { fetchMoreRecords: fetchMoreCompanies } = useFindManyRecords({
skip: !savedOpportunities.length,
objectNameSingular: 'company',
filter: {
id: {
in: savedOpportunities.map(
(opportunity) => opportunity.companyId || '',
),
},
},
onCompleted: useCallback(
(data: PaginatedRecordTypeResults<Company>) => {
setSavedCompanies(data.edges.map((edge) => edge.node));
},
[setSavedCompanies],
),
});
return {
opportunities,
loading,
fetchMoreOpportunities,
fetchMoreCompanies,
};
};

View File

@ -0,0 +1,104 @@
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { Company } from '@/companies/types/Company';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { turnFiltersIntoWhereClause } from '@/object-record/object-filter-dropdown/utils/turnFiltersIntoWhereClause';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { PipelineStep } from '@/pipeline/types/PipelineStep';
import { useFindManyRecords } from './useFindManyRecords';
export const useObjectRecordBoard = () => {
const objectNameSingular = 'opportunity';
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
{
objectNameSingular,
},
);
const {
isBoardLoadedState,
boardFiltersState,
boardSortsState,
savedCompaniesState,
savedOpportunitiesState,
savedPipelineStepsState,
} = useRecordBoardScopedStates();
const setIsBoardLoaded = useSetRecoilState(isBoardLoadedState);
const boardFilters = useRecoilValue(boardFiltersState);
const boardSorts = useRecoilValue(boardSortsState);
const setSavedCompanies = useSetRecoilState(savedCompaniesState);
const [savedOpportunities] = useRecoilState(savedOpportunitiesState);
const [savedPipelineSteps, setSavedPipelineSteps] = useRecoilState(
savedPipelineStepsState,
);
const filter = turnFiltersIntoWhereClause(
boardFilters,
foundObjectMetadataItem?.fields ?? [],
);
const orderBy = turnSortsIntoOrderBy(
boardSorts,
foundObjectMetadataItem?.fields ?? [],
);
useFindManyRecords({
objectNameSingular: 'pipelineStep',
filter: {},
onCompleted: useCallback(
(data: PaginatedRecordTypeResults<PipelineStep>) => {
setSavedPipelineSteps(data.edges.map((edge) => edge.node));
},
[setSavedPipelineSteps],
),
});
const {
records: opportunities,
loading,
fetchMoreRecords: fetchMoreOpportunities,
} = useFindManyRecords<Opportunity>({
skip: !savedPipelineSteps.length,
objectNameSingular: 'opportunity',
filter: filter,
orderBy: orderBy as any, // TODO: finish typing
onCompleted: useCallback(() => {
setIsBoardLoaded(true);
}, [setIsBoardLoaded]),
});
const { fetchMoreRecords: fetchMoreCompanies } = useFindManyRecords({
skip: !savedOpportunities.length,
objectNameSingular: 'company',
filter: {
id: {
in: savedOpportunities.map(
(opportunity) => opportunity.companyId || '',
),
},
},
onCompleted: useCallback(
(data: PaginatedRecordTypeResults<Company>) => {
setSavedCompanies(data.edges.map((edge) => edge.node));
},
[setSavedCompanies],
),
});
return {
opportunities,
loading,
fetchMoreOpportunities,
fetchMoreCompanies,
};
};

View File

@ -0,0 +1,62 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { turnFiltersIntoWhereClause } from '@/object-record/object-filter-dropdown/utils/turnFiltersIntoWhereClause';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { signInBackgroundMockCompanies } from '@/sign-in-background-mock/constants/signInBackgroundMockCompanies';
import { useFindManyRecords } from './useFindManyRecords';
export const useObjectRecordTable = () => {
const { scopeId: objectNamePlural, setRecordTableData } = useRecordTable();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
{
objectNameSingular,
},
);
const { tableFiltersState, tableSortsState, tableLastRowVisibleState } =
useRecordTableScopedStates();
const tableFilters = useRecoilValue(tableFiltersState);
const tableSorts = useRecoilValue(tableSortsState);
const setLastRowVisible = useSetRecoilState(tableLastRowVisibleState);
const filter = turnFiltersIntoWhereClause(
tableFilters,
foundObjectMetadataItem?.fields ?? [],
);
// TODO: finish typing
const orderBy = turnSortsIntoOrderBy(
tableSorts,
foundObjectMetadataItem?.fields ?? [],
) as any;
const { records, loading, fetchMoreRecords, queryStateIdentifier } =
useFindManyRecords({
objectNameSingular,
filter,
orderBy,
onCompleted: () => {
setLastRowVisible(false);
},
});
return {
records: currentWorkspace ? records : signInBackgroundMockCompanies,
loading,
fetchMoreRecords,
queryStateIdentifier,
setRecordTableData,
};
};

View File

@ -0,0 +1,155 @@
import { useCallback } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { selectedRowIdsSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsSelector';
import { IconHeart, IconHeartOff, IconTrash } from '@/ui/display/icon';
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
type useRecordTableContextMenuEntriesProps = {
recordTableScopeId?: string;
};
// TODO: refactor this
export const useRecordTableContextMenuEntries = (
props?: useRecordTableContextMenuEntriesProps,
) => {
const scopeId = useAvailableScopeIdOrThrow(
RecordTableScopeInternalContext,
props?.recordTableScopeId,
);
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState);
const selectedRowIds = useRecoilValue(selectedRowIdsSelector);
const { scopeId: objectNamePlural, resetTableRowSelection } = useRecordTable({
recordTableScopeId: scopeId,
});
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { createFavorite, deleteFavorite, favorites } = useFavorites({
objectNamePlural,
});
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
const selectedRowIds = snapshot
.getLoadable(selectedRowIdsSelector)
.getValue();
const selectedRowId = selectedRowIds.length === 1 ? selectedRowIds[0] : '';
const isFavorite =
!!selectedRowId &&
!!favorites?.find((favorite) => favorite.recordId === selectedRowId);
resetTableRowSelection();
if (isFavorite) {
deleteFavorite(selectedRowId);
} else {
createFavorite(selectedRowId);
}
});
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular,
});
const handleDeleteClick = useRecoilCallback(
({ snapshot }) =>
async () => {
const rowIdsToDelete = snapshot
.getLoadable(selectedRowIdsSelector)
.getValue();
resetTableRowSelection();
await Promise.all(
rowIdsToDelete.map(async (rowId) => {
await deleteOneRecord(rowId);
}),
);
},
[deleteOneRecord, resetTableRowSelection],
);
return {
setContextMenuEntries: useCallback(() => {
const selectedRowId =
selectedRowIds.length === 1 ? selectedRowIds[0] : '';
const isFavorite =
isNonEmptyString(selectedRowId) &&
!!favorites?.find((favorite) => favorite.recordId === selectedRowId);
const contextMenuEntries = [
// {
// label: 'New task',
// Icon: IconCheckbox,
// onClick: () => {},
// },
// {
// label: 'New note',
// Icon: IconNotes,
// onClick: () => {},
// },
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: () => handleDeleteClick(),
},
] as ContextMenuEntry[];
if (selectedRowIds.length === 1) {
contextMenuEntries.unshift({
label: isFavorite ? 'Remove from favorites' : 'Add to favorites',
Icon: isFavorite ? IconHeartOff : IconHeart,
onClick: () => handleFavoriteButtonClick(),
});
}
setContextMenuEntries(contextMenuEntries);
}, [
selectedRowIds,
favorites,
handleDeleteClick,
handleFavoriteButtonClick,
setContextMenuEntries,
]),
setActionBarEntries: useRecoilCallback(() => () => {
setActionBarEntriesState([
// {
// label: 'Task',
// Icon: IconCheckbox,
// onClick: () => {},
// },
// {
// label: 'Note',
// Icon: IconNotes,
// onClick: () => {},
// },
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: () => handleDeleteClick(),
},
]);
}),
};
};

View File

@ -0,0 +1,68 @@
import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
type useUpdateOneRecordProps = {
objectNameSingular: string;
refetchFindManyQuery?: boolean;
};
export const useUpdateOneRecord = <T>({
objectNameSingular,
refetchFindManyQuery = false,
}: useUpdateOneRecordProps) => {
const {
objectMetadataItem,
updateOneRecordMutation,
getRecordFromCache,
findManyRecordsQuery,
} = useObjectMetadataItem({
objectNameSingular,
});
const apolloClient = useApolloClient();
const updateOneRecord = async ({
idToUpdate,
input,
}: {
idToUpdate: string;
input: Record<string, any>;
forceRefetch?: boolean;
}) => {
const cachedRecord = getRecordFromCache(idToUpdate);
const updatedRecord = await apolloClient.mutate({
mutation: updateOneRecordMutation,
variables: {
idToUpdate: idToUpdate,
input: {
...input,
},
},
optimisticResponse: {
[`update${capitalize(objectMetadataItem.nameSingular)}`]: {
...(cachedRecord ?? {}),
...input,
},
},
refetchQueries: refetchFindManyQuery
? [getOperationName(findManyRecordsQuery) ?? '']
: [],
});
if (!updatedRecord?.data) {
return null;
}
return updatedRecord.data[
`update${capitalize(objectMetadataItem.nameSingular)}`
] as T;
};
return {
updateOneRecord,
};
};