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:
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user