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,35 @@
import { useMemo } from 'react';
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { useRecoilState } from 'recoil';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { ApolloMetadataClientContext } from '../context/ApolloClientMetadataContext';
export const ApolloMetadataClientProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [tokenPair] = useRecoilState(tokenPairState);
const apolloMetadataClient = useMemo(() => {
if (tokenPair?.accessToken.token) {
return new ApolloClient({
uri: `${REACT_APP_SERVER_BASE_URL}/metadata`,
cache: new InMemoryCache(),
headers: {
Authorization: `Bearer ${tokenPair.accessToken.token}`,
},
});
} else {
return null;
}
}, [tokenPair]);
return (
<ApolloMetadataClientContext.Provider value={apolloMetadataClient}>
{children}
</ApolloMetadataClientContext.Provider>
);
};

View File

@ -0,0 +1,23 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const ObjectMetadataItemsLoadEffect = () => {
const { objectMetadataItems: newObjectMetadataItems } =
useFindManyObjectMetadataItems();
const [objectMetadataItems, setObjectMetadataItems] = useRecoilState(
objectMetadataItemsState,
);
useEffect(() => {
if (!isDeeplyEqual(objectMetadataItems, newObjectMetadataItems)) {
setObjectMetadataItems(newObjectMetadataItems);
}
}, [newObjectMetadataItems, objectMetadataItems, setObjectMetadataItems]);
return <></>;
};

View File

@ -0,0 +1,33 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
export const ObjectMetadataItemsProvider = ({
children,
}: React.PropsWithChildren) => {
useFindManyObjectMetadataItems();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
return (
<>
<ObjectMetadataItemsLoadEffect />
{objectMetadataItems.length < 1 && currentWorkspace ? (
<></>
) : (
<>
<ObjectMetadataItemsRelationPickerEffect />
<RelationPickerScope relationPickerScopeId="relation-picker">
{children}
</RelationPickerScope>
</>
)}
</>
);
};

View File

@ -0,0 +1,82 @@
import { useEffect } from 'react';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { IdentifiersMapper } from '@/object-record/relation-picker/types/IdentifiersMapper';
import { getLogoUrlFromDomainName } from '~/utils';
export const ObjectMetadataItemsRelationPickerEffect = () => {
const { setIdentifiersMapper, setSearchQuery } = useRelationPicker({
relationPickerScopeId: 'relation-picker',
});
const computeFilterFields = (relationPickerType: string) => {
if (relationPickerType === 'company') {
return ['name'];
}
if (['workspaceMember', 'person'].includes(relationPickerType)) {
return ['name.firstName', 'name.lastName'];
}
return ['name'];
};
const identifierMapper: IdentifiersMapper = (
record: any,
objectMetadataItemSingularName: string,
) => {
if (!record) {
return;
}
if (objectMetadataItemSingularName === 'company') {
return {
id: record.id,
name: record.name,
avatarUrl: getLogoUrlFromDomainName(record.domainName ?? ''),
avatarType: 'squared',
record: record,
};
}
if (
['workspaceMember', 'person'].includes(objectMetadataItemSingularName)
) {
return {
id: record.id,
name:
(record.name?.firstName ?? '') + ' ' + (record.name?.lastName ?? ''),
avatarUrl: record.avatarUrl,
avatarType: 'rounded',
record: record,
};
}
if (['opportunity'].includes(objectMetadataItemSingularName)) {
return {
id: record.id,
name: record?.company?.name,
avatarUrl: record.avatarUrl,
avatarType: 'rounded',
record: record,
};
}
return {
id: record.id,
name: record.name,
avatarUrl: record.avatarUrl,
avatarType: 'rounded',
record,
};
};
useEffect(() => {
setIdentifiersMapper(() => identifierMapper);
setSearchQuery({
computeFilterFields,
});
}, [setIdentifiersMapper, setSearchQuery]);
return <></>;
};

View File

@ -0,0 +1,34 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { Icon123 } from '@/ui/input/constants/icons';
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
export const ObjectMetadataNavItems = () => {
const { activeObjectMetadataItems } = useObjectMetadataItemForSettings();
const navigate = useNavigate();
const { icons } = useLazyLoadIcons();
const currentPath = useLocation().pathname;
return (
<>
{activeObjectMetadataItems.map((objectMetadataItem) =>
objectMetadataItem.nameSingular === 'opportunity' ? null : (
<NavigationDrawerItem
key={objectMetadataItem.id}
label={objectMetadataItem.labelPlural}
to={`/objects/${objectMetadataItem.namePlural}`}
active={currentPath == `/objects/${objectMetadataItem.namePlural}`}
Icon={
objectMetadataItem.icon ? icons[objectMetadataItem.icon] : Icon123
}
onClick={() => {
navigate(`/objects/${objectMetadataItem.namePlural}`);
}}
/>
),
)}
</>
);
};

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
import { ApolloClient, NormalizedCacheObject } from '@apollo/client';
export const ApolloMetadataClientContext =
createContext<ApolloClient<NormalizedCacheObject> | null>(null);

View File

@ -0,0 +1,13 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export class ObjectMetadataItemNotFoundError extends Error {
constructor(objectName: string, objectMetadataItems: ObjectMetadataItem[]) {
const message = `Object metadata item "${objectName}" cannot be found in an array of ${
objectMetadataItems?.length ?? 0
} elements`;
super(message);
Object.setPrototypeOf(this, ObjectMetadataItemNotFoundError.prototype);
}
}

View File

@ -0,0 +1,141 @@
import { gql } from '@apollo/client';
export const CREATE_ONE_OBJECT_METADATA_ITEM = gql`
mutation CreateOneObjectMetadataItem($input: CreateOneObjectInput!) {
createOneObject(input: $input) {
id
dataSourceId
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isActive
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
}
}
`;
export const CREATE_ONE_FIELD_METADATA_ITEM = gql`
mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {
createOneField(input: $input) {
id
type
name
label
description
icon
isCustom
isActive
isNullable
createdAt
updatedAt
defaultValue
options
}
}
`;
export const CREATE_ONE_RELATION_METADATA_ITEM = gql`
mutation CreateOneRelationMetadata($input: CreateOneRelationInput!) {
createOneRelation(input: $input) {
id
relationType
fromObjectMetadataId
toObjectMetadataId
fromFieldMetadataId
toFieldMetadataId
createdAt
updatedAt
}
}
`;
export const UPDATE_ONE_FIELD_METADATA_ITEM = gql`
mutation UpdateOneFieldMetadataItem(
$idToUpdate: ID!
$updatePayload: UpdateFieldInput!
) {
updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {
id
type
name
label
description
icon
isCustom
isActive
isNullable
createdAt
updatedAt
}
}
`;
export const UPDATE_ONE_OBJECT_METADATA_ITEM = gql`
mutation UpdateOneObjectMetadataItem(
$idToUpdate: ID!
$updatePayload: UpdateObjectInput!
) {
updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {
id
dataSourceId
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isActive
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
}
}
`;
export const DELETE_ONE_OBJECT_METADATA_ITEM = gql`
mutation DeleteOneObjectMetadataItem($idToDelete: ID!) {
deleteOneObject(input: { id: $idToDelete }) {
id
dataSourceId
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isActive
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
}
}
`;
export const DELETE_ONE_FIELD_METADATA_ITEM = gql`
mutation DeleteOneFieldMetadataItem($idToDelete: ID!) {
deleteOneField(input: { id: $idToDelete }) {
id
type
name
label
description
icon
isCustom
isActive
isNullable
createdAt
updatedAt
}
}
`;

View File

@ -0,0 +1,86 @@
import { gql } from '@apollo/client';
export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
query ObjectMetadataItems(
$objectFilter: objectFilter
$fieldFilter: fieldFilter
) {
objects(paging: { first: 1000 }, filter: $objectFilter) {
edges {
node {
id
dataSourceId
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isActive
isSystem
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
fields(paging: { first: 1000 }, filter: $fieldFilter) {
edges {
node {
id
type
name
label
description
icon
isCustom
isActive
isSystem
isNullable
createdAt
updatedAt
fromRelationMetadata {
id
relationType
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
}
toFieldMetadataId
}
toRelationMetadata {
id
relationType
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
}
fromFieldMetadataId
}
defaultValue
options
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`;

View File

@ -0,0 +1,9 @@
import { useContext } from 'react';
import { ApolloMetadataClientContext } from '../context/ApolloClientMetadataContext';
export const useApolloMetadataClient = () => {
const apolloMetadataClient = useContext(ApolloMetadataClientContext);
return apolloMetadataClient;
};

View File

@ -0,0 +1,52 @@
import { useMemo } from 'react';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { Nullable } from '~/types/Nullable';
import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition';
import { formatFieldMetadataItemsAsFilterDefinitions } from '../utils/formatFieldMetadataItemsAsFilterDefinitions';
import { formatFieldMetadataItemsAsSortDefinitions } from '../utils/formatFieldMetadataItemsAsSortDefinitions';
export const useColumnDefinitionsFromFieldMetadata = (
objectMetadataItem?: Nullable<ObjectMetadataItem>,
) => {
const activeFieldMetadataItems = useMemo(
() =>
objectMetadataItem
? objectMetadataItem.fields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
)
: [],
[objectMetadataItem],
);
const columnDefinitions: ColumnDefinition<FieldMetadata>[] = useMemo(
() =>
objectMetadataItem
? activeFieldMetadataItems.map((field, index) =>
formatFieldMetadataItemAsColumnDefinition({
position: index,
field,
objectMetadataItem,
}),
)
: [],
[activeFieldMetadataItems, objectMetadataItem],
);
const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({
fields: activeFieldMetadataItems,
});
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
fields: activeFieldMetadataItems,
});
return {
columnDefinitions,
filterDefinitions,
sortDefinitions,
};
};

View File

@ -0,0 +1,53 @@
import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { FieldType } from '@/object-record/field/types/FieldType';
import {
CreateOneFieldMetadataItemMutation,
CreateOneFieldMetadataItemMutationVariables,
FieldMetadataType,
} from '~/generated-metadata/graphql';
import { CREATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { useApolloMetadataClient } from './useApolloMetadataClient';
type CreateOneFieldMetadataItemArgs = Omit<
CreateOneFieldMetadataItemMutationVariables['input']['field'],
'type'
> & {
type: FieldType;
};
export const useCreateOneFieldMetadataItem = () => {
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
CreateOneFieldMetadataItemMutation,
CreateOneFieldMetadataItemMutationVariables
>(CREATE_ONE_FIELD_METADATA_ITEM, {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const createOneFieldMetadataItem = async (
input: CreateOneFieldMetadataItemArgs,
) => {
return await mutate({
variables: {
input: {
field: {
...input,
type: input.type as FieldMetadataType, // Todo improve typing once we have aligned backend and frontend
},
},
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
});
};
return {
createOneFieldMetadataItem,
};
};

View File

@ -0,0 +1,43 @@
import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import {
CreateOneObjectMetadataItemMutation,
CreateOneObjectMetadataItemMutationVariables,
} from '~/generated-metadata/graphql';
import { CREATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useCreateOneObjectRecordMetadataItem = () => {
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
CreateOneObjectMetadataItemMutation,
CreateOneObjectMetadataItemMutationVariables
>(CREATE_ONE_OBJECT_METADATA_ITEM, {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const createOneObjectMetadataItem = async (
input: CreateOneObjectMetadataItemMutationVariables['input']['object'],
) => {
return await mutate({
variables: {
input: {
object: {
...input,
},
},
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
});
};
return {
createOneObjectMetadataItem,
};
};

View File

@ -0,0 +1,41 @@
import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import {
CreateOneRelationMetadataMutation,
CreateOneRelationMetadataMutationVariables,
} from '~/generated-metadata/graphql';
import { CREATE_ONE_RELATION_METADATA_ITEM } from '../graphql/mutations';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import {
formatRelationMetadataInput,
FormatRelationMetadataInputParams,
} from '../utils/formatRelationMetadataInput';
import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useCreateOneRelationMetadataItem = () => {
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
CreateOneRelationMetadataMutation,
CreateOneRelationMetadataMutationVariables
>(CREATE_ONE_RELATION_METADATA_ITEM, {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const createOneRelationMetadataItem = async (
input: FormatRelationMetadataInputParams,
) => {
return await mutate({
variables: { input: { relation: formatRelationMetadataInput(input) } },
awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
});
};
return {
createOneRelationMetadataItem,
};
};

View File

@ -0,0 +1,39 @@
import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import {
DeleteOneFieldMetadataItemMutation,
DeleteOneFieldMetadataItemMutationVariables,
} from '~/generated-metadata/graphql';
import { DELETE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useDeleteOneFieldMetadataItem = () => {
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
DeleteOneFieldMetadataItemMutation,
DeleteOneFieldMetadataItemMutationVariables
>(DELETE_ONE_FIELD_METADATA_ITEM, {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const deleteOneFieldMetadataItem = async (
idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete'],
) => {
return await mutate({
variables: {
idToDelete,
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
});
};
return {
deleteOneFieldMetadataItem,
};
};

View File

@ -0,0 +1,39 @@
import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import {
DeleteOneObjectMetadataItemMutation,
DeleteOneObjectMetadataItemMutationVariables,
} from '~/generated-metadata/graphql';
import { DELETE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useDeleteOneObjectMetadataItem = () => {
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
DeleteOneObjectMetadataItemMutation,
DeleteOneObjectMetadataItemMutationVariables
>(DELETE_ONE_OBJECT_METADATA_ITEM, {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const deleteOneObjectMetadataItem = async (
idToDelete: DeleteOneObjectMetadataItemMutationVariables['idToDelete'],
) => {
return await mutate({
variables: {
idToDelete,
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
});
};
return {
deleteOneObjectMetadataItem,
};
};

View File

@ -0,0 +1,72 @@
import { v4 } from 'uuid';
import { FieldType } from '@/object-record/field/types/FieldType';
import { Field } from '~/generated/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { FieldMetadataOption } from '../types/FieldMetadataOption';
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
import { useCreateOneFieldMetadataItem } from './useCreateOneFieldMetadataItem';
import { useDeleteOneFieldMetadataItem } from './useDeleteOneFieldMetadataItem';
import { useUpdateOneFieldMetadataItem } from './useUpdateOneFieldMetadataItem';
export const useFieldMetadataItem = () => {
const { createOneFieldMetadataItem } = useCreateOneFieldMetadataItem();
const { updateOneFieldMetadataItem } = useUpdateOneFieldMetadataItem();
const { deleteOneFieldMetadataItem } = useDeleteOneFieldMetadataItem();
const createMetadataField = (
input: Pick<Field, 'label' | 'icon' | 'description'> & {
objectMetadataId: string;
options?: Omit<FieldMetadataOption, 'id'>[];
type: FieldMetadataType;
},
) =>
createOneFieldMetadataItem({
...formatFieldMetadataItemInput(input),
objectMetadataId: input.objectMetadataId,
type: input.type as FieldType,
});
const editMetadataField = (
input: Pick<Field, 'id' | 'label' | 'icon' | 'description'> & {
options?: FieldMetadataOption[];
},
) =>
updateOneFieldMetadataItem({
fieldMetadataIdToUpdate: input.id,
updatePayload: formatFieldMetadataItemInput({
...input,
// In Edit mode, all options need an id,
// so we generate an id for newly created options.
options: input.options?.map((option) =>
option.id ? option : { ...option, id: v4() },
),
}),
});
const activateMetadataField = (metadataField: FieldMetadataItem) =>
updateOneFieldMetadataItem({
fieldMetadataIdToUpdate: metadataField.id,
updatePayload: { isActive: true },
});
const disableMetadataField = (metadataField: FieldMetadataItem) =>
updateOneFieldMetadataItem({
fieldMetadataIdToUpdate: metadataField.id,
updatePayload: { isActive: false },
});
const eraseMetadataField = (metadataField: FieldMetadataItem) =>
deleteOneFieldMetadataItem(metadataField.id);
return {
activateMetadataField,
createMetadataField,
disableMetadataField,
eraseMetadataField,
editMetadataField,
};
};

View File

@ -0,0 +1,63 @@
import { useMemo } from 'react';
import { useQuery } from '@apollo/client';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import {
FieldFilter,
ObjectFilter,
ObjectMetadataItemsQuery,
ObjectMetadataItemsQueryVariables,
} from '~/generated-metadata/graphql';
import { logError } from '~/utils/logError';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '../utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useFindManyObjectMetadataItems = ({
skip,
objectFilter,
fieldFilter,
}: {
skip?: boolean;
objectFilter?: ObjectFilter;
fieldFilter?: FieldFilter;
} = {}) => {
const apolloMetadataClient = useApolloMetadataClient();
const { enqueueSnackBar } = useSnackBar();
const { data, loading, error } = useQuery<
ObjectMetadataItemsQuery,
ObjectMetadataItemsQueryVariables
>(FIND_MANY_OBJECT_METADATA_ITEMS, {
variables: {
objectFilter,
fieldFilter,
},
client: apolloMetadataClient ?? undefined,
skip: skip || !apolloMetadataClient,
onError: (error) => {
logError('useFindManyObjectMetadataItems error : ' + error);
enqueueSnackBar(
`Error during useFindManyObjectMetadataItems, ${error.message}`,
{
variant: 'error',
},
);
},
});
const objectMetadataItems = useMemo(() => {
return mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: data,
});
}, [data]);
return {
objectMetadataItems,
loading,
error,
};
};

View File

@ -0,0 +1,14 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { getObjectOrderByField } from '@/object-metadata/utils/getObjectOrderByField';
export const useGetObjectOrderByField = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
return (orderBy: OrderBy): OrderByField => {
return getObjectOrderByField(objectMetadataItem, orderBy);
};
};

View File

@ -0,0 +1,108 @@
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { FieldType } from '@/object-record/field/types/FieldType';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
export const useMapFieldMetadataToGraphQLQuery = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const mapFieldMetadataToGraphQLQuery = (
field: FieldMetadataItem,
maxDepthForRelations: number = 2,
): any => {
if (maxDepthForRelations <= 0) {
return '';
}
// TODO: parse
const fieldType = field.type as FieldType;
const fieldIsSimpleValue = (
[
'UUID',
'TEXT',
'PHONE',
'DATE_TIME',
'EMAIL',
'NUMBER',
'BOOLEAN',
] as FieldType[]
).includes(fieldType);
if (fieldIsSimpleValue) {
return field.name;
} else if (
fieldType === 'RELATION' &&
field.toRelationMetadata?.relationType === 'ONE_TO_MANY'
) {
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
(field.toRelationMetadata as any)?.fromObjectMetadata?.id,
);
return `${field.name}
{
id
${(relationMetadataItem?.fields ?? [])
.filter((field) => field.type !== 'RELATION')
.map((field) =>
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
)
.join('\n')}
}`;
} else if (
fieldType === 'RELATION' &&
field.fromRelationMetadata?.relationType === 'ONE_TO_MANY'
) {
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
(field.fromRelationMetadata as any)?.toObjectMetadata?.id,
);
return `${field.name}
{
edges {
node {
id
${(relationMetadataItem?.fields ?? [])
.filter((field) => field.type !== 'RELATION')
.map((field) =>
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
)
.join('\n')}
}
}
}`;
} else if (fieldType === 'LINK') {
return `
${field.name}
{
label
url
}
`;
} else if (fieldType === 'CURRENCY') {
return `
${field.name}
{
amountMicros
currencyCode
}
`;
} else if (fieldType === 'FULL_NAME') {
return `
${field.name}
{
firstName
lastName
}
`;
}
};
return mapFieldMetadataToGraphQLQuery;
};

View File

@ -0,0 +1,69 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
export const useMapToObjectRecordIdentifier = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
return (record: any): ObjectRecordIdentifier => {
switch (objectMetadataItem.nameSingular) {
case CoreObjectNameSingular.Opportunity:
return {
id: record.id,
name: record?.company?.name,
avatarUrl: record.avatarUrl,
avatarType: 'rounded',
};
}
const labelIdentifierFieldMetadata = objectMetadataItem.fields.find(
(field) =>
field.id === objectMetadataItem.labelIdentifierFieldMetadataId ||
field.name === 'name',
);
let labelIdentifierFieldValue = '';
switch (labelIdentifierFieldMetadata?.type) {
case FieldMetadataType.FullName: {
labelIdentifierFieldValue = `${record.name?.firstName ?? ''} ${
record.name?.lastName ?? ''
}`;
break;
}
default:
labelIdentifierFieldValue = labelIdentifierFieldMetadata
? record[labelIdentifierFieldMetadata.name]
: '';
}
const imageIdentifierFieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === objectMetadataItem.imageIdentifierFieldMetadataId,
);
const imageIdentifierFieldValue = imageIdentifierFieldMetadata
? (record[imageIdentifierFieldMetadata.name] as string)
: null;
const avatarType =
objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
? 'squared'
: 'rounded';
const avatarUrl =
objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
? getLogoUrlFromDomainName(imageIdentifierFieldValue ?? '')
: imageIdentifierFieldValue ?? null;
return {
id: record.id,
name: labelIdentifierFieldValue,
avatarUrl,
avatarType,
};
};
};

View File

@ -0,0 +1,130 @@
import { gql } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
import { useGetObjectOrderByField } from '@/object-metadata/hooks/useGetObjectOrderByField';
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery';
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
import { useGenerateUpdateOneRecordMutation } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
import { useGetRecordFromCache } from '@/object-record/hooks/useGetRecordFromCache';
import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache';
import { generateDeleteOneRecordMutation } from '@/object-record/utils/generateDeleteOneRecordMutation';
import { isDefined } from '~/utils/isDefined';
import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier';
export const EMPTY_QUERY = gql`
query EmptyQuery {
empty
}
`;
export const EMPTY_MUTATION = gql`
mutation EmptyMutation {
empty
}
`;
export const useObjectMetadataItem = (
{ objectNameSingular }: ObjectMetadataItemIdentifier,
depth?: number,
) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
objectMetadataItemFamilySelector({
objectName: objectNameSingular,
objectNameType: 'singular',
}),
);
let objectMetadataItems = useRecoilValue(objectMetadataItemsState);
if (!currentWorkspace) {
objectMetadataItem =
mockObjectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
) ?? null;
objectMetadataItems = mockObjectMetadataItems;
}
if (!isDefined(objectMetadataItem)) {
throw new ObjectMetadataItemNotFoundError(
objectNameSingular,
objectMetadataItems,
);
}
const mapToObjectRecordIdentifier = useMapToObjectRecordIdentifier({
objectMetadataItem,
});
const getObjectOrderByField = useGetObjectOrderByField({
objectMetadataItem,
});
const getRecordFromCache = useGetRecordFromCache({
objectMetadataItem,
});
const modifyRecordFromCache = useModifyRecordFromCache({
objectMetadataItem,
});
const findManyRecordsQuery = useGenerateFindManyRecordsQuery({
objectMetadataItem,
depth,
});
const findOneRecordQuery = useGenerateFindOneRecordQuery({
objectMetadataItem,
depth,
});
const createOneRecordMutation = useGenerateCreateOneRecordMutation({
objectMetadataItem,
});
const createManyRecordsMutation = useGenerateCreateManyRecordMutation({
objectMetadataItem,
});
const updateOneRecordMutation = useGenerateUpdateOneRecordMutation({
objectMetadataItem,
});
const deleteOneRecordMutation = generateDeleteOneRecordMutation({
objectMetadataItem,
});
const labelIdentifierFieldMetadataId = objectMetadataItem.fields.find(
({ name }) => name === 'name',
)?.id;
const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`;
return {
labelIdentifierFieldMetadataId,
basePathToShowPage,
objectMetadataItem,
getRecordFromCache,
modifyRecordFromCache,
findManyRecordsQuery,
findOneRecordQuery,
createOneRecordMutation,
updateOneRecordMutation,
deleteOneRecordMutation,
createManyRecordsMutation,
mapToObjectRecordIdentifier,
getObjectOrderByField,
};
};

View File

@ -0,0 +1,95 @@
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
import { formatObjectMetadataItemInput } from '../utils/formatObjectMetadataItemInput';
import { getObjectSlug } from '../utils/getObjectSlug';
import { useCreateOneObjectRecordMetadataItem } from './useCreateOneObjectMetadataItem';
import { useDeleteOneObjectMetadataItem } from './useDeleteOneObjectMetadataItem';
import { useUpdateOneObjectMetadataItem } from './useUpdateOneObjectMetadataItem';
export const useObjectMetadataItemForSettings = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const activeObjectMetadataItems = objectMetadataItems.filter(
({ isActive, isSystem }) => isActive && !isSystem,
);
const disabledObjectMetadataItems = objectMetadataItems.filter(
({ isActive, isSystem }) => !isActive && !isSystem,
);
const findActiveObjectMetadataItemBySlug = (slug: string) =>
activeObjectMetadataItems.find(
(activeObjectMetadataItem) =>
getObjectSlug(activeObjectMetadataItem) === slug,
);
const findObjectMetadataItemById = (id: string) =>
objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.id === id,
);
const findObjectMetadataItemByNamePlural = (namePlural: string) =>
objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.namePlural === namePlural,
);
const { createOneObjectMetadataItem } =
useCreateOneObjectRecordMetadataItem();
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
const createObjectMetadataItem = (
input: Pick<
ObjectMetadataItem,
'labelPlural' | 'labelSingular' | 'icon' | 'description'
>,
) => createOneObjectMetadataItem(formatObjectMetadataItemInput(input));
const editObjectMetadataItem = (
input: Pick<
ObjectMetadataItem,
'id' | 'labelPlural' | 'labelSingular' | 'icon' | 'description'
>,
) =>
updateOneObjectMetadataItem({
idToUpdate: input.id,
updatePayload: formatObjectMetadataItemInput(input),
});
const activateObjectMetadataItem = (
objectMetadataItem: Pick<ObjectMetadataItem, 'id'>,
) =>
updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: { isActive: true },
});
const disableObjectMetadataItem = (
objectMetadataItem: Pick<ObjectMetadataItem, 'id'>,
) =>
updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: { isActive: false },
});
const eraseObjectMetadataItem = (
objectMetadataItem: Pick<ObjectMetadataItem, 'id'>,
) => deleteOneObjectMetadataItem(objectMetadataItem.id);
return {
activateObjectMetadataItem,
activeObjectMetadataItems,
createObjectMetadataItem,
disabledObjectMetadataItems,
disableObjectMetadataItem,
editObjectMetadataItem,
eraseObjectMetadataItem,
findActiveObjectMetadataItemBySlug,
findObjectMetadataItemById,
findObjectMetadataItemByNamePlural,
objectMetadataItems,
};
};

View File

@ -0,0 +1,38 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { isDefined } from '~/utils/isDefined';
export const useObjectNamePluralFromSingular = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
objectMetadataItemFamilySelector({
objectName: objectNameSingular,
objectNameType: 'singular',
}),
);
if (!currentWorkspace) {
objectMetadataItem =
mockObjectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
) ?? null;
}
if (!isDefined(objectMetadataItem)) {
throw new Error(
`Object metadata item not found for ${objectNameSingular} object`,
);
}
return { objectNamePlural: objectMetadataItem.namePlural };
};

View File

@ -0,0 +1,38 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { isDefined } from '~/utils/isDefined';
export const useObjectNameSingularFromPlural = ({
objectNamePlural,
}: {
objectNamePlural: string;
}) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
let objectMetadataItem = useRecoilValue(
objectMetadataItemFamilySelector({
objectName: objectNamePlural,
objectNameType: 'plural',
}),
);
if (!currentWorkspace) {
objectMetadataItem =
mockObjectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.namePlural === objectNamePlural,
) ?? null;
}
if (!isDefined(objectMetadataItem)) {
throw new Error(
`Object metadata item not found for ${objectNamePlural} object`,
);
}
return { objectNameSingular: objectMetadataItem.nameSingular };
};

View File

@ -0,0 +1,49 @@
import { RelationType } from '@/settings/data-model/types/RelationType';
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { useObjectMetadataItemForSettings } from '../hooks/useObjectMetadataItemForSettings';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
export const useRelationMetadata = ({
fieldMetadataItem,
}: {
fieldMetadataItem?: FieldMetadataItem;
}) => {
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
const relationMetadata =
fieldMetadataItem?.fromRelationMetadata ||
fieldMetadataItem?.toRelationMetadata;
const relationType =
relationMetadata?.relationType === RelationMetadataType.OneToMany &&
fieldMetadataItem?.toRelationMetadata
? 'MANY_TO_ONE'
: (relationMetadata?.relationType as RelationType | undefined);
const relationObjectMetadataId =
relationMetadata && 'toObjectMetadata' in relationMetadata
? relationMetadata.toObjectMetadata.id
: relationMetadata?.fromObjectMetadata.id;
const relationObjectMetadataItem = relationObjectMetadataId
? findObjectMetadataItemById(relationObjectMetadataId)
: undefined;
const relationFieldMetadataId =
relationMetadata && 'toFieldMetadataId' in relationMetadata
? relationMetadata.toFieldMetadataId
: relationMetadata?.fromFieldMetadataId;
const relationFieldMetadataItem = relationFieldMetadataId
? relationObjectMetadataItem?.fields?.find(
(field) => field.id === relationFieldMetadataId,
)
: undefined;
return {
relationFieldMetadataItem,
relationObjectMetadataItem,
relationType,
};
};

View File

@ -0,0 +1,50 @@
import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import {
UpdateOneFieldMetadataItemMutation,
UpdateOneFieldMetadataItemMutationVariables,
} from '~/generated-metadata/graphql';
import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useUpdateOneFieldMetadataItem = () => {
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
UpdateOneFieldMetadataItemMutation,
UpdateOneFieldMetadataItemMutationVariables
>(UPDATE_ONE_FIELD_METADATA_ITEM, {
client: apolloMetadataClient ?? undefined,
});
const updateOneFieldMetadataItem = async ({
fieldMetadataIdToUpdate,
updatePayload,
}: {
fieldMetadataIdToUpdate: UpdateOneFieldMetadataItemMutationVariables['idToUpdate'];
updatePayload: Pick<
UpdateOneFieldMetadataItemMutationVariables['updatePayload'],
'description' | 'icon' | 'isActive' | 'label' | 'name'
>;
}) => {
return await mutate({
variables: {
idToUpdate: fieldMetadataIdToUpdate,
updatePayload: {
...updatePayload,
label: updatePayload.label ?? undefined,
},
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
});
};
return {
updateOneFieldMetadataItem,
};
};

View File

@ -0,0 +1,54 @@
import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import {
UpdateOneObjectMetadataItemMutation,
UpdateOneObjectMetadataItemMutationVariables,
} from '~/generated-metadata/graphql';
import { UPDATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { useApolloMetadataClient } from './useApolloMetadataClient';
// TODO: Slice the Apollo store synchronously in the update function instead of subscribing, so we can use update after read in the same function call
export const useUpdateOneObjectMetadataItem = () => {
const apolloClientMetadata = useApolloMetadataClient();
const [mutate] = useMutation<
UpdateOneObjectMetadataItemMutation,
UpdateOneObjectMetadataItemMutationVariables
>(UPDATE_ONE_OBJECT_METADATA_ITEM, {
client: apolloClientMetadata ?? undefined,
});
const updateOneObjectMetadataItem = async ({
idToUpdate,
updatePayload,
}: {
idToUpdate: UpdateOneObjectMetadataItemMutationVariables['idToUpdate'];
updatePayload: Pick<
UpdateOneObjectMetadataItemMutationVariables['updatePayload'],
| 'description'
| 'icon'
| 'isActive'
| 'labelPlural'
| 'labelSingular'
| 'namePlural'
| 'nameSingular'
>;
}) => {
return await mutate({
variables: {
idToUpdate,
updatePayload,
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
});
};
return {
updateOneObjectMetadataItem,
};
};

View File

@ -0,0 +1,39 @@
import { selectorFamily } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
type ObjectMetadataItemSelector = {
objectName: string;
objectNameType: 'singular' | 'plural';
};
export const objectMetadataItemFamilySelector = selectorFamily<
ObjectMetadataItem | null,
ObjectMetadataItemSelector
>({
key: 'objectMetadataItemFamilySelector',
get:
({ objectNameType, objectName }: ObjectMetadataItemSelector) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
if (objectNameType === 'singular') {
return (
objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectName,
) ?? null
);
} else if (objectNameType === 'plural') {
return (
objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.namePlural === objectName,
) ?? null
);
}
return null;
},
});

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const objectMetadataItemsState = atom<ObjectMetadataItem[]>({
key: 'objectMetadataItemsState',
default: [],
});

View File

@ -0,0 +1,6 @@
export enum CoreObjectNameSingular {
Company = 'company',
Person = 'person',
WorkspaceMember = 'workspaceMember',
Opportunity = 'opportunity',
}

View File

@ -0,0 +1,32 @@
import { ThemeColor } from '@/ui/theme/constants/colors';
import { Field, Relation } from '~/generated-metadata/graphql';
export type FieldMetadataItem = Omit<
Field,
'fromRelationMetadata' | 'toRelationMetadata' | 'defaultValue' | 'options'
> & {
fromRelationMetadata?:
| (Pick<Relation, 'id' | 'toFieldMetadataId' | 'relationType'> & {
toObjectMetadata: Pick<
Relation['toObjectMetadata'],
'id' | 'nameSingular' | 'namePlural'
>;
})
| null;
toRelationMetadata?:
| (Pick<Relation, 'id' | 'fromFieldMetadataId' | 'relationType'> & {
fromObjectMetadata: Pick<
Relation['fromObjectMetadata'],
'id' | 'nameSingular' | 'namePlural'
>;
})
| null;
defaultValue?: string;
options?: {
color: ThemeColor;
id: string;
label: string;
position: number;
value: string;
}[];
};

View File

@ -0,0 +1,8 @@
import { ThemeColor } from '@/ui/theme/constants/colors';
export type FieldMetadataOption = {
color?: ThemeColor;
id?: string;
isDefault?: boolean;
label: string;
};

View File

@ -0,0 +1,10 @@
import { Object as GeneratedObject } from '~/generated-metadata/graphql';
import { FieldMetadataItem } from './FieldMetadataItem';
export type ObjectMetadataItem = Omit<
GeneratedObject,
'fields' | 'dataSourceId'
> & {
fields: FieldMetadataItem[];
};

View File

@ -0,0 +1,3 @@
export type ObjectMetadataItemIdentifier = {
objectNameSingular: string;
};

View File

@ -0,0 +1,5 @@
export type OrderBy =
| 'AscNullsLast'
| 'DescNullsLast'
| 'AscNullsFirst'
| 'DescNullsFirst';

View File

@ -0,0 +1,5 @@
import { OrderBy } from '@/object-metadata/types/OrderBy';
export type OrderByField = {
[fieldName: string]: OrderBy | { [subFieldName: string]: OrderBy };
};

View File

@ -0,0 +1,41 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { parseFieldType } from './parseFieldType';
export const formatFieldMetadataItemAsColumnDefinition = ({
position,
field,
objectMetadataItem,
}: {
position: number;
field: FieldMetadataItem;
objectMetadataItem: ObjectMetadataItem;
}): ColumnDefinition<FieldMetadata> => {
const relationObjectMetadataItem =
field.toRelationMetadata?.fromObjectMetadata;
return {
position,
fieldMetadataId: field.id,
label: field.label,
size: 100,
type: parseFieldType(field.type),
metadata: {
fieldName: field.name,
placeHolder: field.label,
relationType: parseFieldRelationType(field),
relationObjectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular ?? '',
relationObjectMetadataNamePlural:
relationObjectMetadataItem?.namePlural ?? '',
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
},
iconName: field.icon ?? 'Icon123',
isVisible: true,
};
};

View File

@ -0,0 +1,34 @@
import toCamelCase from 'lodash.camelcase';
import toSnakeCase from 'lodash.snakecase';
import { Field } from '~/generated-metadata/graphql';
import { FieldMetadataOption } from '../types/FieldMetadataOption';
const getOptionValueFromLabel = (label: string) =>
toSnakeCase(label.trim()).toUpperCase();
export const formatFieldMetadataItemInput = (
input: Pick<Field, 'label' | 'icon' | 'description' | 'defaultValue'> & {
options?: FieldMetadataOption[];
},
) => {
const defaultOption = input.options?.find((option) => option.isDefault);
return {
defaultValue: defaultOption
? getOptionValueFromLabel(defaultOption.label)
: undefined,
description: input.description?.trim() ?? null,
icon: input.icon,
label: input.label.trim(),
name: toCamelCase(input.label.trim()),
options: input.options?.map((option, index) => ({
color: option.color,
id: option.id,
label: option.label.trim(),
position: index,
value: getOptionValueFromLabel(option.label),
})),
};
};

View File

@ -0,0 +1,71 @@
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const formatFieldMetadataItemsAsFilterDefinitions = ({
fields,
}: {
fields: Array<ObjectMetadataItem['fields'][0]>;
}): FilterDefinition[] =>
fields.reduce((acc, field) => {
if (
![
FieldMetadataType.DateTime,
FieldMetadataType.Text,
FieldMetadataType.Email,
FieldMetadataType.Number,
FieldMetadataType.Link,
FieldMetadataType.FullName,
FieldMetadataType.Relation,
FieldMetadataType.Currency,
].includes(field.type)
) {
return acc;
}
// Todo: remove once Rating fieldtype is implemented
if (field.name === 'probability') {
return acc;
}
if (field.type === FieldMetadataType.Relation) {
if (field.fromRelationMetadata) {
return acc;
}
}
return [...acc, formatFieldMetadataItemAsFilterDefinition({ field })];
}, [] as FilterDefinition[]);
const formatFieldMetadataItemAsFilterDefinition = ({
field,
}: {
field: ObjectMetadataItem['fields'][0];
}): FilterDefinition => ({
fieldMetadataId: field.id,
label: field.label,
iconName: field.icon ?? 'Icon123',
relationObjectMetadataNamePlural:
field.toRelationMetadata?.fromObjectMetadata.namePlural,
relationObjectMetadataNameSingular:
field.toRelationMetadata?.fromObjectMetadata.nameSingular,
type:
field.type === FieldMetadataType.DateTime
? 'DATE_TIME'
: field.type === FieldMetadataType.Link
? 'LINK'
: field.type === FieldMetadataType.FullName
? 'FULL_NAME'
: field.type === FieldMetadataType.Number
? 'NUMBER'
: field.type === FieldMetadataType.Currency
? 'CURRENCY'
: field.type === FieldMetadataType.Email
? 'TEXT'
: field.type === FieldMetadataType.Phone
? 'TEXT'
: field.type === FieldMetadataType.Relation
? 'RELATION'
: 'TEXT',
});

View File

@ -0,0 +1,31 @@
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const formatFieldMetadataItemsAsSortDefinitions = ({
fields,
}: {
fields: Array<ObjectMetadataItem['fields'][0]>;
}): SortDefinition[] =>
fields.reduce((acc, field) => {
if (
![
FieldMetadataType.DateTime,
FieldMetadataType.Number,
FieldMetadataType.Text,
FieldMetadataType.Boolean,
].includes(field.type)
) {
return acc;
}
return [
...acc,
{
fieldMetadataId: field.id,
label: field.label,
iconName: field.icon ?? 'Icon123',
},
];
}, [] as SortDefinition[]);

View File

@ -0,0 +1,17 @@
import toCamelCase from 'lodash.camelcase';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const formatObjectMetadataItemInput = (
input: Pick<
ObjectMetadataItem,
'labelPlural' | 'labelSingular' | 'icon' | 'description'
>,
) => ({
description: input.description?.trim() ?? null,
icon: input.icon,
labelPlural: input.labelPlural.trim(),
labelSingular: input.labelSingular.trim(),
namePlural: toCamelCase(input.labelPlural.trim()),
nameSingular: toCamelCase(input.labelSingular.trim()),
});

View File

@ -0,0 +1,59 @@
import { RelationType } from '@/settings/data-model/types/RelationType';
import {
CreateRelationInput,
Field,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { formatFieldMetadataItemInput } from './formatFieldMetadataItemInput';
export type FormatRelationMetadataInputParams = {
relationType: RelationType;
field: Pick<Field, 'label' | 'icon' | 'description'>;
objectMetadataId: string;
connect: {
field: Pick<Field, 'label' | 'icon'>;
objectMetadataId: string;
};
};
export const formatRelationMetadataInput = (
input: FormatRelationMetadataInputParams,
): CreateRelationInput => {
// /!\ MANY_TO_ONE does not exist on backend.
// => Transform into ONE_TO_MANY and invert "from" and "to" data.
const isManyToOne = input.relationType === 'MANY_TO_ONE';
const relationType = isManyToOne
? RelationMetadataType.OneToMany
: (input.relationType as RelationMetadataType);
const { field: fromField, objectMetadataId: fromObjectMetadataId } =
isManyToOne ? input.connect : input;
const { field: toField, objectMetadataId: toObjectMetadataId } = isManyToOne
? input
: input.connect;
const {
description,
icon: fromIcon,
label: fromLabel,
name: fromName,
} = formatFieldMetadataItemInput(fromField);
const {
icon: toIcon,
label: toLabel,
name: toName,
} = formatFieldMetadataItemInput(toField);
return {
description,
fromIcon,
fromLabel,
fromName,
fromObjectMetadataId,
relationType,
toIcon,
toLabel,
toName,
toObjectMetadataId,
};
};

View File

@ -0,0 +1,6 @@
import toKebabCase from 'lodash.kebabcase';
import { Field } from '~/generated-metadata/graphql';
export const getFieldSlug = (metadataField: Pick<Field, 'label'>) =>
toKebabCase(metadataField.label);

View File

@ -0,0 +1,35 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getObjectOrderByField = (
objectMetadataItem: ObjectMetadataItem,
orderBy: OrderBy,
): OrderByField => {
const labelIdentifierFieldMetadata = objectMetadataItem.fields.find(
(field) =>
field.id === objectMetadataItem.labelIdentifierFieldMetadataId ||
field.name === 'name',
);
if (labelIdentifierFieldMetadata) {
switch (labelIdentifierFieldMetadata.type) {
case FieldMetadataType.FullName:
return {
[labelIdentifierFieldMetadata.name]: {
firstName: orderBy,
lastName: orderBy,
},
};
default:
return {
[labelIdentifierFieldMetadata.name]: orderBy,
};
}
} else {
return {
createdAt: orderBy,
};
}
};

View File

@ -0,0 +1,7 @@
import toKebabCase from 'lodash.kebabcase';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const getObjectSlug = (
objectMetadataItem: Pick<ObjectMetadataItem, 'labelPlural'>,
) => toKebabCase(objectMetadataItem.labelPlural);

View File

@ -0,0 +1,17 @@
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
pagedObjectMetadataItems,
}: {
pagedObjectMetadataItems: ObjectMetadataItemsQuery | undefined;
}) => {
const formattedObjects: ObjectMetadataItem[] =
pagedObjectMetadataItems?.objects.edges.map((object) => ({
...object.node,
fields: object.node.fields.edges.map((field) => field.node),
})) ?? [];
return formattedObjects;
};

View File

@ -0,0 +1,51 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldDefinitionRelationType } from '@/object-record/field/types/FieldDefinition';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
export const parseFieldRelationType = (
field: FieldMetadataItem | undefined,
): FieldDefinitionRelationType | undefined => {
if (!field || field.type !== FieldMetadataType.Relation) return;
const config: Record<
RelationMetadataType,
{ from: FieldDefinitionRelationType; to: FieldDefinitionRelationType }
> = {
[RelationMetadataType.ManyToMany]: {
from: 'FROM_MANY_OBJECTS',
to: 'TO_MANY_OBJECTS',
},
[RelationMetadataType.OneToMany]: {
from: 'FROM_MANY_OBJECTS',
to: 'TO_ONE_OBJECT',
},
[RelationMetadataType.OneToOne]: {
from: 'FROM_ONE_OBJECT',
to: 'TO_ONE_OBJECT',
},
};
if (
isDefined(field.fromRelationMetadata) &&
field.fromRelationMetadata.relationType in config
) {
return config[field.fromRelationMetadata.relationType].from;
}
if (
isDefined(field.toRelationMetadata) &&
field.toRelationMetadata.relationType in config
) {
return config[field.toRelationMetadata.relationType].to;
}
throw new Error(
`Cannot determine field relation type for field : ${JSON.stringify(
field,
)}.`,
);
};

View File

@ -0,0 +1,14 @@
import { FieldType } from '@/object-record/field/types/FieldType';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const parseFieldType = (fieldType: FieldMetadataType): FieldType => {
if (fieldType === FieldMetadataType.Link) {
return 'LINK';
}
if (fieldType === FieldMetadataType.Currency) {
return 'CURRENCY';
}
return fieldType as FieldType;
};

View File

@ -0,0 +1,4 @@
const metadataLabelValidationPattern = /^[a-zA-Z][a-zA-Z0-9 ]*$/;
export const validateMetadataLabel = (value: string) =>
!!value.match(metadataLabelValidationPattern);