Add unique indexes and indexes for composite types (#7162)
Add support for indexes on composite fields and unicity constraint on indexes This pull request includes several changes across multiple files to improve error handling, enforce unique constraints, and update database migrations. The most important changes include updating error messages for snack bars, adding a new command to enforce unique constraints, and updating database migrations to include new fields and constraints. ### Error Handling Improvements: * [`packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx`](diffhunk://#diff-e7dc05ced8e4730430f5c7fcd0c75b3aa723da438c26e0bef8130b614427dd9aL23-R23): Updated error messages in `enqueueSnackBar` to use `error.message` directly. * [`packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts`](diffhunk://#diff-74c126d6bc7a5ed6b63be994d298df6669058034bfbc367b11045f9f31a3abe6L44-R46): Simplified error messages in `enqueueSnackBar`. * [`packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts`](diffhunk://#diff-af23a1d99639a66c251f87473e63e2b7bceaa4ee4f70fedfa0fcffe5c7d79181L56-R58): Simplified error messages in `enqueueSnackBar`. * [`packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts`](diffhunk://#diff-da04296cbe280202a1eaf6b1244a30490d4f400411bee139651172c59719088eL22-R24): Simplified error messages in `enqueueSnackBar`. ### New Command for Unique Constraints: * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-enforce-unique-constraints.command.ts`](diffhunk://#diff-8337096c8c80dd2619a5ba691ae5145101f8ae0368a75192a050047e8c6ab7cbR1-R159): Added a new command to enforce unique constraints on company domain names and person emails. * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command.ts`](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR13-R14): Integrated the new `EnforceUniqueConstraintsCommand` into the upgrade process. [[1]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR13-R14) [[2]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR31) [[3]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR64-R68) * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module.ts`](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R7): Registered the new `EnforceUniqueConstraintsCommand` in the module. [[1]](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R7) [[2]](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R24) ### Database Migrations: * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726757368824-migrationDebt.ts`](diffhunk://#diff-c450aeae7bc0ef4416a0ade2dc613ca3f688629f35d2a32f90a09c3f494febdcR1-R53): Added a migration to update the `relationMetadata_ondeleteaction_enum` and set default values. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726757368825-addIsUniqueToIndexMetadata.ts`](diffhunk://#diff-8f1e14bd7f6835ec2c3bb39bcc51e3c318a3008d576a981e682f4c985e746fbfR1-R19): Added a migration to include the `isUnique` field in `indexMetadata`. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726762935841-addCompostiveColumnToIndexFieldMetadata.ts`](diffhunk://#diff-7c96b7276c7722d41ff31de23b2de4d6e09adfdc74815356ba63bc96a2669440R1-R19): Added a migration to include the `compositeColumn` field in `indexFieldMetadata`. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726766871572-addWhereToIndexMetadata.ts`](diffhunk://#diff-26651295a975eb50e672dce0e4e274e861f66feb1b68105eee5a04df32796190R1-R14): Added a migration to include the `indexWhereClause` field in `indexMetadata`. ### GraphQL Exception Handling: * [`packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts`](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R1-R4): Enhanced exception handling for `QueryFailedError` to provide more specific error messages for unique constraint violations. [[1]](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R1-R4) [[2]](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R23-R59) * [`packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts`](diffhunk://#diff-233d58ab2333586dd45e46e33d4f07e04a4b8adde4a11a48e25d86985e5a7943L58-R58): Updated the `workspaceQueryRunnerGraphqlApiExceptionHandler` call to include context. * [`packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts`](diffhunk://#diff-68b803f0762c407f5d2d1f5f8d389655a60654a2dd2394a81318655dcd44dc43L58-R58): Updated the `workspaceQueryRunnerGraphqlApiExceptionHandler` call to include context. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -136,6 +136,7 @@ describe('useCommandMenu', () => {
|
||||
'ab7901eb-43e1-4dc7-8f3b-cdee2857eb9a',
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
fields: [],
|
||||
indexMetadatas: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
@ -20,7 +20,7 @@ export const PromiseRejectionEffect = () => {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
enqueueSnackBar(`Error: ${event.reason}`, {
|
||||
enqueueSnackBar(`${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
|
||||
@ -47,38 +47,36 @@ export const initialFavorites = [
|
||||
},
|
||||
];
|
||||
|
||||
export const sortedFavorites = [
|
||||
{
|
||||
"avatarType": "rounded",
|
||||
"avatarUrl": "",
|
||||
"id": "1",
|
||||
"labelIdentifier": " ",
|
||||
"link": "/object/person/1",
|
||||
"position": 0,
|
||||
"recordId": "1",
|
||||
"workspaceMemberId": undefined,
|
||||
},
|
||||
{
|
||||
"avatarType": "rounded",
|
||||
"avatarUrl": "",
|
||||
"id": "2",
|
||||
"labelIdentifier": " ",
|
||||
"link": "/object/person/3",
|
||||
"position": 1,
|
||||
"recordId": "3",
|
||||
"workspaceMemberId": undefined,
|
||||
},
|
||||
{
|
||||
"avatarType": "squared",
|
||||
"avatarUrl": "example.com",
|
||||
"id": "3",
|
||||
"key": "8f3b2121-f194-4ba4-9fbf-2d5a37126806",
|
||||
"labelIdentifier": "favoriteLabel",
|
||||
"link": "example.com",
|
||||
"position": 2,
|
||||
"recordId": "1",
|
||||
},
|
||||
]
|
||||
export const sortedFavorites = [
|
||||
{
|
||||
id: '1',
|
||||
recordId: '2',
|
||||
position: 0,
|
||||
avatarType: 'squared',
|
||||
avatarUrl: undefined,
|
||||
labelIdentifier: 'ABC Corp',
|
||||
link: '/object/company/2',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
recordId: '4',
|
||||
position: 1,
|
||||
avatarType: 'squared',
|
||||
avatarUrl: undefined,
|
||||
labelIdentifier: 'Company Test',
|
||||
link: '/object/company/4',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
position: 2,
|
||||
key: '8f3b2121-f194-4ba4-9fbf-2d5a37126806',
|
||||
labelIdentifier: 'favoriteLabel',
|
||||
avatarUrl: 'example.com',
|
||||
avatarType: 'squared',
|
||||
link: 'example.com',
|
||||
recordId: '1',
|
||||
},
|
||||
];
|
||||
|
||||
export const mocks = [
|
||||
{
|
||||
@ -343,8 +341,8 @@ export const mocks = [
|
||||
mutation DeleteOneFavorite($idToDelete: ID!) {
|
||||
deleteFavorite(id: $idToDelete) {
|
||||
__typename
|
||||
deletedAt
|
||||
id
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption';
|
||||
|
||||
describe('findAvailableTimeZoneOption', () => {
|
||||
it('should find the matching available IANA time zone select option from a given IANA time zone', () => {
|
||||
const ianaTimeZone = 'Europe/Paris';
|
||||
const expectedOption = {
|
||||
label: '(GMT+02:00) Central European Summer Time - Paris',
|
||||
value: 'Europe/Paris',
|
||||
};
|
||||
|
||||
const option = findAvailableTimeZoneOption(ianaTimeZone);
|
||||
|
||||
expect(option).toEqual(expectedOption);
|
||||
});
|
||||
});
|
||||
@ -6,24 +6,11 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { WorkspaceActivationStatus } from '~/generated/graphql';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
const filterTsVectorFields = (
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
): ObjectMetadataItem[] => {
|
||||
return objectMetadataItems.map((item) => ({
|
||||
...item,
|
||||
fields: item.fields.filter(
|
||||
(field) => field.type !== FieldMetadataType.TsVector,
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
export const ObjectMetadataItemsLoadEffect = () => {
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
@ -37,13 +24,12 @@ export const ObjectMetadataItemsLoadEffect = () => {
|
||||
const updateObjectMetadataItems = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
() => {
|
||||
const filteredFields = filterTsVectorFields(newObjectMetadataItems);
|
||||
const toSetObjectMetadataItems =
|
||||
isUndefinedOrNull(currentUser) ||
|
||||
currentWorkspace?.activationStatus !==
|
||||
WorkspaceActivationStatus.Active
|
||||
? generatedMockObjectMetadataItems
|
||||
: filteredFields;
|
||||
: newObjectMetadataItems;
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
|
||||
@ -24,6 +24,30 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
||||
updatedAt
|
||||
labelIdentifierFieldMetadataId
|
||||
imageIdentifierFieldMetadataId
|
||||
indexMetadatas(paging: { first: 100 }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
name
|
||||
indexWhereClause
|
||||
indexType
|
||||
isUnique
|
||||
indexFieldMetadatas(paging: { first: 100 }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
order
|
||||
fieldMetadataId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fields(paging: { first: 1000 }, filter: $fieldFilter) {
|
||||
edges {
|
||||
node {
|
||||
@ -37,6 +61,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
||||
isActive
|
||||
isSystem
|
||||
isNullable
|
||||
isUnique
|
||||
createdAt
|
||||
updatedAt
|
||||
defaultValue
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@ -41,12 +41,9 @@ export const useFindManyObjectMetadataItems = ({
|
||||
skip: skip || !apolloMetadataClient,
|
||||
onError: (error) => {
|
||||
logError('useFindManyObjectMetadataItems error : ' + error);
|
||||
enqueueSnackBar(
|
||||
`Error during useFindManyObjectMetadataItems, ${error.message}`,
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
enqueueSnackBar(`${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { IndexField as GeneratedIndexField } from '~/generated-metadata/graphql';
|
||||
|
||||
export type IndexFieldMetadataItem = Omit<GeneratedIndexField, '__typename'> & {
|
||||
__typename?: string;
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { IndexFieldMetadataItem } from '@/object-metadata/types/IndexFieldMetadataItem';
|
||||
import { Index as GeneratedIndex } from '~/generated-metadata/graphql';
|
||||
|
||||
export type IndexMetadataItem = Omit<
|
||||
GeneratedIndex,
|
||||
'__typename' | 'indexFieldMetadatas' | 'objectMetadata'
|
||||
> & {
|
||||
__typename?: string;
|
||||
indexFieldMetadatas: IndexFieldMetadataItem[];
|
||||
};
|
||||
@ -1,11 +1,13 @@
|
||||
import { Object as GeneratedObject } from '~/generated-metadata/graphql';
|
||||
|
||||
import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
|
||||
import { FieldMetadataItem } from './FieldMetadataItem';
|
||||
|
||||
export type ObjectMetadataItem = Omit<
|
||||
GeneratedObject,
|
||||
'__typename' | 'fields' | 'dataSourceId'
|
||||
'__typename' | 'fields' | 'dataSourceId' | 'indexMetadatas'
|
||||
> & {
|
||||
__typename?: string;
|
||||
fields: FieldMetadataItem[];
|
||||
indexMetadatas: IndexMetadataItem[];
|
||||
};
|
||||
|
||||
@ -11,6 +11,12 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
|
||||
pagedObjectMetadataItems?.objects.edges.map((object) => ({
|
||||
...object.node,
|
||||
fields: object.node.fields.edges.map((field) => field.node),
|
||||
indexMetadatas: object.node.indexMetadatas.edges.map((index) => ({
|
||||
...index.node,
|
||||
indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map(
|
||||
(indexField) => indexField.node,
|
||||
),
|
||||
})),
|
||||
})) ?? [];
|
||||
|
||||
return formattedObjects;
|
||||
|
||||
@ -20,6 +20,7 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
|
||||
isActive: z.boolean(),
|
||||
isCustom: z.boolean(),
|
||||
isNullable: z.boolean(),
|
||||
isUnique: z.boolean(),
|
||||
isSystem: z.boolean(),
|
||||
label: metadataLabelSchema(existingLabels),
|
||||
name: camelCaseStringSchema,
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { IndexFieldMetadataItem } from '@/object-metadata/types/IndexFieldMetadataItem';
|
||||
|
||||
export const indexFieldMetadataItemSchema = z.object({
|
||||
__typename: z.literal('indexField'),
|
||||
fieldMetadataId: z.string().uuid(),
|
||||
id: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
order: z.number(),
|
||||
}) satisfies z.ZodType<IndexFieldMetadataItem>;
|
||||
@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
|
||||
import { indexFieldMetadataItemSchema } from '@/object-metadata/validation-schemas/indexFieldMetadataItemSchema';
|
||||
import { IndexType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const indexMetadataItemSchema = z.object({
|
||||
__typename: z.literal('index'),
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
indexFieldMetadatas: z.array(indexFieldMetadataItemSchema),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
indexType: z.nativeEnum(IndexType),
|
||||
indexWhereClause: z.string().nullable(),
|
||||
isUnique: z.boolean(),
|
||||
objectMetadata: z.any(),
|
||||
}) satisfies z.ZodType<IndexMetadataItem>;
|
||||
@ -2,6 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||
import { indexMetadataItemSchema } from '@/object-metadata/validation-schemas/indexMetadataItemSchema';
|
||||
import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metadataLabelSchema';
|
||||
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
|
||||
|
||||
@ -11,6 +12,7 @@ export const objectMetadataItemSchema = z.object({
|
||||
dataSourceId: z.string().uuid(),
|
||||
description: z.string().trim().nullable().optional(),
|
||||
fields: z.array(fieldMetadataItemSchema()),
|
||||
indexMetadatas: z.array(indexMetadataItemSchema),
|
||||
icon: z.string().startsWith('Icon').trim(),
|
||||
id: z.string().uuid(),
|
||||
imageIdentifierFieldMetadataId: z.string().uuid().nullable(),
|
||||
|
||||
@ -13,6 +13,7 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
@ -132,7 +133,7 @@ export const useDeleteManyRecords = ({
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
cachedRecords.forEach((cachedRecord) => {
|
||||
if (!cachedRecord) {
|
||||
if (isUndefinedOrNull(cachedRecord?.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -53,12 +53,9 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
`useFindDuplicateRecords for "${objectMetadataItem.nameSingular}" error : ` +
|
||||
error,
|
||||
);
|
||||
enqueueSnackBar(
|
||||
`Error during useFindDuplicateRecords for "${objectMetadataItem.nameSingular}", ${error.message}`,
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
enqueueSnackBar(`Error finding duplicates:", ${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@ -19,12 +19,9 @@ export const useHandleFindManyRecordsError = ({
|
||||
`useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` +
|
||||
error,
|
||||
);
|
||||
enqueueSnackBar(
|
||||
`Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`,
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
enqueueSnackBar(`${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
handleError?.(error);
|
||||
};
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRe
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useUpdateOneRecordProps = {
|
||||
@ -130,7 +131,7 @@ export const useUpdateOneRecord = <
|
||||
},
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (!cachedRecord) {
|
||||
if (isUndefinedOrNull(cachedRecord?.id)) {
|
||||
throw error;
|
||||
}
|
||||
updateRecordFromCache({
|
||||
|
||||
@ -13,6 +13,7 @@ const sortDefinition: SortDefinition = {
|
||||
const objectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'object1',
|
||||
fields: [],
|
||||
indexMetadatas: [],
|
||||
createdAt: '2021-01-01',
|
||||
updatedAt: '2021-01-01',
|
||||
nameSingular: 'object1',
|
||||
|
||||
@ -156,6 +156,11 @@ export type FieldPhonesMetadata = {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldTsVectorMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldMetadata =
|
||||
| FieldBooleanMetadata
|
||||
| FieldCurrencyMetadata
|
||||
@ -174,8 +179,8 @@ export type FieldMetadata =
|
||||
| FieldUuidMetadata
|
||||
| FieldAddressMetadata
|
||||
| FieldActorMetadata
|
||||
| FieldArrayMetadata;
|
||||
|
||||
| FieldArrayMetadata
|
||||
| FieldTsVectorMetadata;
|
||||
export type FieldTextValue = string;
|
||||
export type FieldUUidValue = string; // TODO: can we replace with a template literal type, or maybe overkill ?
|
||||
export type FieldDateTimeValue = string | null;
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { FieldMetadata, FieldTsVectorMetadata } from '../FieldMetadata';
|
||||
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const isFieldTsVector = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
|
||||
): field is FieldDefinition<FieldTsVectorMetadata> =>
|
||||
field.type === FieldMetadataType.TsVector;
|
||||
@ -32,6 +32,7 @@ import { isFieldRichText } from '@/object-record/record-field/types/guards/isFie
|
||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||
import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue';
|
||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||
import { isFieldTsVector } from '@/object-record/record-field/types/guards/isFieldTsVectorValue';
|
||||
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
@ -130,6 +131,10 @@ export const isFieldValueEmpty = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (isFieldTsVector(fieldDefinition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
|
||||
);
|
||||
|
||||
@ -220,7 +220,7 @@ describe('useTableData', () => {
|
||||
delayMs: 0,
|
||||
viewType: ViewType.Kanban,
|
||||
}),
|
||||
setKanbanFieldName: useRecordBoard(recordIndexId),
|
||||
useRecordBoardHook: useRecordBoard(recordIndexId),
|
||||
kanbanFieldName: useRecoilValue(kanbanFieldNameState),
|
||||
kanbanData: useRecordIndexOptionsForBoard({
|
||||
objectNameSingular,
|
||||
@ -243,7 +243,7 @@ describe('useTableData', () => {
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
result.current.setKanbanFieldName.setKanbanFieldMetadataName(
|
||||
result.current.useRecordBoardHook.setKanbanFieldMetadataName(
|
||||
updatedAtFieldMetadataItem?.name,
|
||||
);
|
||||
});
|
||||
@ -278,10 +278,14 @@ describe('useTableData', () => {
|
||||
relationObjectMetadataNameSingular: '',
|
||||
relationType: undefined,
|
||||
targetFieldMetadataName: '',
|
||||
settings: {},
|
||||
settings: {
|
||||
displayAsRelativeDate: true,
|
||||
},
|
||||
},
|
||||
position: expect.any(Number),
|
||||
settings: {
|
||||
displayAsRelativeDate: true,
|
||||
},
|
||||
position: 7,
|
||||
settings: {},
|
||||
showLabel: undefined,
|
||||
size: 100,
|
||||
type: 'DATE_TIME',
|
||||
|
||||
@ -27,6 +27,7 @@ describe('useLimitPerMetadataItem', () => {
|
||||
nameSingular: 'nameSingular',
|
||||
updatedAt: 'updatedAt',
|
||||
fields: [],
|
||||
indexMetadatas: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ const objectData: ObjectMetadataItem[] = [
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
indexMetadatas: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
|
||||
export type SortedIndexByTableFamilyStateKey = {
|
||||
objectMetadataItemId: string;
|
||||
};
|
||||
|
||||
export const settingsObjectIndexesFamilyState = createFamilyState<
|
||||
IndexMetadataItem[] | null,
|
||||
SortedIndexByTableFamilyStateKey
|
||||
>({
|
||||
key: 'settingsObjectIndexesFamilyState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -1,7 +1,7 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledEllipsisDisplay = styled.div<{ maxWidth?: number }>`
|
||||
max-width: ${({ maxWidth }) => maxWidth ?? '100%'};
|
||||
max-width: ${({ maxWidth }) => (maxWidth ? maxWidth + 'px' : '100%')};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -2,7 +2,7 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { sortedFieldByTableFamilyState } from '@/ui/layout/table/states/sortedFieldByTableFamilyState';
|
||||
import { TableSortValue } from '@/ui/layout/table/types/TableSortValue';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { IconArrowDown, IconArrowUp } from 'twenty-ui';
|
||||
import { IconArrowDown, IconArrowUp, IconComponent } from 'twenty-ui';
|
||||
|
||||
export const SortableTableHeader = ({
|
||||
tableId,
|
||||
@ -10,12 +10,14 @@ export const SortableTableHeader = ({
|
||||
label,
|
||||
align = 'left',
|
||||
initialSort,
|
||||
Icon,
|
||||
}: {
|
||||
tableId: string;
|
||||
fieldName: string;
|
||||
label: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
initialSort?: TableSortValue;
|
||||
Icon?: IconComponent;
|
||||
}) => {
|
||||
const [sortedFieldByTable, setSortedFieldByTable] = useRecoilState(
|
||||
sortedFieldByTableFamilyState({ tableId }),
|
||||
@ -54,6 +56,7 @@ export const SortableTableHeader = ({
|
||||
<IconArrowDown size="14" />
|
||||
)
|
||||
) : null}
|
||||
{Icon && <Icon size={14} />}
|
||||
{label}
|
||||
{isSortActive && align === 'left' ? (
|
||||
isAsc ? (
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
export type TableFieldMetadata<ItemType> = {
|
||||
fieldLabel: string;
|
||||
fieldName: keyof ItemType;
|
||||
fieldType: 'string' | 'number';
|
||||
align: 'left' | 'right';
|
||||
FieldIcon?: IconComponent;
|
||||
};
|
||||
|
||||
@ -14,4 +14,5 @@ export type FeatureFlagKey =
|
||||
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
|
||||
| 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED'
|
||||
| 'IS_WORKSPACE_MIGRATED_FOR_SEARCH'
|
||||
| 'IS_ANALYTICS_V2_ENABLED';
|
||||
| 'IS_ANALYTICS_V2_ENABLED'
|
||||
| 'IS_UNIQUE_INDEXES_ENABLED';
|
||||
|
||||
Reference in New Issue
Block a user