Add field isLabelSyncedWithName (#8829)
## Context
The recent addition of object renaming introduced issues with enum
names. Enum names should follow the pattern
`${schemaName}.${tableName}_${columnName}_enum`. To address this, and to
allow users to customize the API name (which is included in the enum
name, columnName), this PR implements behavior similar to object
renaming by introducing a `isLabelSyncedWithName` boolean.
<img width="624" alt="Screenshot 2024-12-02 at 11 58 49"
src="https://github.com/user-attachments/assets/690fb71c-83f0-4922-80c0-946c92dacc30">
<img width="596" alt="Screenshot 2024-12-02 at 11 58 39"
src="https://github.com/user-attachments/assets/af9a0037-7cf5-40c3-9ed5-d51b340c8087">
This commit is contained in:
@ -75,6 +75,7 @@ export const UPDATE_ONE_FIELD_METADATA_ITEM = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
isLabelSyncedWithName
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -69,6 +69,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
||||
defaultValue
|
||||
options
|
||||
settings
|
||||
isLabelSyncedWithName
|
||||
relationDefinition {
|
||||
relationId
|
||||
direction
|
||||
|
||||
@ -21,13 +21,13 @@ export const variables = {
|
||||
fromDescription: null,
|
||||
fromIcon: undefined,
|
||||
fromLabel: 'label',
|
||||
fromName: 'label',
|
||||
fromName: 'name',
|
||||
fromObjectMetadataId: 'objectMetadataId',
|
||||
relationType: 'ONE_TO_ONE',
|
||||
toDescription: null,
|
||||
toIcon: undefined,
|
||||
toLabel: 'Another label',
|
||||
toName: 'anotherLabel',
|
||||
toName: 'anotherName',
|
||||
toObjectMetadataId: 'objectMetadataId1',
|
||||
},
|
||||
},
|
||||
|
||||
@ -6,27 +6,22 @@ export const FIELD_RELATION_METADATA_ID =
|
||||
'4da0302d-358a-45cd-9973-9f92723ed3c1';
|
||||
export const RELATION_METADATA_ID = 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6';
|
||||
|
||||
const baseFields = `
|
||||
id
|
||||
type
|
||||
name
|
||||
label
|
||||
description
|
||||
icon
|
||||
isCustom
|
||||
isActive
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
`;
|
||||
|
||||
|
||||
export const queries = {
|
||||
deleteMetadataField: gql`
|
||||
mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {
|
||||
deleteOneField(input: { id: $idToDelete }) {
|
||||
${baseFields}
|
||||
id
|
||||
type
|
||||
name
|
||||
label
|
||||
description
|
||||
icon
|
||||
isCustom
|
||||
isActive
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
}
|
||||
}
|
||||
`,
|
||||
@ -74,7 +69,19 @@ export const queries = {
|
||||
$updatePayload: UpdateFieldInput!
|
||||
) {
|
||||
updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {
|
||||
${baseFields}
|
||||
id
|
||||
type
|
||||
name
|
||||
label
|
||||
description
|
||||
icon
|
||||
isCustom
|
||||
isActive
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
isLabelSyncedWithName
|
||||
}
|
||||
}
|
||||
`,
|
||||
@ -98,6 +105,84 @@ export const queries = {
|
||||
}
|
||||
}
|
||||
`,
|
||||
getCurrentUser: gql`
|
||||
query GetCurrentUser {
|
||||
currentUser {
|
||||
...UserQueryFragment
|
||||
}
|
||||
}
|
||||
|
||||
fragment UserQueryFragment on User {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
email
|
||||
canImpersonate
|
||||
supportUserHash
|
||||
analyticsTinybirdJwts {
|
||||
getWebhookAnalytics
|
||||
getPageviewsAnalytics
|
||||
getUsersAnalytics
|
||||
getServerlessFunctionDuration
|
||||
getServerlessFunctionSuccessRate
|
||||
getServerlessFunctionErrorCount
|
||||
}
|
||||
onboardingStatus
|
||||
workspaceMember {
|
||||
...WorkspaceMemberQueryFragment
|
||||
}
|
||||
workspaceMembers {
|
||||
...WorkspaceMemberQueryFragment
|
||||
}
|
||||
defaultWorkspace {
|
||||
id
|
||||
displayName
|
||||
logo
|
||||
domainName
|
||||
inviteHash
|
||||
allowImpersonation
|
||||
activationStatus
|
||||
isPublicInviteLinkEnabled
|
||||
hasValidEntrepriseKey
|
||||
featureFlags {
|
||||
id
|
||||
key
|
||||
value
|
||||
workspaceId
|
||||
}
|
||||
metadataVersion
|
||||
currentBillingSubscription {
|
||||
id
|
||||
status
|
||||
interval
|
||||
}
|
||||
workspaceMembersCount
|
||||
}
|
||||
workspaces {
|
||||
workspace {
|
||||
id
|
||||
logo
|
||||
displayName
|
||||
domainName
|
||||
}
|
||||
}
|
||||
userVars
|
||||
}
|
||||
|
||||
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
|
||||
id
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
colorScheme
|
||||
avatarUrl
|
||||
locale
|
||||
timeZone
|
||||
dateFormat
|
||||
timeFormat
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export const objectMetadataId = '25611fce-6637-4089-b0ca-91afeec95784';
|
||||
@ -107,7 +192,7 @@ export const variables = {
|
||||
deleteMetadataFieldRelation: { idToDelete: RELATION_METADATA_ID },
|
||||
activateMetadataField: {
|
||||
idToUpdate: FIELD_METADATA_ID,
|
||||
updatePayload: { isActive: true, label: undefined },
|
||||
updatePayload: { isActive: true },
|
||||
},
|
||||
createMetadataField: {
|
||||
input: {
|
||||
@ -116,9 +201,10 @@ export const variables = {
|
||||
description: null,
|
||||
icon: undefined,
|
||||
label: 'fieldLabel',
|
||||
name: 'fieldlabel',
|
||||
name: 'fieldName',
|
||||
options: undefined,
|
||||
settings: undefined,
|
||||
isLabelSyncedWithName: true,
|
||||
objectMetadataId,
|
||||
type: 'TEXT',
|
||||
},
|
||||
@ -159,4 +245,54 @@ export const responseData = {
|
||||
defaultValue: '',
|
||||
options: [],
|
||||
},
|
||||
getCurrentUser: {
|
||||
currentUser: {
|
||||
id: 'test-user-id',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
email: 'test@example.com',
|
||||
canImpersonate: false,
|
||||
supportUserHash: null,
|
||||
analyticsTinybirdJwts: {
|
||||
getWebhookAnalytics: null,
|
||||
getPageviewsAnalytics: null,
|
||||
getUsersAnalytics: null,
|
||||
getServerlessFunctionDuration: null,
|
||||
getServerlessFunctionSuccessRate: null,
|
||||
getServerlessFunctionErrorCount: null,
|
||||
},
|
||||
onboardingStatus: 'completed',
|
||||
workspaceMember: {
|
||||
id: 'test-workspace-member-id',
|
||||
name: {
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
},
|
||||
colorScheme: 'light',
|
||||
avatarUrl: null,
|
||||
locale: 'en',
|
||||
timeZone: 'UTC',
|
||||
dateFormat: 'MM/DD/YYYY',
|
||||
timeFormat: '24',
|
||||
},
|
||||
workspaceMembers: [],
|
||||
defaultWorkspace: {
|
||||
id: 'test-workspace-id',
|
||||
displayName: 'Test Workspace',
|
||||
logo: null,
|
||||
domainName: 'test',
|
||||
inviteHash: 'test-hash',
|
||||
allowImpersonation: false,
|
||||
activationStatus: 'active',
|
||||
isPublicInviteLinkEnabled: false,
|
||||
hasValidEntrepriseKey: false,
|
||||
featureFlags: [],
|
||||
metadataVersion: 1,
|
||||
currentBillingSubscription: null,
|
||||
workspaceMembersCount: 1,
|
||||
},
|
||||
workspaces: [],
|
||||
userVars: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -25,7 +25,6 @@ export const query = gql`
|
||||
labelIdentifierFieldMetadataId
|
||||
imageIdentifierFieldMetadataId
|
||||
shortcut
|
||||
isLabelSyncedWithName
|
||||
fields(paging: { first: 1000 }, filter: $fieldFilter) {
|
||||
edges {
|
||||
node {
|
||||
@ -41,6 +40,7 @@ export const query = gql`
|
||||
isNullable
|
||||
createdAt
|
||||
updatedAt
|
||||
isLabelSyncedWithName
|
||||
relationDefinition {
|
||||
relationId
|
||||
direction
|
||||
|
||||
@ -45,11 +45,13 @@ describe('useCreateOneRelationMetadataItem', () => {
|
||||
relationType: RelationDefinitionType.OneToOne,
|
||||
field: {
|
||||
label: 'label',
|
||||
name: 'name',
|
||||
},
|
||||
objectMetadataId: 'objectMetadataId',
|
||||
connect: {
|
||||
field: {
|
||||
label: 'Another label',
|
||||
name: 'anotherName',
|
||||
},
|
||||
objectMetadataId: 'objectMetadataId1',
|
||||
},
|
||||
|
||||
@ -23,6 +23,7 @@ const fieldMetadataItem: FieldMetadataItem = {
|
||||
name: 'name',
|
||||
type: FieldMetadataType.Text,
|
||||
updatedAt: '',
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const fieldRelationMetadataItem: FieldMetadataItem = {
|
||||
@ -32,6 +33,7 @@ const fieldRelationMetadataItem: FieldMetadataItem = {
|
||||
name: 'name',
|
||||
type: FieldMetadataType.Relation,
|
||||
updatedAt: '',
|
||||
isLabelSyncedWithName: true,
|
||||
relationDefinition: {
|
||||
relationId: RELATION_METADATA_ID,
|
||||
direction: RelationDefinitionType.OneToMany,
|
||||
@ -137,6 +139,24 @@ const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: queries.getCurrentUser,
|
||||
variables: {},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: responseData.getCurrentUser,
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: queries.getCurrentUser,
|
||||
variables: {},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: responseData.getCurrentUser,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
@ -171,6 +191,8 @@ describe('useFieldMetadataItem', () => {
|
||||
label: 'fieldLabel',
|
||||
objectMetadataId,
|
||||
type: FieldMetadataType.Text,
|
||||
name: 'fieldName',
|
||||
isLabelSyncedWithName: true,
|
||||
});
|
||||
|
||||
expect(res.data).toEqual({
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useDeleteOneRelationMetadataItem } from '@/object-metadata/hooks/useDeleteOneRelationMetadataItem';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { Field } from '~/generated/graphql';
|
||||
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
|
||||
@ -18,6 +17,7 @@ export const useFieldMetadataItem = () => {
|
||||
const createMetadataField = (
|
||||
input: Pick<
|
||||
Field,
|
||||
| 'name'
|
||||
| 'label'
|
||||
| 'icon'
|
||||
| 'description'
|
||||
@ -25,6 +25,7 @@ export const useFieldMetadataItem = () => {
|
||||
| 'type'
|
||||
| 'options'
|
||||
| 'settings'
|
||||
| 'isLabelSyncedWithName'
|
||||
> & {
|
||||
objectMetadataId: string;
|
||||
},
|
||||
@ -37,6 +38,7 @@ export const useFieldMetadataItem = () => {
|
||||
type: input.type,
|
||||
label: formattedInput.label ?? '',
|
||||
name: formattedInput.name ?? '',
|
||||
isLabelSyncedWithName: formattedInput.isLabelSyncedWithName ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -9,14 +9,28 @@ import {
|
||||
import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
|
||||
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||
import { useApolloMetadataClient } from './useApolloMetadataClient';
|
||||
|
||||
export const useUpdateOneFieldMetadataItem = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
|
||||
const { refetch: refetchCurrentUser } = useGetCurrentUserQuery({
|
||||
onCompleted: (data) => {
|
||||
if (isDefined(data?.currentUser?.defaultWorkspace)) {
|
||||
setCurrentWorkspace(data.currentUser.defaultWorkspace);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { findManyRecordsQuery } = useFindManyRecordsQuery({
|
||||
objectNameSingular: CoreObjectNameSingular.View,
|
||||
recordGqlFields: {
|
||||
@ -54,20 +68,20 @@ export const useUpdateOneFieldMetadataItem = () => {
|
||||
| 'name'
|
||||
| 'defaultValue'
|
||||
| 'options'
|
||||
| 'isLabelSyncedWithName'
|
||||
>;
|
||||
}) => {
|
||||
const result = await mutate({
|
||||
variables: {
|
||||
idToUpdate: fieldMetadataIdToUpdate,
|
||||
updatePayload: {
|
||||
...updatePayload,
|
||||
label: updatePayload.label ?? undefined,
|
||||
},
|
||||
updatePayload: updatePayload,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
|
||||
});
|
||||
|
||||
await refetchCurrentUser();
|
||||
|
||||
await apolloClient.query({
|
||||
query: findManyRecordsQuery,
|
||||
variables: {
|
||||
|
||||
@ -39,4 +39,5 @@ export type FieldMetadataItem = Omit<
|
||||
settings?: {
|
||||
displayAsRelativeDate?: boolean;
|
||||
};
|
||||
isLabelSyncedWithName?: boolean;
|
||||
};
|
||||
|
||||
@ -24,10 +24,12 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
const input = {
|
||||
defaultValue: "'OPTION_1'",
|
||||
label: 'Example Label',
|
||||
name: 'exampleLabel',
|
||||
icon: 'example-icon',
|
||||
type: FieldMetadataType.Select,
|
||||
description: 'Example description',
|
||||
options,
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
@ -37,6 +39,7 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
name: 'exampleLabel',
|
||||
options,
|
||||
defaultValue: "'OPTION_1'",
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const result = formatFieldMetadataItemInput(input);
|
||||
@ -47,9 +50,11 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
it('should handle input without options', () => {
|
||||
const input = {
|
||||
label: 'Example Label',
|
||||
name: 'exampleLabel',
|
||||
icon: 'example-icon',
|
||||
type: FieldMetadataType.Select,
|
||||
description: 'Example description',
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
@ -59,6 +64,7 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
name: 'exampleLabel',
|
||||
options: undefined,
|
||||
defaultValue: undefined,
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const result = formatFieldMetadataItemInput(input);
|
||||
@ -86,10 +92,12 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
const input = {
|
||||
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
|
||||
label: 'Example Label',
|
||||
name: 'exampleLabel',
|
||||
icon: 'example-icon',
|
||||
type: FieldMetadataType.MultiSelect,
|
||||
description: 'Example description',
|
||||
options,
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
@ -99,6 +107,7 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
name: 'exampleLabel',
|
||||
options,
|
||||
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const result = formatFieldMetadataItemInput(input);
|
||||
@ -109,9 +118,11 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
it('should handle multi select input without options', () => {
|
||||
const input = {
|
||||
label: 'Example Label',
|
||||
name: 'exampleLabel',
|
||||
icon: 'example-icon',
|
||||
type: FieldMetadataType.MultiSelect,
|
||||
description: 'Example description',
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
@ -121,6 +132,7 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
name: 'exampleLabel',
|
||||
options: undefined,
|
||||
defaultValue: undefined,
|
||||
isLabelSyncedWithName: true,
|
||||
};
|
||||
|
||||
const result = formatFieldMetadataItemInput(input);
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||
|
||||
export const formatFieldMetadataItemInput = (
|
||||
input: Partial<
|
||||
Pick<
|
||||
FieldMetadataItem,
|
||||
| 'type'
|
||||
| 'name'
|
||||
| 'label'
|
||||
| 'defaultValue'
|
||||
| 'icon'
|
||||
| 'description'
|
||||
| 'defaultValue'
|
||||
| 'type'
|
||||
| 'options'
|
||||
| 'settings'
|
||||
| 'isLabelSyncedWithName'
|
||||
>
|
||||
>,
|
||||
) => {
|
||||
const label = input.label?.trim();
|
||||
|
||||
return {
|
||||
defaultValue: input.defaultValue,
|
||||
description: input.description?.trim() ?? null,
|
||||
icon: input.icon,
|
||||
label,
|
||||
name: label ? computeMetadataNameFromLabel(label) : undefined,
|
||||
label: input.label?.trim(),
|
||||
name: input.name?.trim(),
|
||||
options: input.options,
|
||||
settings: input.settings,
|
||||
isLabelSyncedWithName: input.isLabelSyncedWithName,
|
||||
};
|
||||
};
|
||||
|
||||
@ -10,10 +10,10 @@ import { formatFieldMetadataItemInput } from './formatFieldMetadataItemInput';
|
||||
|
||||
export type FormatRelationMetadataInputParams = {
|
||||
relationType: RelationType;
|
||||
field: Pick<Field, 'label' | 'icon' | 'description'>;
|
||||
field: Pick<Field, 'label' | 'icon' | 'description' | 'name'>;
|
||||
objectMetadataId: string;
|
||||
connect: {
|
||||
field: Pick<Field, 'label' | 'icon'>;
|
||||
field: Pick<Field, 'label' | 'icon' | 'name'>;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
};
|
||||
|
||||
@ -23,6 +23,7 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
|
||||
isUnique: z.boolean(),
|
||||
isSystem: z.boolean(),
|
||||
label: metadataLabelSchema(existingLabels),
|
||||
isLabelSyncedWithName: z.boolean(),
|
||||
name: camelCaseStringSchema,
|
||||
options: z
|
||||
.array(
|
||||
|
||||
Reference in New Issue
Block a user