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:
Weiko
2024-12-03 13:22:12 +01:00
committed by GitHub
parent 7e4277fbe4
commit 3c7805c6d0
27 changed files with 1118 additions and 125 deletions

View File

@ -4,17 +4,39 @@ import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
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 { IconPicker } from '@/ui/input/components/IconPicker';
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 = (
existingOtherLabels: string[] = [],
) => {
return fieldMetadataItemSchema(existingOtherLabels).pick({
icon: true,
label: true,
});
return fieldMetadataItemSchema(existingOtherLabels)
.pick({
icon: true,
label: true,
})
.merge(
fieldMetadataItemSchema()
.pick({
name: true,
isLabelSyncedWithName: true,
})
.partial(),
);
};
type SettingsDataModelFieldIconLabelFormValues = z.infer<
@ -28,57 +50,182 @@ const StyledInputsContainer = styled.div`
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 = {
disabled?: boolean;
fieldMetadataItem?: FieldMetadataItem;
maxLength?: number;
canToggleSyncLabelWithName?: boolean;
};
export const SettingsDataModelFieldIconLabelForm = ({
canToggleSyncLabelWithName = true,
disabled,
fieldMetadataItem,
maxLength,
}: SettingsDataModelFieldIconLabelFormProps) => {
const {
control,
trigger,
setValue,
watch,
formState: { errors },
} = 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 (
<StyledInputsContainer>
<Controller
name="icon"
control={control}
defaultValue={fieldMetadataItem?.icon ?? 'IconUsers'}
render={({ field: { onChange, value } }) => (
<IconPicker
disabled={disabled}
selectedIconKey={value ?? ''}
onChange={({ iconKey }) => onChange(iconKey)}
variant="primary"
/>
)}
/>
<Controller
name="label"
control={control}
defaultValue={fieldMetadataItem?.label}
render={({ field: { onChange, value } }) => (
<TextInput
placeholder="Employees"
value={value}
onChange={(e) => {
onChange(e);
trigger('label');
}}
error={getErrorMessageFromError(errors.label?.message)}
disabled={disabled}
maxLength={maxLength}
fullWidth
/>
)}
/>
</StyledInputsContainer>
<>
<StyledInputsContainer>
<Controller
name="icon"
control={control}
defaultValue={fieldMetadataItem?.icon ?? 'IconUsers'}
render={({ field: { onChange, value } }) => (
<IconPicker
disabled={disabled}
selectedIconKey={value ?? ''}
onChange={({ iconKey }) => onChange(iconKey)}
variant="primary"
/>
)}
/>
<Controller
name="label"
control={control}
defaultValue={fieldMetadataItem?.label}
render={({ field: { onChange, value } }) => (
<TextInput
placeholder="Employees"
value={value}
onChange={(value) => {
onChange(value);
if (isLabelSyncedWithName === true) {
fillNameFromLabel(value);
}
}}
error={getErrorMessageFromError(errors.label?.message)}
disabled={disabled}
maxLength={maxLength}
fullWidth
/>
)}
/>
</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>
)}
</>
);
};

View File

@ -20,10 +20,20 @@ import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const settingsDataModelFieldRelationFormSchema = z.object({
relation: z.object({
field: fieldMetadataItemSchema().pick({
icon: true,
label: true,
}),
field: fieldMetadataItemSchema()
.pick({
icon: true,
label: true,
})
// NOT SURE IF THIS IS CORRECT
.merge(
fieldMetadataItemSchema()
.pick({
name: true,
isLabelSyncedWithName: true,
})
.partial(),
),
objectMetadataId: z.string().uuid(),
type: z.enum(
Object.keys(RELATION_TYPES) as [