Setup relations for remote objects (#5149)

New strategy:
- add settings field on FieldMetadata. Contains a boolean isIdField and
for numbers, a precision
- if idField, the graphql scalar returned will be a GraphQL id. This
will allow the app to work even for ids that are not uuid
- remove globals dateScalar and numberScalar modes. These were not used
- set limit as Integer
- check manually in query runner mutations that we send a valid id

Todo left:
- remove WorkspaceBuildSchemaOptions since this is not used anymore.
Will do in another PR

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Thomas Trompette
2024-04-26 14:37:34 +02:00
committed by GitHub
parent dc576d0818
commit 224c8d361b
71 changed files with 616 additions and 223 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -861,6 +861,7 @@ export type Field = {
name: Scalars['String'];
options?: Maybe<Scalars['JSON']>;
relationDefinition?: Maybe<RelationDefinition>;
settings?: Maybe<Scalars['JSON']>;
toRelationMetadata?: Maybe<Relation>;
type: FieldMetadataType;
updatedAt: Scalars['DateTime'];

View File

@ -51,7 +51,7 @@ const mocks: MockedResponse[] = [
$filter: ActivityTargetFilterInput
$orderBy: ActivityTargetOrderByInput
$lastCursor: String
$limit: Float
$limit: Int
) {
activityTargets(
filter: $filter
@ -105,7 +105,7 @@ const mocks: MockedResponse[] = [
$filter: ActivityFilterInput
$orderBy: ActivityOrderByInput
$lastCursor: String
$limit: Float
$limit: Int
) {
activities(
filter: $filter

View File

@ -36,7 +36,7 @@ const mocks: MockedResponse[] = [
$filter: ActivityTargetFilterInput
$orderBy: ActivityTargetOrderByInput
$lastCursor: String
$limit: Float
$limit: Int
) {
activityTargets(
filter: $filter

View File

@ -16,7 +16,7 @@ const mocks: MockedResponse[] = [
{
request: {
query: gql`
query FindOneWorkspaceMember($objectRecordId: UUID!) {
query FindOneWorkspaceMember($objectRecordId: ID!) {
workspaceMember(filter: { id: { eq: $objectRecordId } }) {
__typename
colorScheme

View File

@ -17,7 +17,7 @@ const mocks: MockedResponse[] = [
request: {
query: gql`
mutation UpdateOneActivity(
$idToUpdate: UUID!
$idToUpdate: ID!
$input: ActivityUpdateInput!
) {
updateActivity(id: $idToUpdate, data: $input) {

View File

@ -177,7 +177,7 @@ export const mocks = [
{
request: {
query: gql`
mutation DeleteOneFavorite($idToDelete: UUID!) {
mutation DeleteOneFavorite($idToDelete: ID!) {
deleteFavorite(id: $idToDelete) {
id
}
@ -197,7 +197,7 @@ export const mocks = [
request: {
query: gql`
mutation UpdateOneFavorite(
$idToUpdate: UUID!
$idToUpdate: ID!
$input: FavoriteUpdateInput!
) {
updateFavorite(id: $idToUpdate, data: $input) {

View File

@ -48,6 +48,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
nameSingular
namePlural
isSystem
isRemote
}
toFieldMetadataId
}
@ -60,6 +61,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
nameSingular
namePlural
isSystem
isRemote
}
fromFieldMetadataId
}

View File

@ -26,7 +26,7 @@ export const findManyViewsQuery = gql`
$filter: ViewFilterInput
$orderBy: ViewOrderByInput
$lastCursor: String
$limit: Float
$limit: Int
) {
views(
filter: $filter

View File

@ -21,6 +21,7 @@ export type FieldMetadataItem = Omit<
| 'toRelationMetadata'
| 'defaultValue'
| 'options'
| 'settings'
| 'relationDefinition'
> & {
__typename?: string;
@ -28,7 +29,7 @@ export type FieldMetadataItem = Omit<
| (Pick<Relation, 'id' | 'toFieldMetadataId' | 'relationType'> & {
toObjectMetadata: Pick<
Relation['toObjectMetadata'],
'id' | 'nameSingular' | 'namePlural' | 'isSystem'
'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
>;
})
| null;
@ -36,7 +37,7 @@ export type FieldMetadataItem = Omit<
| (Pick<Relation, 'id' | 'fromFieldMetadataId' | 'relationType'> & {
fromObjectMetadata: Pick<
Relation['fromObjectMetadata'],
'id' | 'nameSingular' | 'namePlural' | 'isSystem'
'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
>;
})
| null;

View File

@ -2,10 +2,15 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const isObjectMetadataAvailableForRelation = (
objectMetadataItem: Pick<ObjectMetadataItem, 'isSystem' | 'nameSingular'>,
objectMetadataItem: Pick<
ObjectMetadataItem,
'isSystem' | 'nameSingular' | 'isRemote'
>,
) => {
return (
!objectMetadataItem.isSystem ||
objectMetadataItem.nameSingular === CoreObjectNameSingular.WorkspaceMember
(!objectMetadataItem.isSystem ||
objectMetadataItem.nameSingular ===
CoreObjectNameSingular.WorkspaceMember) &&
!objectMetadataItem.isRemote
);
};

View File

@ -1,8 +1,6 @@
import { useContext } from 'react';
import { EntityChip, EntityChipVariant } from 'twenty-ui';
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
@ -25,16 +23,8 @@ export const RecordChip = ({
objectNameSingular,
});
// Will only exists if the chip is inside a record table.
// This is temporary until we have the show page for remote objects.
const { isReadOnly } = useContext(RecordTableRowContext);
const objectRecordIdentifier = mapToObjectRecordIdentifier(record);
const linkToEntity = isReadOnly
? undefined
: objectRecordIdentifier.linkToShowPage;
return (
<EntityChip
entityId={record.id}
@ -43,7 +33,7 @@ export const RecordChip = ({
avatarUrl={
getImageAbsoluteURIOrBase64(objectRecordIdentifier.avatarUrl) || ''
}
linkToEntity={linkToEntity}
linkToEntity={objectRecordIdentifier.linkToShowPage}
maxWidth={maxWidth}
className={className}
variant={variant}

View File

@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
export const query = gql`
mutation DeleteOnePerson($idToDelete: UUID!) {
mutation DeleteOnePerson($idToDelete: ID!) {
deletePerson(id: $idToDelete) {
id
}

View File

@ -3,7 +3,7 @@ import { gql } from '@apollo/client';
export { responseData } from './useUpdateOneRecord';
export const query = gql`
mutation ExecuteQuickActionOnOnePerson($idToExecuteQuickActionOn: UUID!) {
mutation ExecuteQuickActionOnOnePerson($idToExecuteQuickActionOn: ID!) {
executeQuickActionOnPerson(id: $idToExecuteQuickActionOn) {
__typename
xLink {

View File

@ -5,7 +5,7 @@ export const query = gql`
$filter: PersonFilterInput
$orderBy: PersonOrderByInput
$lastCursor: String
$limit: Float
$limit: Int
) {
people(
filter: $filter

View File

@ -3,7 +3,7 @@ import { gql } from '@apollo/client';
import { responseData as person } from './useUpdateOneRecord';
export const query = gql`
query FindOnePerson($objectRecordId: UUID!) {
query FindOnePerson($objectRecordId: ID!) {
person(filter: { id: { eq: $objectRecordId } }) {
__typename
xLink {

View File

@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
export const query = gql`
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) {
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
updatePerson(id: $idToUpdate, data: $input) {
__typename
xLink {

View File

@ -26,7 +26,7 @@ export const useDeleteOneRecordMutation = ({
);
const deleteOneRecordMutation = gql`
mutation DeleteOne${capitalizedObjectName}($idToDelete: UUID!) {
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
${mutationResponseField}(id: $idToDelete) {
id
}

View File

@ -39,7 +39,7 @@ export const useExecuteQuickActionOnOneRecordMutation = ({
});
const executeQuickActionOnOneRecordMutation = gql`
mutation ExecuteQuickActionOnOne${capitalizedObjectName}($idToExecuteQuickActionOn: UUID!) {
mutation ExecuteQuickActionOnOne${capitalizedObjectName}($idToExecuteQuickActionOn: ID!) {
${graphQLFieldForExecuteQuickActionOnOneRecordMutation}(id: $idToExecuteQuickActionOn) ${mapObjectMetadataToGraphQLQuery(
{
objectMetadataItems,

View File

@ -23,7 +23,7 @@ export const useFindDuplicateRecordsQuery = ({
const findDuplicateRecordsQuery = gql`
query FindDuplicate${capitalize(
objectMetadataItem.nameSingular,
)}($id: UUID) {
)}($id: ID!) {
${getFindDuplicateRecordsQueryResponseField(
objectMetadataItem.nameSingular,
)}(id: $id) {

View File

@ -22,7 +22,7 @@ export const useFindOneRecordQuery = ({
const findOneRecordQuery = gql`
query FindOne${capitalize(
objectMetadataItem.nameSingular,
)}($objectRecordId: UUID!) {
)}($objectRecordId: ID!) {
${objectMetadataItem.nameSingular}(filter: {
id: {
eq: $objectRecordId
@ -32,7 +32,7 @@ export const useFindOneRecordQuery = ({
objectMetadataItem,
depth,
})}
}
},
`;
return {

View File

@ -35,7 +35,7 @@ export const useUpdateOneRecordMutation = ({
);
const updateOneRecordMutation = gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!) {
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery(
{
objectMetadataItems,

View File

@ -47,7 +47,7 @@ export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({
const limitPerMetadataItemArray = capitalizedObjectNameSingulars
.map(
(capitalizedObjectNameSingular) =>
`$limit${capitalizedObjectNameSingular}: Float`,
`$limit${capitalizedObjectNameSingular}: Int`,
)
.join(', ');

View File

@ -21,7 +21,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
const query = gql`
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) {
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
updatePerson(id: $idToUpdate, data: $input) {
__typename
xLink {

View File

@ -21,7 +21,7 @@ const mocks: MockedResponse[] = [
request: {
query: gql`
mutation UpdateOneCompany(
$idToUpdate: UUID!
$idToUpdate: ID!
$input: CompanyUpdateInput!
) {
updateCompany(id: $idToUpdate, data: $input) {

View File

@ -8,7 +8,7 @@ import { isFieldDateValue } from '@/object-record/record-field/types/guards/isFi
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';

View File

@ -13,7 +13,7 @@ const query = gql`
$filterNameSingular: NameSingularFilterInput
$orderByNameSingular: NameSingularOrderByInput
$lastCursorNameSingular: String
$limitNameSingular: Float
$limitNameSingular: Int
) {
namePlural(
filter: $filterNameSingular

View File

@ -24,7 +24,7 @@ query FindMany${capitalize(
objectMetadataItem.nameSingular,
)}FilterInput, $orderBy: ${capitalize(
objectMetadataItem.nameSingular,
)}OrderByInput, $lastCursor: String, $limit: Float) {
)}OrderByInput, $lastCursor: String, $limit: Int) {
${
objectMetadataItem.namePlural
}(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){

View File

@ -5,7 +5,7 @@ export const query = gql`
$filter: PersonFilterInput
$orderBy: PersonOrderByInput
$lastCursor: String
$limit: Float = 60
$limit: Int = 60
) {
people(
filter: $filter

View File

@ -65,7 +65,12 @@ export const ShowPageRightContainer = ({
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
const activeTabId = useRecoilValue(activeTabIdState);
const shouldDisplayCalendarTab = useIsFeatureEnabled('IS_CALENDAR_ENABLED');
const shouldDisplayCalendarTab =
useIsFeatureEnabled('IS_CALENDAR_ENABLED') &&
(targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Company ||
targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Person);
const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED');
const shouldDisplayEmailsTab =

View File

@ -96,11 +96,6 @@ export const RecordShowPage = () => {
? `${pageName} - ${capitalize(objectNameSingular)}`
: capitalize(objectNameSingular);
// Temporarily since we don't have relations for remote objects yet
if (objectMetadataItem.isRemote) {
return null;
}
return (
<PageContainer>
<PageTitle title={pageTitle} />

View File

@ -88,6 +88,8 @@ export const SettingsObjectDetail = () => {
},
});
const shouldDisplayAddFieldButton = !activeObjectMetadataItem.isRemote;
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
@ -207,21 +209,23 @@ export const SettingsObjectDetail = () => {
</TableSection>
)}
</Table>
<StyledDiv>
<Button
Icon={IconPlus}
title="Add Field"
size="small"
variant="secondary"
onClick={() =>
navigate(
disabledMetadataFields.length
? './new-field/step-1'
: './new-field/step-2',
)
}
/>
</StyledDiv>
{shouldDisplayAddFieldButton && (
<StyledDiv>
<Button
Icon={IconPlus}
title="Add Field"
size="small"
variant="secondary"
onClick={() =>
navigate(
disabledMetadataFields.length
? './new-field/step-1'
: './new-field/step-2',
)
}
/>
</StyledDiv>
)}
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>

View File

@ -9,6 +9,7 @@ export const mockedClientConfig: ClientConfig = {
google: true,
password: true,
magicLink: false,
microsoft: false,
__typename: 'AuthProviders',
},
telemetry: {

View File

@ -63,6 +63,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'person',
namePlural: 'people',
isSystem: false,
isRemote: false,
},
toFieldMetadataId: 'c756f6ff-8c00-4fe5-a923-c6cfc7b1ac4a',
},
@ -91,6 +92,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'opportunity',
namePlural: 'opportunities',
isSystem: false,
isRemote: false,
},
toFieldMetadataId: '00468e2a-a601-4635-ae9c-a9bb826cc860',
},
@ -119,6 +121,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'activityTarget',
namePlural: 'activityTargets',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: 'bba19feb-c248-487b-92d7-98df54c51e44',
},
@ -221,6 +224,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'attachment',
namePlural: 'attachments',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '0880dac5-37d2-43a6-b143-722126d4923f',
},
@ -331,6 +335,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'workspaceMember',
namePlural: 'workspaceMembers',
isSystem: true,
isRemote: false,
},
fromFieldMetadataId: '0f3e456f-3bb4-4261-a436-95246dc0e159',
},
@ -378,6 +383,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'favorite',
namePlural: 'favorites',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '8fd8965b-bd4e-4a9b-90e9-c75652dadda1',
},