mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 12:02:10 +03:00
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:
parent
7e4277fbe4
commit
3c7805c6d0
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1508,6 +1508,7 @@ export type Field = {
|
|||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
isActive?: Maybe<Scalars['Boolean']>;
|
isActive?: Maybe<Scalars['Boolean']>;
|
||||||
isCustom?: Maybe<Scalars['Boolean']>;
|
isCustom?: Maybe<Scalars['Boolean']>;
|
||||||
|
isLabelSyncedWithName: Scalars['Boolean'];
|
||||||
isNullable?: Maybe<Scalars['Boolean']>;
|
isNullable?: Maybe<Scalars['Boolean']>;
|
||||||
isSystem?: Maybe<Scalars['Boolean']>;
|
isSystem?: Maybe<Scalars['Boolean']>;
|
||||||
isUnique?: Maybe<Scalars['Boolean']>;
|
isUnique?: Maybe<Scalars['Boolean']>;
|
||||||
|
@ -75,6 +75,7 @@ export const UPDATE_ONE_FIELD_METADATA_ITEM = gql`
|
|||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
settings
|
settings
|
||||||
|
isLabelSyncedWithName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -69,6 +69,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
|||||||
defaultValue
|
defaultValue
|
||||||
options
|
options
|
||||||
settings
|
settings
|
||||||
|
isLabelSyncedWithName
|
||||||
relationDefinition {
|
relationDefinition {
|
||||||
relationId
|
relationId
|
||||||
direction
|
direction
|
||||||
|
@ -21,13 +21,13 @@ export const variables = {
|
|||||||
fromDescription: null,
|
fromDescription: null,
|
||||||
fromIcon: undefined,
|
fromIcon: undefined,
|
||||||
fromLabel: 'label',
|
fromLabel: 'label',
|
||||||
fromName: 'label',
|
fromName: 'name',
|
||||||
fromObjectMetadataId: 'objectMetadataId',
|
fromObjectMetadataId: 'objectMetadataId',
|
||||||
relationType: 'ONE_TO_ONE',
|
relationType: 'ONE_TO_ONE',
|
||||||
toDescription: null,
|
toDescription: null,
|
||||||
toIcon: undefined,
|
toIcon: undefined,
|
||||||
toLabel: 'Another label',
|
toLabel: 'Another label',
|
||||||
toName: 'anotherLabel',
|
toName: 'anotherName',
|
||||||
toObjectMetadataId: 'objectMetadataId1',
|
toObjectMetadataId: 'objectMetadataId1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -6,27 +6,22 @@ export const FIELD_RELATION_METADATA_ID =
|
|||||||
'4da0302d-358a-45cd-9973-9f92723ed3c1';
|
'4da0302d-358a-45cd-9973-9f92723ed3c1';
|
||||||
export const RELATION_METADATA_ID = 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6';
|
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 = {
|
export const queries = {
|
||||||
deleteMetadataField: gql`
|
deleteMetadataField: gql`
|
||||||
mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {
|
mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {
|
||||||
deleteOneField(input: { id: $idToDelete }) {
|
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!
|
$updatePayload: UpdateFieldInput!
|
||||||
) {
|
) {
|
||||||
updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {
|
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';
|
export const objectMetadataId = '25611fce-6637-4089-b0ca-91afeec95784';
|
||||||
@ -107,7 +192,7 @@ export const variables = {
|
|||||||
deleteMetadataFieldRelation: { idToDelete: RELATION_METADATA_ID },
|
deleteMetadataFieldRelation: { idToDelete: RELATION_METADATA_ID },
|
||||||
activateMetadataField: {
|
activateMetadataField: {
|
||||||
idToUpdate: FIELD_METADATA_ID,
|
idToUpdate: FIELD_METADATA_ID,
|
||||||
updatePayload: { isActive: true, label: undefined },
|
updatePayload: { isActive: true },
|
||||||
},
|
},
|
||||||
createMetadataField: {
|
createMetadataField: {
|
||||||
input: {
|
input: {
|
||||||
@ -116,9 +201,10 @@ export const variables = {
|
|||||||
description: null,
|
description: null,
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
label: 'fieldLabel',
|
label: 'fieldLabel',
|
||||||
name: 'fieldlabel',
|
name: 'fieldName',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
settings: undefined,
|
settings: undefined,
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
objectMetadataId,
|
objectMetadataId,
|
||||||
type: 'TEXT',
|
type: 'TEXT',
|
||||||
},
|
},
|
||||||
@ -159,4 +245,54 @@ export const responseData = {
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
options: [],
|
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
|
labelIdentifierFieldMetadataId
|
||||||
imageIdentifierFieldMetadataId
|
imageIdentifierFieldMetadataId
|
||||||
shortcut
|
shortcut
|
||||||
isLabelSyncedWithName
|
|
||||||
fields(paging: { first: 1000 }, filter: $fieldFilter) {
|
fields(paging: { first: 1000 }, filter: $fieldFilter) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
@ -41,6 +40,7 @@ export const query = gql`
|
|||||||
isNullable
|
isNullable
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
|
isLabelSyncedWithName
|
||||||
relationDefinition {
|
relationDefinition {
|
||||||
relationId
|
relationId
|
||||||
direction
|
direction
|
||||||
|
@ -45,11 +45,13 @@ describe('useCreateOneRelationMetadataItem', () => {
|
|||||||
relationType: RelationDefinitionType.OneToOne,
|
relationType: RelationDefinitionType.OneToOne,
|
||||||
field: {
|
field: {
|
||||||
label: 'label',
|
label: 'label',
|
||||||
|
name: 'name',
|
||||||
},
|
},
|
||||||
objectMetadataId: 'objectMetadataId',
|
objectMetadataId: 'objectMetadataId',
|
||||||
connect: {
|
connect: {
|
||||||
field: {
|
field: {
|
||||||
label: 'Another label',
|
label: 'Another label',
|
||||||
|
name: 'anotherName',
|
||||||
},
|
},
|
||||||
objectMetadataId: 'objectMetadataId1',
|
objectMetadataId: 'objectMetadataId1',
|
||||||
},
|
},
|
||||||
|
@ -23,6 +23,7 @@ const fieldMetadataItem: FieldMetadataItem = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
type: FieldMetadataType.Text,
|
type: FieldMetadataType.Text,
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldRelationMetadataItem: FieldMetadataItem = {
|
const fieldRelationMetadataItem: FieldMetadataItem = {
|
||||||
@ -32,6 +33,7 @@ const fieldRelationMetadataItem: FieldMetadataItem = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
type: FieldMetadataType.Relation,
|
type: FieldMetadataType.Relation,
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
relationDefinition: {
|
relationDefinition: {
|
||||||
relationId: RELATION_METADATA_ID,
|
relationId: RELATION_METADATA_ID,
|
||||||
direction: RelationDefinitionType.OneToMany,
|
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({
|
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||||
@ -171,6 +191,8 @@ describe('useFieldMetadataItem', () => {
|
|||||||
label: 'fieldLabel',
|
label: 'fieldLabel',
|
||||||
objectMetadataId,
|
objectMetadataId,
|
||||||
type: FieldMetadataType.Text,
|
type: FieldMetadataType.Text,
|
||||||
|
name: 'fieldName',
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.data).toEqual({
|
expect(res.data).toEqual({
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useDeleteOneRelationMetadataItem } from '@/object-metadata/hooks/useDeleteOneRelationMetadataItem';
|
import { useDeleteOneRelationMetadataItem } from '@/object-metadata/hooks/useDeleteOneRelationMetadataItem';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { Field } from '~/generated/graphql';
|
|
||||||
|
|
||||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||||
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
|
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
|
||||||
@ -18,6 +17,7 @@ export const useFieldMetadataItem = () => {
|
|||||||
const createMetadataField = (
|
const createMetadataField = (
|
||||||
input: Pick<
|
input: Pick<
|
||||||
Field,
|
Field,
|
||||||
|
| 'name'
|
||||||
| 'label'
|
| 'label'
|
||||||
| 'icon'
|
| 'icon'
|
||||||
| 'description'
|
| 'description'
|
||||||
@ -25,6 +25,7 @@ export const useFieldMetadataItem = () => {
|
|||||||
| 'type'
|
| 'type'
|
||||||
| 'options'
|
| 'options'
|
||||||
| 'settings'
|
| 'settings'
|
||||||
|
| 'isLabelSyncedWithName'
|
||||||
> & {
|
> & {
|
||||||
objectMetadataId: string;
|
objectMetadataId: string;
|
||||||
},
|
},
|
||||||
@ -37,6 +38,7 @@ export const useFieldMetadataItem = () => {
|
|||||||
type: input.type,
|
type: input.type,
|
||||||
label: formattedInput.label ?? '',
|
label: formattedInput.label ?? '',
|
||||||
name: formattedInput.name ?? '',
|
name: formattedInput.name ?? '',
|
||||||
|
isLabelSyncedWithName: formattedInput.isLabelSyncedWithName ?? true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,14 +9,28 @@ import {
|
|||||||
import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
|
import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
|
||||||
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
|
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
|
||||||
|
|
||||||
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
|
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';
|
import { useApolloMetadataClient } from './useApolloMetadataClient';
|
||||||
|
|
||||||
export const useUpdateOneFieldMetadataItem = () => {
|
export const useUpdateOneFieldMetadataItem = () => {
|
||||||
const apolloMetadataClient = useApolloMetadataClient();
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
const apolloClient = useApolloClient();
|
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({
|
const { findManyRecordsQuery } = useFindManyRecordsQuery({
|
||||||
objectNameSingular: CoreObjectNameSingular.View,
|
objectNameSingular: CoreObjectNameSingular.View,
|
||||||
recordGqlFields: {
|
recordGqlFields: {
|
||||||
@ -54,20 +68,20 @@ export const useUpdateOneFieldMetadataItem = () => {
|
|||||||
| 'name'
|
| 'name'
|
||||||
| 'defaultValue'
|
| 'defaultValue'
|
||||||
| 'options'
|
| 'options'
|
||||||
|
| 'isLabelSyncedWithName'
|
||||||
>;
|
>;
|
||||||
}) => {
|
}) => {
|
||||||
const result = await mutate({
|
const result = await mutate({
|
||||||
variables: {
|
variables: {
|
||||||
idToUpdate: fieldMetadataIdToUpdate,
|
idToUpdate: fieldMetadataIdToUpdate,
|
||||||
updatePayload: {
|
updatePayload: updatePayload,
|
||||||
...updatePayload,
|
|
||||||
label: updatePayload.label ?? undefined,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
awaitRefetchQueries: true,
|
awaitRefetchQueries: true,
|
||||||
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
|
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await refetchCurrentUser();
|
||||||
|
|
||||||
await apolloClient.query({
|
await apolloClient.query({
|
||||||
query: findManyRecordsQuery,
|
query: findManyRecordsQuery,
|
||||||
variables: {
|
variables: {
|
||||||
|
@ -39,4 +39,5 @@ export type FieldMetadataItem = Omit<
|
|||||||
settings?: {
|
settings?: {
|
||||||
displayAsRelativeDate?: boolean;
|
displayAsRelativeDate?: boolean;
|
||||||
};
|
};
|
||||||
|
isLabelSyncedWithName?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -24,10 +24,12 @@ describe('formatFieldMetadataItemInput', () => {
|
|||||||
const input = {
|
const input = {
|
||||||
defaultValue: "'OPTION_1'",
|
defaultValue: "'OPTION_1'",
|
||||||
label: 'Example Label',
|
label: 'Example Label',
|
||||||
|
name: 'exampleLabel',
|
||||||
icon: 'example-icon',
|
icon: 'example-icon',
|
||||||
type: FieldMetadataType.Select,
|
type: FieldMetadataType.Select,
|
||||||
description: 'Example description',
|
description: 'Example description',
|
||||||
options,
|
options,
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
@ -37,6 +39,7 @@ describe('formatFieldMetadataItemInput', () => {
|
|||||||
name: 'exampleLabel',
|
name: 'exampleLabel',
|
||||||
options,
|
options,
|
||||||
defaultValue: "'OPTION_1'",
|
defaultValue: "'OPTION_1'",
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = formatFieldMetadataItemInput(input);
|
const result = formatFieldMetadataItemInput(input);
|
||||||
@ -47,9 +50,11 @@ describe('formatFieldMetadataItemInput', () => {
|
|||||||
it('should handle input without options', () => {
|
it('should handle input without options', () => {
|
||||||
const input = {
|
const input = {
|
||||||
label: 'Example Label',
|
label: 'Example Label',
|
||||||
|
name: 'exampleLabel',
|
||||||
icon: 'example-icon',
|
icon: 'example-icon',
|
||||||
type: FieldMetadataType.Select,
|
type: FieldMetadataType.Select,
|
||||||
description: 'Example description',
|
description: 'Example description',
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
@ -59,6 +64,7 @@ describe('formatFieldMetadataItemInput', () => {
|
|||||||
name: 'exampleLabel',
|
name: 'exampleLabel',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = formatFieldMetadataItemInput(input);
|
const result = formatFieldMetadataItemInput(input);
|
||||||
@ -86,10 +92,12 @@ describe('formatFieldMetadataItemInput', () => {
|
|||||||
const input = {
|
const input = {
|
||||||
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
|
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
|
||||||
label: 'Example Label',
|
label: 'Example Label',
|
||||||
|
name: 'exampleLabel',
|
||||||
icon: 'example-icon',
|
icon: 'example-icon',
|
||||||
type: FieldMetadataType.MultiSelect,
|
type: FieldMetadataType.MultiSelect,
|
||||||
description: 'Example description',
|
description: 'Example description',
|
||||||
options,
|
options,
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
@ -99,6 +107,7 @@ describe('formatFieldMetadataItemInput', () => {
|
|||||||
name: 'exampleLabel',
|
name: 'exampleLabel',
|
||||||
options,
|
options,
|
||||||
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
|
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = formatFieldMetadataItemInput(input);
|
const result = formatFieldMetadataItemInput(input);
|
||||||
@ -109,9 +118,11 @@ describe('formatFieldMetadataItemInput', () => {
|
|||||||
it('should handle multi select input without options', () => {
|
it('should handle multi select input without options', () => {
|
||||||
const input = {
|
const input = {
|
||||||
label: 'Example Label',
|
label: 'Example Label',
|
||||||
|
name: 'exampleLabel',
|
||||||
icon: 'example-icon',
|
icon: 'example-icon',
|
||||||
type: FieldMetadataType.MultiSelect,
|
type: FieldMetadataType.MultiSelect,
|
||||||
description: 'Example description',
|
description: 'Example description',
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
@ -121,6 +132,7 @@ describe('formatFieldMetadataItemInput', () => {
|
|||||||
name: 'exampleLabel',
|
name: 'exampleLabel',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = formatFieldMetadataItemInput(input);
|
const result = formatFieldMetadataItemInput(input);
|
||||||
|
@ -1,29 +1,29 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
|
||||||
|
|
||||||
export const formatFieldMetadataItemInput = (
|
export const formatFieldMetadataItemInput = (
|
||||||
input: Partial<
|
input: Partial<
|
||||||
Pick<
|
Pick<
|
||||||
FieldMetadataItem,
|
FieldMetadataItem,
|
||||||
| 'type'
|
| 'name'
|
||||||
| 'label'
|
| 'label'
|
||||||
| 'defaultValue'
|
|
||||||
| 'icon'
|
| 'icon'
|
||||||
| 'description'
|
| 'description'
|
||||||
|
| 'defaultValue'
|
||||||
|
| 'type'
|
||||||
| 'options'
|
| 'options'
|
||||||
| 'settings'
|
| 'settings'
|
||||||
|
| 'isLabelSyncedWithName'
|
||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const label = input.label?.trim();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
defaultValue: input.defaultValue,
|
defaultValue: input.defaultValue,
|
||||||
description: input.description?.trim() ?? null,
|
description: input.description?.trim() ?? null,
|
||||||
icon: input.icon,
|
icon: input.icon,
|
||||||
label,
|
label: input.label?.trim(),
|
||||||
name: label ? computeMetadataNameFromLabel(label) : undefined,
|
name: input.name?.trim(),
|
||||||
options: input.options,
|
options: input.options,
|
||||||
settings: input.settings,
|
settings: input.settings,
|
||||||
|
isLabelSyncedWithName: input.isLabelSyncedWithName,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -10,10 +10,10 @@ import { formatFieldMetadataItemInput } from './formatFieldMetadataItemInput';
|
|||||||
|
|
||||||
export type FormatRelationMetadataInputParams = {
|
export type FormatRelationMetadataInputParams = {
|
||||||
relationType: RelationType;
|
relationType: RelationType;
|
||||||
field: Pick<Field, 'label' | 'icon' | 'description'>;
|
field: Pick<Field, 'label' | 'icon' | 'description' | 'name'>;
|
||||||
objectMetadataId: string;
|
objectMetadataId: string;
|
||||||
connect: {
|
connect: {
|
||||||
field: Pick<Field, 'label' | 'icon'>;
|
field: Pick<Field, 'label' | 'icon' | 'name'>;
|
||||||
objectMetadataId: string;
|
objectMetadataId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,7 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
|
|||||||
isUnique: z.boolean(),
|
isUnique: z.boolean(),
|
||||||
isSystem: z.boolean(),
|
isSystem: z.boolean(),
|
||||||
label: metadataLabelSchema(existingLabels),
|
label: metadataLabelSchema(existingLabels),
|
||||||
|
isLabelSyncedWithName: z.boolean(),
|
||||||
name: camelCaseStringSchema,
|
name: camelCaseStringSchema,
|
||||||
options: z
|
options: z
|
||||||
.array(
|
.array(
|
||||||
|
@ -4,17 +4,39 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||||
|
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
|
||||||
|
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||||
|
import { DATABASE_IDENTIFIER_MAXIMUM_LENGTH } from '@/settings/data-model/constants/DatabaseIdentifierMaximumLength';
|
||||||
import { getErrorMessageFromError } from '@/settings/data-model/fields/forms/utils/errorMessages';
|
import { getErrorMessageFromError } from '@/settings/data-model/fields/forms/utils/errorMessages';
|
||||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import {
|
||||||
|
AppTooltip,
|
||||||
|
Card,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconRefresh,
|
||||||
|
isDefined,
|
||||||
|
TooltipDelay,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
|
||||||
export const settingsDataModelFieldIconLabelFormSchema = (
|
export const settingsDataModelFieldIconLabelFormSchema = (
|
||||||
existingOtherLabels: string[] = [],
|
existingOtherLabels: string[] = [],
|
||||||
) => {
|
) => {
|
||||||
return fieldMetadataItemSchema(existingOtherLabels).pick({
|
return fieldMetadataItemSchema(existingOtherLabels)
|
||||||
icon: true,
|
.pick({
|
||||||
label: true,
|
icon: true,
|
||||||
});
|
label: true,
|
||||||
|
})
|
||||||
|
.merge(
|
||||||
|
fieldMetadataItemSchema()
|
||||||
|
.pick({
|
||||||
|
name: true,
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsDataModelFieldIconLabelFormValues = z.infer<
|
type SettingsDataModelFieldIconLabelFormValues = z.infer<
|
||||||
@ -28,57 +50,182 @@ const StyledInputsContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledAdvancedSettingsSectionInputWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAdvancedSettingsOuterContainer = styled.div`
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAdvancedSettingsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
type SettingsDataModelFieldIconLabelFormProps = {
|
type SettingsDataModelFieldIconLabelFormProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
fieldMetadataItem?: FieldMetadataItem;
|
fieldMetadataItem?: FieldMetadataItem;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
|
canToggleSyncLabelWithName?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsDataModelFieldIconLabelForm = ({
|
export const SettingsDataModelFieldIconLabelForm = ({
|
||||||
|
canToggleSyncLabelWithName = true,
|
||||||
disabled,
|
disabled,
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
maxLength,
|
maxLength,
|
||||||
}: SettingsDataModelFieldIconLabelFormProps) => {
|
}: SettingsDataModelFieldIconLabelFormProps) => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
trigger,
|
setValue,
|
||||||
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useFormContext<SettingsDataModelFieldIconLabelFormValues>();
|
} = useFormContext<SettingsDataModelFieldIconLabelFormValues>();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const isLabelSyncedWithName =
|
||||||
|
watch('isLabelSyncedWithName') ??
|
||||||
|
(isDefined(fieldMetadataItem)
|
||||||
|
? fieldMetadataItem.isLabelSyncedWithName
|
||||||
|
: true);
|
||||||
|
const label = watch('label');
|
||||||
|
|
||||||
|
const apiNameTooltipText = isLabelSyncedWithName
|
||||||
|
? 'Deactivate "Synchronize Objects Labels and API Names" to set a custom API name'
|
||||||
|
: 'Input must be in camel case and cannot start with a number';
|
||||||
|
|
||||||
|
const fillNameFromLabel = (label: string) => {
|
||||||
|
isDefined(label) &&
|
||||||
|
setValue('name', computeMetadataNameFromLabel(label), {
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledInputsContainer>
|
<>
|
||||||
<Controller
|
<StyledInputsContainer>
|
||||||
name="icon"
|
<Controller
|
||||||
control={control}
|
name="icon"
|
||||||
defaultValue={fieldMetadataItem?.icon ?? 'IconUsers'}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
defaultValue={fieldMetadataItem?.icon ?? 'IconUsers'}
|
||||||
<IconPicker
|
render={({ field: { onChange, value } }) => (
|
||||||
disabled={disabled}
|
<IconPicker
|
||||||
selectedIconKey={value ?? ''}
|
disabled={disabled}
|
||||||
onChange={({ iconKey }) => onChange(iconKey)}
|
selectedIconKey={value ?? ''}
|
||||||
variant="primary"
|
onChange={({ iconKey }) => onChange(iconKey)}
|
||||||
/>
|
variant="primary"
|
||||||
)}
|
/>
|
||||||
/>
|
)}
|
||||||
<Controller
|
/>
|
||||||
name="label"
|
<Controller
|
||||||
control={control}
|
name="label"
|
||||||
defaultValue={fieldMetadataItem?.label}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
defaultValue={fieldMetadataItem?.label}
|
||||||
<TextInput
|
render={({ field: { onChange, value } }) => (
|
||||||
placeholder="Employees"
|
<TextInput
|
||||||
value={value}
|
placeholder="Employees"
|
||||||
onChange={(e) => {
|
value={value}
|
||||||
onChange(e);
|
onChange={(value) => {
|
||||||
trigger('label');
|
onChange(value);
|
||||||
}}
|
if (isLabelSyncedWithName === true) {
|
||||||
error={getErrorMessageFromError(errors.label?.message)}
|
fillNameFromLabel(value);
|
||||||
disabled={disabled}
|
}
|
||||||
maxLength={maxLength}
|
}}
|
||||||
fullWidth
|
error={getErrorMessageFromError(errors.label?.message)}
|
||||||
/>
|
disabled={disabled}
|
||||||
)}
|
maxLength={maxLength}
|
||||||
/>
|
fullWidth
|
||||||
</StyledInputsContainer>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
{canToggleSyncLabelWithName && (
|
||||||
|
<StyledAdvancedSettingsOuterContainer>
|
||||||
|
<AdvancedSettingsWrapper>
|
||||||
|
<StyledAdvancedSettingsContainer>
|
||||||
|
<StyledAdvancedSettingsSectionInputWrapper>
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
defaultValue={fieldMetadataItem?.name}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
label="API Name"
|
||||||
|
placeholder="employees"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled || isLabelSyncedWithName}
|
||||||
|
fullWidth
|
||||||
|
maxLength={DATABASE_IDENTIFIER_MAXIMUM_LENGTH}
|
||||||
|
RightIcon={() =>
|
||||||
|
apiNameTooltipText && (
|
||||||
|
<>
|
||||||
|
<IconInfoCircle
|
||||||
|
id="info-circle-id-name"
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
color={theme.font.color.tertiary}
|
||||||
|
style={{ outline: 'none' }}
|
||||||
|
/>
|
||||||
|
<AppTooltip
|
||||||
|
anchorSelect="#info-circle-id-name"
|
||||||
|
content={apiNameTooltipText}
|
||||||
|
offset={5}
|
||||||
|
noArrow
|
||||||
|
place="bottom"
|
||||||
|
positionStrategy="fixed"
|
||||||
|
delay={TooltipDelay.shortDelay}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
<Controller
|
||||||
|
name="isLabelSyncedWithName"
|
||||||
|
control={control}
|
||||||
|
defaultValue={
|
||||||
|
fieldMetadataItem?.isLabelSyncedWithName ?? true
|
||||||
|
}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Card rounded>
|
||||||
|
<SettingsOptionCardContentToggle
|
||||||
|
Icon={IconRefresh}
|
||||||
|
title="Synchronize Field Label and API Name"
|
||||||
|
description="Should changing a field's label also change the API name?"
|
||||||
|
checked={value ?? true}
|
||||||
|
disabled={
|
||||||
|
isDefined(fieldMetadataItem) &&
|
||||||
|
!fieldMetadataItem.isCustom
|
||||||
|
}
|
||||||
|
advancedMode
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange(value);
|
||||||
|
if (value === true) {
|
||||||
|
fillNameFromLabel(label);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledAdvancedSettingsSectionInputWrapper>
|
||||||
|
</StyledAdvancedSettingsContainer>
|
||||||
|
</AdvancedSettingsWrapper>
|
||||||
|
</StyledAdvancedSettingsOuterContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -20,10 +20,20 @@ import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
|||||||
|
|
||||||
export const settingsDataModelFieldRelationFormSchema = z.object({
|
export const settingsDataModelFieldRelationFormSchema = z.object({
|
||||||
relation: z.object({
|
relation: z.object({
|
||||||
field: fieldMetadataItemSchema().pick({
|
field: fieldMetadataItemSchema()
|
||||||
icon: true,
|
.pick({
|
||||||
label: true,
|
icon: true,
|
||||||
}),
|
label: true,
|
||||||
|
})
|
||||||
|
// NOT SURE IF THIS IS CORRECT
|
||||||
|
.merge(
|
||||||
|
fieldMetadataItemSchema()
|
||||||
|
.pick({
|
||||||
|
name: true,
|
||||||
|
isLabelSyncedWithName: true,
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
),
|
||||||
objectMetadataId: z.string().uuid(),
|
objectMetadataId: z.string().uuid(),
|
||||||
type: z.enum(
|
type: z.enum(
|
||||||
Object.keys(RELATION_TYPES) as [
|
Object.keys(RELATION_TYPES) as [
|
||||||
|
@ -2,7 +2,7 @@ import { useApolloClient } from '@apollo/client';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import omit from 'lodash.omit';
|
import omit from 'lodash.omit';
|
||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
@ -49,6 +49,7 @@ type SettingsDataModelFieldEditFormValues = z.infer<
|
|||||||
export const SettingsObjectFieldEdit = () => {
|
export const SettingsObjectFieldEdit = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const [isPersisting, setIsPersisting] = useState(false);
|
||||||
|
|
||||||
const { objectSlug = '', fieldSlug = '' } = useParams();
|
const { objectSlug = '', fieldSlug = '' } = useParams();
|
||||||
const { findObjectMetadataItemBySlug } = useFilteredObjectMetadataItems();
|
const { findObjectMetadataItemBySlug } = useFilteredObjectMetadataItems();
|
||||||
@ -87,14 +88,16 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
type: fieldMetadataItem?.type as SettingsFieldType,
|
type: fieldMetadataItem?.type as SettingsFieldType,
|
||||||
label: fieldMetadataItem?.label ?? '',
|
label: fieldMetadataItem?.label ?? '',
|
||||||
description: fieldMetadataItem?.description,
|
description: fieldMetadataItem?.description,
|
||||||
|
isLabelSyncedWithName: fieldMetadataItem?.isLabelSyncedWithName ?? true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isPersisting) return;
|
||||||
if (!objectMetadataItem || !fieldMetadataItem) {
|
if (!objectMetadataItem || !fieldMetadataItem) {
|
||||||
navigate(AppPath.NotFound);
|
navigate(AppPath.NotFound);
|
||||||
}
|
}
|
||||||
}, [fieldMetadataItem, objectMetadataItem, navigate]);
|
}, [navigate, objectMetadataItem, fieldMetadataItem, isPersisting]);
|
||||||
|
|
||||||
const { isDirty, isValid, isSubmitting } = formConfig.formState;
|
const { isDirty, isValid, isSubmitting } = formConfig.formState;
|
||||||
const canSave = isDirty && isValid && !isSubmitting;
|
const canSave = isDirty && isValid && !isSubmitting;
|
||||||
@ -125,6 +128,8 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
}) ?? {};
|
}) ?? {};
|
||||||
|
|
||||||
if (isDefined(relationFieldMetadataItem)) {
|
if (isDefined(relationFieldMetadataItem)) {
|
||||||
|
setIsPersisting(true);
|
||||||
|
|
||||||
await updateOneFieldMetadataItem({
|
await updateOneFieldMetadataItem({
|
||||||
objectMetadataId: objectMetadataItem.id,
|
objectMetadataId: objectMetadataItem.id,
|
||||||
fieldMetadataIdToUpdate: relationFieldMetadataItem.id,
|
fieldMetadataIdToUpdate: relationFieldMetadataItem.id,
|
||||||
@ -141,6 +146,8 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
Object.keys(otherDirtyFields),
|
Object.keys(otherDirtyFields),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setIsPersisting(true);
|
||||||
|
|
||||||
await updateOneFieldMetadataItem({
|
await updateOneFieldMetadataItem({
|
||||||
objectMetadataId: objectMetadataItem.id,
|
objectMetadataId: objectMetadataItem.id,
|
||||||
fieldMetadataIdToUpdate: fieldMetadataItem.id,
|
fieldMetadataIdToUpdate: fieldMetadataItem.id,
|
||||||
@ -155,6 +162,8 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
enqueueSnackBar((error as Error).message, {
|
enqueueSnackBar((error as Error).message, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsPersisting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -210,6 +219,9 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
disabled={!fieldMetadataItem.isCustom}
|
disabled={!fieldMetadataItem.isCustom}
|
||||||
fieldMetadataItem={fieldMetadataItem}
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
|
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
|
||||||
|
canToggleSyncLabelWithName={
|
||||||
|
fieldMetadataItem.type !== FieldMetadataType.Relation
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
|
@ -29,6 +29,7 @@ import { H2Title, Section } from 'twenty-ui';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { DEFAULT_ICONS_BY_FIELD_TYPE } from '~/pages/settings/data-model/constants/DefaultIconsByFieldType';
|
import { DEFAULT_ICONS_BY_FIELD_TYPE } from '~/pages/settings/data-model/constants/DefaultIconsByFieldType';
|
||||||
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
type SettingsDataModelNewFieldFormValues = z.infer<
|
type SettingsDataModelNewFieldFormValues = z.infer<
|
||||||
@ -67,6 +68,7 @@ export const SettingsObjectNewFieldConfigure = () => {
|
|||||||
DEFAULT_ICONS_BY_FIELD_TYPE[fieldType] ?? DEFAULT_ICON_FOR_NEW_FIELD,
|
DEFAULT_ICONS_BY_FIELD_TYPE[fieldType] ?? DEFAULT_ICON_FOR_NEW_FIELD,
|
||||||
label: '',
|
label: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
name: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -134,12 +136,22 @@ export const SettingsObjectNewFieldConfigure = () => {
|
|||||||
|
|
||||||
await createOneRelationMetadata({
|
await createOneRelationMetadata({
|
||||||
relationType: relationFormValues.type,
|
relationType: relationFormValues.type,
|
||||||
field: pick(fieldFormValues, ['icon', 'label', 'description']),
|
field: pick(fieldFormValues, [
|
||||||
|
'icon',
|
||||||
|
'label',
|
||||||
|
'description',
|
||||||
|
'name',
|
||||||
|
'isLabelSyncedWithName',
|
||||||
|
]),
|
||||||
objectMetadataId: activeObjectMetadataItem.id,
|
objectMetadataId: activeObjectMetadataItem.id,
|
||||||
connect: {
|
connect: {
|
||||||
field: {
|
field: {
|
||||||
icon: relationFormValues.field.icon,
|
icon: relationFormValues.field.icon,
|
||||||
label: relationFormValues.field.label,
|
label: relationFormValues.field.label,
|
||||||
|
name:
|
||||||
|
(relationFormValues.field.isLabelSyncedWithName ?? true)
|
||||||
|
? computeMetadataNameFromLabel(relationFormValues.field.label)
|
||||||
|
: relationFormValues.field.name,
|
||||||
},
|
},
|
||||||
objectMetadataId: relationFormValues.objectMetadataId,
|
objectMetadataId: relationFormValues.objectMetadataId,
|
||||||
},
|
},
|
||||||
@ -204,6 +216,9 @@ export const SettingsObjectNewFieldConfigure = () => {
|
|||||||
/>
|
/>
|
||||||
<SettingsDataModelFieldIconLabelForm
|
<SettingsDataModelFieldIconLabelForm
|
||||||
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
|
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
|
||||||
|
canToggleSyncLabelWithName={
|
||||||
|
fieldType !== FieldMetadataType.Relation
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddIsLabelSyncedWithNameToFieldMetadata1733153195498
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddIsLabelSyncedWithNameToFieldMetadata1733153195498';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."fieldMetadata" ADD "isLabelSyncedWithName" boolean NOT NULL DEFAULT false`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "isLabelSyncedWithName"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -145,6 +145,11 @@ export class FieldMetadataDTO<
|
|||||||
|
|
||||||
objectMetadataId: string;
|
objectMetadataId: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Field()
|
||||||
|
isLabelSyncedWithName?: boolean;
|
||||||
|
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
@Field()
|
@Field()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
@ -114,6 +114,9 @@ export class FieldMetadataEntity<
|
|||||||
@Column({ nullable: false, type: 'uuid' })
|
@Column({ nullable: false, type: 'uuid' })
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
isLabelSyncedWithName: boolean;
|
||||||
|
|
||||||
@OneToOne(
|
@OneToOne(
|
||||||
() => RelationMetadataEntity,
|
() => RelationMetadataEntity,
|
||||||
(relation: RelationMetadataEntity) => relation.fromFieldMetadata,
|
(relation: RelationMetadataEntity) => relation.fromFieldMetadata,
|
||||||
|
@ -78,7 +78,7 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
|
|||||||
@Column({ nullable: true, type: 'uuid' })
|
@Column({ nullable: true, type: 'uuid' })
|
||||||
imageIdentifierFieldMetadataId?: string | null;
|
imageIdentifierFieldMetadataId?: string | null;
|
||||||
|
|
||||||
@Column({ default: true })
|
@Column({ default: false })
|
||||||
isLabelSyncedWithName: boolean;
|
isLabelSyncedWithName: boolean;
|
||||||
|
|
||||||
@Column({ nullable: false, type: 'uuid' })
|
@Column({ nullable: false, type: 'uuid' })
|
||||||
|
@ -23,6 +23,13 @@ export class WorkspaceMigrationEnumService {
|
|||||||
tableName: string,
|
tableName: string,
|
||||||
migrationColumn: WorkspaceMigrationColumnAlter,
|
migrationColumn: WorkspaceMigrationColumnAlter,
|
||||||
) {
|
) {
|
||||||
|
const oldEnumTypeName = await this.getEnumTypeName(
|
||||||
|
queryRunner,
|
||||||
|
schemaName,
|
||||||
|
tableName,
|
||||||
|
migrationColumn.currentColumnDefinition.columnName,
|
||||||
|
);
|
||||||
|
|
||||||
// Rename column name
|
// Rename column name
|
||||||
if (
|
if (
|
||||||
migrationColumn.currentColumnDefinition.columnName !==
|
migrationColumn.currentColumnDefinition.columnName !==
|
||||||
@ -37,13 +44,6 @@ export class WorkspaceMigrationEnumService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldEnumTypeName = await this.getEnumTypeName(
|
|
||||||
queryRunner,
|
|
||||||
schemaName,
|
|
||||||
tableName,
|
|
||||||
migrationColumn.currentColumnDefinition.columnName,
|
|
||||||
);
|
|
||||||
|
|
||||||
const columnDefinition = migrationColumn.alteredColumnDefinition;
|
const columnDefinition = migrationColumn.alteredColumnDefinition;
|
||||||
const tempEnumTypeName = `${oldEnumTypeName}_temp`;
|
const tempEnumTypeName = `${oldEnumTypeName}_temp`;
|
||||||
const newEnumTypeName = `${tableName}_${columnDefinition.columnName}_enum`;
|
const newEnumTypeName = `${tableName}_${columnDefinition.columnName}_enum`;
|
||||||
@ -236,7 +236,7 @@ export class WorkspaceMigrationEnumService {
|
|||||||
[schemaName, tableName, columnName],
|
[schemaName, tableName, columnName],
|
||||||
);
|
);
|
||||||
|
|
||||||
const enumTypeName = result[0].udt_name;
|
const enumTypeName = result[0]?.udt_name;
|
||||||
|
|
||||||
if (!enumTypeName) {
|
if (!enumTypeName) {
|
||||||
throw new WorkspaceMigrationException(
|
throw new WorkspaceMigrationException(
|
||||||
|
Loading…
Reference in New Issue
Block a user