mirror of
https://github.com/lingble/twenty.git
synced 2025-11-02 05:37:56 +00:00
Support custom object renaming (#7504)
This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-5491](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-5491). This ticket was imported from: [TWNTY-5491](https://github.com/twentyhq/twenty/issues/5491) --- ### Description **How To Test:**\ 1. Reset db using `npx nx database:reset twenty-server` on this PR 1. Run both backend and frontend 2. Navigate to `settings/data-model/objects/ `page 3. Select a `Custom `object from the list or create a new `Custom `object 4. Navigate to custom object details page and click on edit button 5. Finally edit the object details. **Issues and bugs** The Typecheck is failing but we could not see this error locally There is a bug after updating the label of a custom object. View title is not updated till refreshing the page. We could not find a consistent way to update this, should we reload the page after editing an object? ### Demo <https://www.loom.com/share/64ecb57efad7498d99085cb11480b5dd?sid=28d0868c-e54f-454d-8432-3f789be9e2b7> ### Refs #5491 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu> Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
committed by
GitHub
parent
c6ef14acc4
commit
414f2ac498
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1160,6 +1160,7 @@ export type UpdateObjectPayload = {
|
|||||||
labelSingular?: InputMaybe<Scalars['String']>;
|
labelSingular?: InputMaybe<Scalars['String']>;
|
||||||
namePlural?: InputMaybe<Scalars['String']>;
|
namePlural?: InputMaybe<Scalars['String']>;
|
||||||
nameSingular?: InputMaybe<Scalars['String']>;
|
nameSingular?: InputMaybe<Scalars['String']>;
|
||||||
|
shouldSyncLabelAndName?: InputMaybe<Scalars['Boolean']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateOneObjectInput = {
|
export type UpdateOneObjectInput = {
|
||||||
@@ -1476,6 +1477,7 @@ export type Object = {
|
|||||||
labelSingular: Scalars['String'];
|
labelSingular: Scalars['String'];
|
||||||
namePlural: Scalars['String'];
|
namePlural: Scalars['String'];
|
||||||
nameSingular: Scalars['String'];
|
nameSingular: Scalars['String'];
|
||||||
|
shouldSyncLabelAndName: Scalars['Boolean'];
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ export const EventRowDynamicComponent = ({
|
|||||||
linkedObjectMetadataItem,
|
linkedObjectMetadataItem,
|
||||||
authorFullName,
|
authorFullName,
|
||||||
}: EventRowDynamicComponentProps) => {
|
}: EventRowDynamicComponentProps) => {
|
||||||
const [eventName] = event.name.split('.');
|
switch (linkedObjectMetadataItem?.nameSingular) {
|
||||||
|
|
||||||
switch (eventName) {
|
|
||||||
case 'calendarEvent':
|
case 'calendarEvent':
|
||||||
return (
|
return (
|
||||||
<EventRowCalendarEvent
|
<EventRowCalendarEvent
|
||||||
@@ -58,7 +56,7 @@ export const EventRowDynamicComponent = ({
|
|||||||
authorFullName={authorFullName}
|
authorFullName={authorFullName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'linked-task':
|
case 'task':
|
||||||
return (
|
return (
|
||||||
<EventRowActivity
|
<EventRowActivity
|
||||||
labelIdentifierValue={labelIdentifierValue}
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
@@ -69,7 +67,7 @@ export const EventRowDynamicComponent = ({
|
|||||||
objectNameSingular={CoreObjectNameSingular.Task}
|
objectNameSingular={CoreObjectNameSingular.Task}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'linked-note':
|
case 'note':
|
||||||
return (
|
return (
|
||||||
<EventRowActivity
|
<EventRowActivity
|
||||||
labelIdentifierValue={labelIdentifierValue}
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
@@ -80,7 +78,7 @@ export const EventRowDynamicComponent = ({
|
|||||||
objectNameSingular={CoreObjectNameSingular.Note}
|
objectNameSingular={CoreObjectNameSingular.Note}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case mainObjectMetadataItem?.nameSingular:
|
default:
|
||||||
return (
|
return (
|
||||||
<EventRowMainObject
|
<EventRowMainObject
|
||||||
labelIdentifierValue={labelIdentifierValue}
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
@@ -90,9 +88,5 @@ export const EventRowDynamicComponent = ({
|
|||||||
authorFullName={authorFullName}
|
authorFullName={authorFullName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Cannot find event component for event name ${eventName}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ describe('useCommandMenu', () => {
|
|||||||
namePlural: 'tasks',
|
namePlural: 'tasks',
|
||||||
labelSingular: 'Task',
|
labelSingular: 'Task',
|
||||||
labelPlural: 'Tasks',
|
labelPlural: 'Tasks',
|
||||||
|
shouldSyncLabelAndName: true,
|
||||||
description: 'A task',
|
description: 'A task',
|
||||||
icon: 'IconCheckbox',
|
icon: 'IconCheckbox',
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
|||||||
updatedAt
|
updatedAt
|
||||||
labelIdentifierFieldMetadataId
|
labelIdentifierFieldMetadataId
|
||||||
imageIdentifierFieldMetadataId
|
imageIdentifierFieldMetadataId
|
||||||
|
shouldSyncLabelAndName
|
||||||
indexMetadatas(paging: { first: 100 }) {
|
indexMetadatas(paging: { first: 100 }) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ describe('objectMetadataItemSchema', () => {
|
|||||||
namePlural: 'notCamelCase',
|
namePlural: 'notCamelCase',
|
||||||
nameSingular: 'notCamelCase',
|
nameSingular: 'notCamelCase',
|
||||||
updatedAt: 'invalid date',
|
updatedAt: 'invalid date',
|
||||||
|
shouldSyncLabelAndName: 'not a boolean',
|
||||||
};
|
};
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ export const objectMetadataItemSchema = z.object({
|
|||||||
namePlural: camelCaseStringSchema,
|
namePlural: camelCaseStringSchema,
|
||||||
nameSingular: camelCaseStringSchema,
|
nameSingular: camelCaseStringSchema,
|
||||||
updatedAt: z.string().datetime(),
|
updatedAt: z.string().datetime(),
|
||||||
|
shouldSyncLabelAndName: z.boolean(),
|
||||||
}) satisfies z.ZodType<ObjectMetadataItem>;
|
}) satisfies z.ZodType<ObjectMetadataItem>;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const objectMetadataItem: ObjectMetadataItem = {
|
|||||||
isRemote: false,
|
isRemote: false,
|
||||||
labelPlural: 'object1s',
|
labelPlural: 'object1s',
|
||||||
labelSingular: 'object1',
|
labelSingular: 'object1',
|
||||||
|
shouldSyncLabelAndName: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('turnSortsIntoOrderBy', () => {
|
describe('turnSortsIntoOrderBy', () => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ describe('useLimitPerMetadataItem', () => {
|
|||||||
namePlural: 'namePlural',
|
namePlural: 'namePlural',
|
||||||
nameSingular: 'nameSingular',
|
nameSingular: 'nameSingular',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
|
shouldSyncLabelAndName: false,
|
||||||
fields: [],
|
fields: [],
|
||||||
indexMetadatas: [],
|
indexMetadatas: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const objectData: ObjectMetadataItem[] = [
|
|||||||
labelSingular: 'labelSingular',
|
labelSingular: 'labelSingular',
|
||||||
namePlural: 'namePlural',
|
namePlural: 'namePlural',
|
||||||
nameSingular: 'nameSingular',
|
nameSingular: 'nameSingular',
|
||||||
|
shouldSyncLabelAndName: false,
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,21 +1,45 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
||||||
import { OBJECT_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/ObjectNameMaximumLength';
|
import { OBJECT_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/ObjectNameMaximumLength';
|
||||||
|
import { SyncObjectLabelAndNameToggle } from '@/settings/data-model/objects/forms/components/SyncObjectLabelAndNameToggle';
|
||||||
|
import { useExpandedHeightAnimation } from '@/settings/hooks/useExpandedHeightAnimation';
|
||||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||||
import { TextArea } from '@/ui/input/components/TextArea';
|
import { TextArea } from '@/ui/input/components/TextArea';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { plural } from 'pluralize';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import {
|
||||||
|
AppTooltip,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconTool,
|
||||||
|
MAIN_COLORS,
|
||||||
|
TooltipDelay,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const settingsDataModelObjectAboutFormSchema =
|
export const settingsDataModelObjectAboutFormSchema = objectMetadataItemSchema
|
||||||
objectMetadataItemSchema.pick({
|
.pick({
|
||||||
description: true,
|
description: true,
|
||||||
icon: true,
|
icon: true,
|
||||||
labelPlural: true,
|
labelPlural: true,
|
||||||
labelSingular: true,
|
labelSingular: true,
|
||||||
});
|
})
|
||||||
|
.merge(
|
||||||
|
objectMetadataItemSchema
|
||||||
|
.pick({
|
||||||
|
nameSingular: true,
|
||||||
|
namePlural: true,
|
||||||
|
shouldSyncLabelAndName: true,
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
);
|
||||||
|
|
||||||
type SettingsDataModelObjectAboutFormValues = z.infer<
|
type SettingsDataModelObjectAboutFormValues = z.infer<
|
||||||
typeof settingsDataModelObjectAboutFormSchema
|
typeof settingsDataModelObjectAboutFormSchema
|
||||||
@@ -34,6 +58,41 @@ const StyledInputsContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledInputContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSectionWrapper = styled.div`
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAdvancedSettingsSectionInputWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAdvancedSettingsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconToolContainer = styled.div`
|
||||||
|
border-right: 1px solid ${MAIN_COLORS.yellow};
|
||||||
|
display: flex;
|
||||||
|
left: ${({ theme }) => theme.spacing(-5)};
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconTool = styled(IconTool)`
|
||||||
|
margin-right: ${({ theme }) => theme.spacing(0.5)};
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledLabel = styled.span`
|
const StyledLabel = styled.span`
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
font-size: ${({ theme }) => theme.font.size.xs};
|
font-size: ${({ theme }) => theme.font.size.xs};
|
||||||
@@ -41,83 +100,247 @@ const StyledLabel = styled.span`
|
|||||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInputContainer = styled.div`
|
const infoCircleElementId = 'info-circle-id';
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsDataModelObjectAboutForm = ({
|
export const SettingsDataModelObjectAboutForm = ({
|
||||||
disabled,
|
disabled,
|
||||||
disableNameEdit,
|
disableNameEdit,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
}: SettingsDataModelObjectAboutFormProps) => {
|
}: SettingsDataModelObjectAboutFormProps) => {
|
||||||
const { control } = useFormContext<SettingsDataModelObjectAboutFormValues>();
|
const { control, watch, setValue } =
|
||||||
|
useFormContext<SettingsDataModelObjectAboutFormValues>();
|
||||||
|
const theme = useTheme();
|
||||||
|
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||||
|
const { contentRef, motionAnimationVariants } = useExpandedHeightAnimation(
|
||||||
|
isAdvancedModeEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldSyncLabelAndName = watch('shouldSyncLabelAndName');
|
||||||
|
const labelSingular = watch('labelSingular');
|
||||||
|
const labelPlural = watch('labelPlural');
|
||||||
|
const apiNameTooltipText = shouldSyncLabelAndName
|
||||||
|
? '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 fillLabelPlural = (labelSingular: string) => {
|
||||||
|
const newLabelPluralValue = isDefined(labelSingular)
|
||||||
|
? plural(labelSingular)
|
||||||
|
: '';
|
||||||
|
setValue('labelPlural', newLabelPluralValue, {
|
||||||
|
shouldDirty: isDefined(labelSingular) ? true : false,
|
||||||
|
});
|
||||||
|
if (shouldSyncLabelAndName === true) {
|
||||||
|
fillNamePluralFromLabelPlural(newLabelPluralValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fillNameSingularFromLabelSingular = (labelSingular: string) => {
|
||||||
|
isDefined(labelSingular) &&
|
||||||
|
setValue(
|
||||||
|
'nameSingular',
|
||||||
|
computeMetadataNameFromLabelOrThrow(labelSingular),
|
||||||
|
{ shouldDirty: false },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fillNamePluralFromLabelPlural = (labelPlural: string) => {
|
||||||
|
isDefined(labelPlural) &&
|
||||||
|
setValue('namePlural', computeMetadataNameFromLabelOrThrow(labelPlural), {
|
||||||
|
shouldDirty: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledInputsContainer>
|
<StyledSectionWrapper>
|
||||||
<StyledInputContainer>
|
<StyledInputsContainer>
|
||||||
<StyledLabel>Icon</StyledLabel>
|
<StyledInputContainer>
|
||||||
|
<StyledLabel>Icon</StyledLabel>
|
||||||
|
<Controller
|
||||||
|
name="icon"
|
||||||
|
control={control}
|
||||||
|
defaultValue={objectMetadataItem?.icon ?? 'IconListNumbers'}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<IconPicker
|
||||||
|
disabled={disabled}
|
||||||
|
selectedIconKey={value}
|
||||||
|
onChange={({ iconKey }) => onChange(iconKey)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledInputContainer>
|
||||||
<Controller
|
<Controller
|
||||||
name="icon"
|
key={`object-labelSingular-text-input`}
|
||||||
|
name={'labelSingular'}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={objectMetadataItem?.icon ?? 'IconListNumbers'}
|
defaultValue={objectMetadataItem?.labelSingular}
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<IconPicker
|
|
||||||
disabled={disabled}
|
|
||||||
selectedIconKey={value}
|
|
||||||
onChange={({ iconKey }) => onChange(iconKey)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</StyledInputContainer>
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
label: 'Singular',
|
|
||||||
fieldName: 'labelSingular' as const,
|
|
||||||
placeholder: 'Listing',
|
|
||||||
defaultValue: objectMetadataItem?.labelSingular,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Plural',
|
|
||||||
fieldName: 'labelPlural' as const,
|
|
||||||
placeholder: 'Listings',
|
|
||||||
defaultValue: objectMetadataItem?.labelPlural,
|
|
||||||
},
|
|
||||||
].map(({ defaultValue, fieldName, label, placeholder }) => (
|
|
||||||
<Controller
|
|
||||||
key={`object-${fieldName}-text-input`}
|
|
||||||
name={fieldName}
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
label={label}
|
label={'Singular'}
|
||||||
placeholder={placeholder}
|
placeholder={'Listing'}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={(value) => {
|
||||||
|
onChange(value);
|
||||||
|
fillLabelPlural(value);
|
||||||
|
if (shouldSyncLabelAndName === true) {
|
||||||
|
fillNameSingularFromLabelSingular(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
disabled={disabled || disableNameEdit}
|
disabled={disabled || disableNameEdit}
|
||||||
fullWidth
|
fullWidth
|
||||||
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))}
|
<Controller
|
||||||
</StyledInputsContainer>
|
key={`object-labelPlural-text-input`}
|
||||||
<Controller
|
name={'labelPlural'}
|
||||||
name="description"
|
control={control}
|
||||||
control={control}
|
defaultValue={objectMetadataItem?.labelPlural}
|
||||||
defaultValue={objectMetadataItem?.description ?? null}
|
render={({ field: { onChange, value } }) => (
|
||||||
render={({ field: { onChange, value } }) => (
|
<TextInput
|
||||||
<TextArea
|
label={'Plural'}
|
||||||
placeholder="Write a description"
|
placeholder={'Listings'}
|
||||||
minRows={4}
|
value={value}
|
||||||
value={value ?? undefined}
|
onChange={(value) => {
|
||||||
onChange={(nextValue) => onChange(nextValue ?? null)}
|
onChange(value);
|
||||||
disabled={disabled}
|
if (shouldSyncLabelAndName === true) {
|
||||||
|
fillNamePluralFromLabelPlural(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled || disableNameEdit}
|
||||||
|
fullWidth
|
||||||
|
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
<Controller
|
||||||
|
name="description"
|
||||||
|
control={control}
|
||||||
|
defaultValue={objectMetadataItem?.description ?? null}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<TextArea
|
||||||
|
placeholder="Write a description"
|
||||||
|
minRows={4}
|
||||||
|
value={value ?? undefined}
|
||||||
|
onChange={(nextValue) => onChange(nextValue ?? null)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledSectionWrapper>
|
||||||
|
<AnimatePresence>
|
||||||
|
{isAdvancedModeEnabled && (
|
||||||
|
<motion.div
|
||||||
|
ref={contentRef}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={motionAnimationVariants}
|
||||||
|
>
|
||||||
|
<StyledAdvancedSettingsContainer>
|
||||||
|
<StyledIconToolContainer>
|
||||||
|
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
||||||
|
</StyledIconToolContainer>
|
||||||
|
<StyledAdvancedSettingsSectionInputWrapper>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
label: 'API Name (Singular)',
|
||||||
|
fieldName: 'nameSingular' as const,
|
||||||
|
placeholder: 'listing',
|
||||||
|
defaultValue: objectMetadataItem?.nameSingular,
|
||||||
|
disabled:
|
||||||
|
disabled || disableNameEdit || shouldSyncLabelAndName,
|
||||||
|
tooltip: apiNameTooltipText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'API Name (Plural)',
|
||||||
|
fieldName: 'namePlural' as const,
|
||||||
|
placeholder: 'listings',
|
||||||
|
defaultValue: objectMetadataItem?.namePlural,
|
||||||
|
disabled:
|
||||||
|
disabled || disableNameEdit || shouldSyncLabelAndName,
|
||||||
|
tooltip: apiNameTooltipText,
|
||||||
|
},
|
||||||
|
].map(
|
||||||
|
({
|
||||||
|
defaultValue,
|
||||||
|
fieldName,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
}) => (
|
||||||
|
<StyledInputContainer
|
||||||
|
key={`object-${fieldName}-text-input`}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name={fieldName}
|
||||||
|
control={control}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
label={label}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
fullWidth
|
||||||
|
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
||||||
|
RightIcon={() =>
|
||||||
|
tooltip && (
|
||||||
|
<>
|
||||||
|
<IconInfoCircle
|
||||||
|
id={infoCircleElementId + fieldName}
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
color={theme.font.color.tertiary}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AppTooltip
|
||||||
|
anchorSelect={`#${infoCircleElementId}${fieldName}`}
|
||||||
|
content={tooltip}
|
||||||
|
offset={5}
|
||||||
|
noArrow
|
||||||
|
place="bottom"
|
||||||
|
positionStrategy="absolute"
|
||||||
|
delay={TooltipDelay.shortDelay}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledInputContainer>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
name="shouldSyncLabelAndName"
|
||||||
|
control={control}
|
||||||
|
defaultValue={
|
||||||
|
objectMetadataItem?.shouldSyncLabelAndName ?? true
|
||||||
|
}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<SyncObjectLabelAndNameToggle
|
||||||
|
value={value ?? true}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange(value);
|
||||||
|
if (value === true) {
|
||||||
|
fillNamePluralFromLabelPlural(labelPlural);
|
||||||
|
fillNameSingularFromLabelSingular(labelSingular);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledAdvancedSettingsSectionInputWrapper>
|
||||||
|
</StyledAdvancedSettingsContainer>
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
/>
|
</AnimatePresence>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconRefresh, MAIN_COLORS, Toggle } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledToggleContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconRefreshContainer = styled.div`
|
||||||
|
border: 2px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: ${({ theme }) => theme.spacing(3)};
|
||||||
|
width: ${({ theme }) => theme.spacing(8)};
|
||||||
|
height: ${({ theme }) => theme.spacing(8)};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitleContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitle = styled.h2`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDescription = styled.h3`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
margin: 0;
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type SyncObjectLabelAndNameToggleProps = {
|
||||||
|
value: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SyncObjectLabelAndNameToggle = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: SyncObjectLabelAndNameToggleProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<StyledToggleContainer>
|
||||||
|
<StyledTitleContainer>
|
||||||
|
<StyledIconRefreshContainer>
|
||||||
|
<IconRefresh size={22.5} color={theme.font.color.tertiary} />
|
||||||
|
</StyledIconRefreshContainer>
|
||||||
|
<div>
|
||||||
|
<StyledTitle>Synchronize Objects Labels and API Names</StyledTitle>
|
||||||
|
<StyledDescription>
|
||||||
|
Should changing an object's label also change the API?
|
||||||
|
</StyledDescription>
|
||||||
|
</div>
|
||||||
|
</StyledTitleContainer>
|
||||||
|
<Toggle onChange={onChange} color={MAIN_COLORS.yellow} value={value} />
|
||||||
|
</StyledToggleContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,6 +12,9 @@ describe('settingsCreateObjectInputSchema', () => {
|
|||||||
icon: 'IconPlus',
|
icon: 'IconPlus',
|
||||||
labelPlural: ' Labels ',
|
labelPlural: ' Labels ',
|
||||||
labelSingular: 'Label ',
|
labelSingular: 'Label ',
|
||||||
|
namePlural: 'namePlural',
|
||||||
|
nameSingular: 'nameSingular',
|
||||||
|
shouldSyncLabelAndName: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// When
|
// When
|
||||||
@@ -24,8 +27,9 @@ describe('settingsCreateObjectInputSchema', () => {
|
|||||||
icon: validInput.icon,
|
icon: validInput.icon,
|
||||||
labelPlural: 'Labels',
|
labelPlural: 'Labels',
|
||||||
labelSingular: 'Label',
|
labelSingular: 'Label',
|
||||||
namePlural: 'labels',
|
namePlural: 'namePlural',
|
||||||
nameSingular: 'label',
|
nameSingular: 'nameSingular',
|
||||||
|
shouldSyncLabelAndName: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ describe('settingsUpdateObjectInputSchema', () => {
|
|||||||
icon: 'IconName',
|
icon: 'IconName',
|
||||||
labelPlural: 'Labels Plural ',
|
labelPlural: 'Labels Plural ',
|
||||||
labelSingular: ' Label Singular',
|
labelSingular: ' Label Singular',
|
||||||
|
namePlural: 'namePlural',
|
||||||
|
nameSingular: 'nameSingular',
|
||||||
labelIdentifierFieldMetadataId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
labelIdentifierFieldMetadataId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,8 +28,8 @@ describe('settingsUpdateObjectInputSchema', () => {
|
|||||||
labelIdentifierFieldMetadataId: validInput.labelIdentifierFieldMetadataId,
|
labelIdentifierFieldMetadataId: validInput.labelIdentifierFieldMetadataId,
|
||||||
labelPlural: 'Labels Plural',
|
labelPlural: 'Labels Plural',
|
||||||
labelSingular: 'Label Singular',
|
labelSingular: 'Label Singular',
|
||||||
namePlural: 'labelsPlural',
|
namePlural: 'namePlural',
|
||||||
nameSingular: 'labelSingular',
|
nameSingular: 'nameSingular',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
import { settingsDataModelObjectAboutFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||||
import { CreateObjectInput } from '~/generated-metadata/graphql';
|
import { CreateObjectInput } from '~/generated-metadata/graphql';
|
||||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
|
||||||
export const settingsCreateObjectInputSchema = objectMetadataItemSchema
|
export const settingsCreateObjectInputSchema =
|
||||||
.pick({
|
settingsDataModelObjectAboutFormSchema.transform<CreateObjectInput>(
|
||||||
description: true,
|
(values) => ({
|
||||||
icon: true,
|
...values,
|
||||||
labelPlural: true,
|
nameSingular:
|
||||||
labelSingular: true,
|
values.nameSingular ??
|
||||||
})
|
computeMetadataNameFromLabelOrThrow(values.labelSingular),
|
||||||
.transform<CreateObjectInput>((value) => ({
|
namePlural:
|
||||||
...value,
|
values.namePlural ??
|
||||||
nameSingular: computeMetadataNameFromLabelOrThrow(value.labelSingular),
|
computeMetadataNameFromLabelOrThrow(values.labelPlural),
|
||||||
namePlural: computeMetadataNameFromLabelOrThrow(value.labelPlural),
|
shouldSyncLabelAndName: values.shouldSyncLabelAndName ?? true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
||||||
import { UpdateObjectPayload } from '~/generated-metadata/graphql';
|
import { settingsDataModelObjectAboutFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
|
||||||
|
|
||||||
export const settingsUpdateObjectInputSchema = objectMetadataItemSchema
|
export const settingsUpdateObjectInputSchema =
|
||||||
.pick({
|
settingsDataModelObjectAboutFormSchema
|
||||||
description: true,
|
.merge(
|
||||||
icon: true,
|
objectMetadataItemSchema.pick({
|
||||||
imageIdentifierFieldMetadataId: true,
|
imageIdentifierFieldMetadataId: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
labelIdentifierFieldMetadataId: true,
|
labelIdentifierFieldMetadataId: true,
|
||||||
labelPlural: true,
|
}),
|
||||||
labelSingular: true,
|
)
|
||||||
})
|
.partial();
|
||||||
.partial()
|
|
||||||
.transform<UpdateObjectPayload>((value) => ({
|
|
||||||
...value,
|
|
||||||
nameSingular: value.labelSingular
|
|
||||||
? computeMetadataNameFromLabelOrThrow(value.labelSingular)
|
|
||||||
: undefined,
|
|
||||||
namePlural: value.labelPlural
|
|
||||||
? computeMetadataNameFromLabelOrThrow(value.labelPlural)
|
|
||||||
: undefined,
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem';
|
||||||
|
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
@@ -20,13 +21,16 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
|
|||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } 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 { useSetRecoilState } from 'recoil';
|
||||||
import { Button, H2Title, IconArchive } from 'twenty-ui';
|
import { Button, H2Title, IconArchive } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
|
||||||
const objectEditFormSchema = z
|
const objectEditFormSchema = z
|
||||||
.object({})
|
.object({})
|
||||||
@@ -45,6 +49,9 @@ export const SettingsObjectEdit = () => {
|
|||||||
const { findActiveObjectMetadataItemBySlug } =
|
const { findActiveObjectMetadataItemBySlug } =
|
||||||
useFilteredObjectMetadataItems();
|
useFilteredObjectMetadataItems();
|
||||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
|
const { lastVisitedObjectMetadataItemId } =
|
||||||
|
useLastVisitedObjectMetadataItem();
|
||||||
|
const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
|
||||||
|
|
||||||
const activeObjectMetadataItem =
|
const activeObjectMetadataItem =
|
||||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||||
@@ -56,6 +63,10 @@ export const SettingsObjectEdit = () => {
|
|||||||
resolver: zodResolver(objectEditFormSchema),
|
resolver: zodResolver(objectEditFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||||
|
navigationMemorizedUrlState,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
||||||
}, [activeObjectMetadataItem, navigate]);
|
}, [activeObjectMetadataItem, navigate]);
|
||||||
@@ -65,25 +76,72 @@ export const SettingsObjectEdit = () => {
|
|||||||
const { isDirty, isValid, isSubmitting } = formConfig.formState;
|
const { isDirty, isValid, isSubmitting } = formConfig.formState;
|
||||||
const canSave = isDirty && isValid && !isSubmitting;
|
const canSave = isDirty && isValid && !isSubmitting;
|
||||||
|
|
||||||
const handleSave = async (
|
const getUpdatePayload = (
|
||||||
formValues: SettingsDataModelObjectEditFormValues,
|
formValues: SettingsDataModelObjectEditFormValues,
|
||||||
) => {
|
) => {
|
||||||
|
let values = formValues;
|
||||||
|
if (
|
||||||
|
formValues.shouldSyncLabelAndName ??
|
||||||
|
activeObjectMetadataItem.shouldSyncLabelAndName
|
||||||
|
) {
|
||||||
|
values = {
|
||||||
|
...values,
|
||||||
|
...(values.labelSingular
|
||||||
|
? {
|
||||||
|
nameSingular: computeMetadataNameFromLabelOrThrow(
|
||||||
|
formValues.labelSingular,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(values.labelPlural
|
||||||
|
? {
|
||||||
|
namePlural: computeMetadataNameFromLabelOrThrow(
|
||||||
|
formValues.labelPlural,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const dirtyFieldKeys = Object.keys(
|
const dirtyFieldKeys = Object.keys(
|
||||||
formConfig.formState.dirtyFields,
|
formConfig.formState.dirtyFields,
|
||||||
) as (keyof SettingsDataModelObjectEditFormValues)[];
|
) as (keyof SettingsDataModelObjectEditFormValues)[];
|
||||||
|
|
||||||
|
return settingsUpdateObjectInputSchema.parse(
|
||||||
|
pick(values, [
|
||||||
|
...dirtyFieldKeys,
|
||||||
|
...(values.namePlural ? ['namePlural'] : []),
|
||||||
|
...(values.nameSingular ? ['nameSingular'] : []),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (
|
||||||
|
formValues: SettingsDataModelObjectEditFormValues,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
|
const updatePayload = getUpdatePayload(formValues);
|
||||||
await updateOneObjectMetadataItem({
|
await updateOneObjectMetadataItem({
|
||||||
idToUpdate: activeObjectMetadataItem.id,
|
idToUpdate: activeObjectMetadataItem.id,
|
||||||
updatePayload: settingsUpdateObjectInputSchema.parse(
|
updatePayload,
|
||||||
pick(formValues, dirtyFieldKeys),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const objectNamePluralForRedirection =
|
||||||
|
updatePayload.namePlural ?? activeObjectMetadataItem.namePlural;
|
||||||
|
|
||||||
|
if (lastVisitedObjectMetadataItemId === activeObjectMetadataItem.id) {
|
||||||
|
const lastVisitedView = getLastVisitedViewIdFromObjectMetadataItemId(
|
||||||
|
activeObjectMetadataItem.id,
|
||||||
|
);
|
||||||
|
setNavigationMemorizedUrl(
|
||||||
|
`/objects/${objectNamePluralForRedirection}?view=${lastVisitedView}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
`${settingsObjectsPagePath}/${getObjectSlug({
|
`${settingsObjectsPagePath}/${getObjectSlug({
|
||||||
...formValues,
|
...updatePayload,
|
||||||
namePlural: formValues.labelPlural,
|
namePlural: objectNamePluralForRedirection,
|
||||||
})}`,
|
})}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -142,7 +200,7 @@ export const SettingsObjectEdit = () => {
|
|||||||
/>
|
/>
|
||||||
<SettingsDataModelObjectAboutForm
|
<SettingsDataModelObjectAboutForm
|
||||||
disabled={!activeObjectMetadataItem.isCustom}
|
disabled={!activeObjectMetadataItem.isCustom}
|
||||||
disableNameEdit
|
disableNameEdit={!activeObjectMetadataItem.isCustom}
|
||||||
objectMetadataItem={activeObjectMetadataItem}
|
objectMetadataItem={activeObjectMetadataItem}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -2,5 +2,8 @@ import { METADATA_NAME_VALID_PATTERN } from '~/pages/settings/data-model/constan
|
|||||||
import { transliterateAndFormatOrThrow } from '~/pages/settings/data-model/utils/transliterate-and-format.utils';
|
import { transliterateAndFormatOrThrow } from '~/pages/settings/data-model/utils/transliterate-and-format.utils';
|
||||||
|
|
||||||
export const computeMetadataNameFromLabelOrThrow = (label: string): string => {
|
export const computeMetadataNameFromLabelOrThrow = (label: string): string => {
|
||||||
|
if (label === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
return transliterateAndFormatOrThrow(label, METADATA_NAME_VALID_PATTERN);
|
return transliterateAndFormatOrThrow(label, METADATA_NAME_VALID_PATTERN);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "fd99213f-1b50-4d72-8708-75ba80097736",
|
"id": "fd99213f-1b50-4d72-8708-75ba80097736",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "noteTarget",
|
"nameSingular": "noteTarget",
|
||||||
@@ -645,6 +646,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "f98ea433-1b70-46d3-aefa-43eb369925d2",
|
"id": "f98ea433-1b70-46d3-aefa-43eb369925d2",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "messageThread",
|
"nameSingular": "messageThread",
|
||||||
@@ -830,6 +832,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "f2414140-86ea-4fa3-bc63-ca5dab9f9044",
|
"id": "f2414140-86ea-4fa3-bc63-ca5dab9f9044",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "workspaceMember",
|
"nameSingular": "workspaceMember",
|
||||||
@@ -1864,6 +1867,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "f04a7171-564a-44ec-a061-63938e29f0c5",
|
"id": "f04a7171-564a-44ec-a061-63938e29f0c5",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "apiKey",
|
"nameSingular": "apiKey",
|
||||||
@@ -2069,6 +2073,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "edfd2da3-26e4-4e84-b490-c0790848dc23",
|
"id": "edfd2da3-26e4-4e84-b490-c0790848dc23",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "taskTarget",
|
"nameSingular": "taskTarget",
|
||||||
@@ -2706,6 +2711,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "eda936a5-97b9-4b9f-986a-d8e19e8ea882",
|
"id": "eda936a5-97b9-4b9f-986a-d8e19e8ea882",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "comment",
|
"nameSingular": "comment",
|
||||||
@@ -3079,6 +3085,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "e5915d30-4425-4c4c-a9c4-1b4bff20c469",
|
"id": "e5915d30-4425-4c4c-a9c4-1b4bff20c469",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "workflowVersion",
|
"nameSingular": "workflowVersion",
|
||||||
@@ -3521,6 +3528,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "d828bda6-68e2-47f0-b0aa-b810b1f33981",
|
"id": "d828bda6-68e2-47f0-b0aa-b810b1f33981",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "connectedAccount",
|
"nameSingular": "connectedAccount",
|
||||||
@@ -4052,6 +4060,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "d00ff1e9-774a-4b08-87fb-03d37c24f174",
|
"id": "d00ff1e9-774a-4b08-87fb-03d37c24f174",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "attachment",
|
"nameSingular": "attachment",
|
||||||
@@ -5082,6 +5091,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "cb8c8d67-16c0-4a38-a919-b375845abf42",
|
"id": "cb8c8d67-16c0-4a38-a919-b375845abf42",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "favorite",
|
"nameSingular": "favorite",
|
||||||
@@ -6157,6 +6167,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "c55193eb-042d-42d5-a6a7-8263fd1433a2",
|
"id": "c55193eb-042d-42d5-a6a7-8263fd1433a2",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "viewSort",
|
"nameSingular": "viewSort",
|
||||||
@@ -6492,6 +6503,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "c46916fc-0528-4331-9766-6ac2247a70fb",
|
"id": "c46916fc-0528-4331-9766-6ac2247a70fb",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "view",
|
"nameSingular": "view",
|
||||||
@@ -7016,6 +7028,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "be13cda6-aff5-4003-8fe9-e936011b3325",
|
"id": "be13cda6-aff5-4003-8fe9-e936011b3325",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "opportunity",
|
"nameSingular": "opportunity",
|
||||||
@@ -7908,6 +7921,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "b74e80b0-7132-469f-bbd9-6e6fc12f04f8",
|
"id": "b74e80b0-7132-469f-bbd9-6e6fc12f04f8",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "person",
|
"nameSingular": "person",
|
||||||
@@ -9058,6 +9072,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "b6e22795-68e7-4d18-a242-545afea5a8a9",
|
"id": "b6e22795-68e7-4d18-a242-545afea5a8a9",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "timelineActivity",
|
"nameSingular": "timelineActivity",
|
||||||
@@ -10068,6 +10083,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "af56ee43-5666-482f-a980-434fefac00c7",
|
"id": "af56ee43-5666-482f-a980-434fefac00c7",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "calendarEventParticipant",
|
"nameSingular": "calendarEventParticipant",
|
||||||
@@ -10640,6 +10656,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "981fd8a9-37a2-4742-98c1-08509d995bd3",
|
"id": "981fd8a9-37a2-4742-98c1-08509d995bd3",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "calendarEvent",
|
"nameSingular": "calendarEvent",
|
||||||
@@ -11154,6 +11171,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "92b529f1-b82b-4352-a0d5-18f32f8e47ab",
|
"id": "92b529f1-b82b-4352-a0d5-18f32f8e47ab",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "messageChannel",
|
"nameSingular": "messageChannel",
|
||||||
@@ -11901,6 +11919,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "8cceadc4-de6b-4ecf-8324-82c6b4eec077",
|
"id": "8cceadc4-de6b-4ecf-8324-82c6b4eec077",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "calendarChannel",
|
"nameSingular": "calendarChannel",
|
||||||
@@ -12575,6 +12594,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "8ae98b12-2ef6-4c20-adc6-240857dd7343",
|
"id": "8ae98b12-2ef6-4c20-adc6-240857dd7343",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "blocklist",
|
"nameSingular": "blocklist",
|
||||||
@@ -12836,6 +12856,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "88f29168-a15b-4330-89a1-680581a2e86b",
|
"id": "88f29168-a15b-4330-89a1-680581a2e86b",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "viewFilter",
|
"nameSingular": "viewFilter",
|
||||||
@@ -13166,6 +13187,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "823e8b9d-1947-48f9-9f43-116a2cbceba3",
|
"id": "823e8b9d-1947-48f9-9f43-116a2cbceba3",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "webhook",
|
"nameSingular": "webhook",
|
||||||
@@ -13371,6 +13393,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "7cab9c82-929f-4ea3-98e1-5c221a12263d",
|
"id": "7cab9c82-929f-4ea3-98e1-5c221a12263d",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "workflow",
|
"nameSingular": "workflow",
|
||||||
@@ -13814,6 +13837,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "6edf5dd8-ee31-42ec-80f9-728b01c50ff4",
|
"id": "6edf5dd8-ee31-42ec-80f9-728b01c50ff4",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "messageParticipant",
|
"nameSingular": "messageParticipant",
|
||||||
@@ -14340,6 +14364,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "6a09bc08-33ae-4321-868a-30064279097f",
|
"id": "6a09bc08-33ae-4321-868a-30064279097f",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "note",
|
"nameSingular": "note",
|
||||||
@@ -14767,6 +14792,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "681f89d7-0581-42b0-b97d-870e3b2a8359",
|
"id": "681f89d7-0581-42b0-b97d-870e3b2a8359",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "task",
|
"nameSingular": "task",
|
||||||
@@ -15375,6 +15401,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "673b8cb8-44c1-4c20-9834-7c35d44fd180",
|
"id": "673b8cb8-44c1-4c20-9834-7c35d44fd180",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "message",
|
"nameSingular": "message",
|
||||||
@@ -15803,6 +15830,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "65cce76e-0f4c-4de1-a68a-6cadce4d000e",
|
"id": "65cce76e-0f4c-4de1-a68a-6cadce4d000e",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "workflowEventListener",
|
"nameSingular": "workflowEventListener",
|
||||||
@@ -16064,6 +16092,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "631882fd-28e8-4a87-8ceb-f8217006a620",
|
"id": "631882fd-28e8-4a87-8ceb-f8217006a620",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "messageChannelMessageAssociation",
|
"nameSingular": "messageChannelMessageAssociation",
|
||||||
@@ -16509,6 +16538,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "5a1aa92b-1ee9-4a7e-ab08-ca8c1e462d16",
|
"id": "5a1aa92b-1ee9-4a7e-ab08-ca8c1e462d16",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "activity",
|
"nameSingular": "activity",
|
||||||
@@ -17144,6 +17174,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "50f61b05-868d-425b-ab3f-c085e1652d82",
|
"id": "50f61b05-868d-425b-ab3f-c085e1652d82",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "viewField",
|
"nameSingular": "viewField",
|
||||||
@@ -17502,6 +17533,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "45b7e1cf-792c-45fa-8d6a-0d5e67e1fa42",
|
"id": "45b7e1cf-792c-45fa-8d6a-0d5e67e1fa42",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "workflowRun",
|
"nameSingular": "workflowRun",
|
||||||
@@ -18034,6 +18066,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "43fe0e45-b323-4b6e-ab98-1d9fe30c9af9",
|
"id": "43fe0e45-b323-4b6e-ab98-1d9fe30c9af9",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "activityTarget",
|
"nameSingular": "activityTarget",
|
||||||
@@ -18671,6 +18704,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "39d5f2b7-03ce-41e7-afe9-7710aeb766a2",
|
"id": "39d5f2b7-03ce-41e7-afe9-7710aeb766a2",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "company",
|
"nameSingular": "company",
|
||||||
@@ -19757,6 +19791,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "2590029a-05d7-4908-8b7a-a253967068a1",
|
"id": "2590029a-05d7-4908-8b7a-a253967068a1",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "auditLog",
|
"nameSingular": "auditLog",
|
||||||
@@ -20133,6 +20168,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "1e5ee6b2-67e5-4549-bebc-8d35bc6bc649",
|
"id": "1e5ee6b2-67e5-4549-bebc-8d35bc6bc649",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "rocket",
|
"nameSingular": "rocket",
|
||||||
@@ -20682,6 +20718,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"__typename": "objectEdge",
|
"__typename": "objectEdge",
|
||||||
"node": {
|
"node": {
|
||||||
"__typename": "object",
|
"__typename": "object",
|
||||||
|
"shouldSyncLabelAndName": true,
|
||||||
"id": "149f1a0d-f528-48a3-a3f8-0203926d07f5",
|
"id": "149f1a0d-f528-48a3-a3f8-0203926d07f5",
|
||||||
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
"dataSourceId": "d8a38ce6-6ac9-4c10-b55f-408386f86290",
|
||||||
"nameSingular": "calendarChannelEventAssociation",
|
"nameSingular": "calendarChannelEventAssociation",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddShouldSyncLabelAndName1728579416430
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddShouldSyncLabelAndName1728579416430';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."objectMetadata" ADD "shouldSyncLabelAndName" boolean NOT NULL DEFAULT false`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."objectMetadata" DROP COLUMN "shouldSyncLabelAndName"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,8 +13,8 @@ import GraphQLJSON from 'graphql-type-json';
|
|||||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||||
|
|
||||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||||
import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook';
|
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@BeforeCreateOne(BeforeCreateOneObject)
|
@BeforeCreateOne(BeforeCreateOneObject)
|
||||||
@@ -81,4 +81,9 @@ export class CreateObjectInput {
|
|||||||
primaryKeyFieldMetadataSettings?: FieldMetadataSettings<
|
primaryKeyFieldMetadataSettings?: FieldMetadataSettings<
|
||||||
FieldMetadataType | 'default'
|
FieldMetadataType | 'default'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Field({ nullable: true })
|
||||||
|
shouldSyncLabelAndName?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,4 +79,7 @@ export class ObjectMetadataDTO {
|
|||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
imageIdentifierFieldMetadataId?: string | null;
|
imageIdentifierFieldMetadataId?: string | null;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
shouldSyncLabelAndName: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export class UpdateObjectPayload {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
imageIdentifierFieldMetadataId?: string;
|
imageIdentifierFieldMetadataId?: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Field({ nullable: true })
|
||||||
|
shouldSyncLabelAndName?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { Equal, In, Repository } from 'typeorm';
|
|||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BeforeUpdateOneObject<T extends UpdateObjectPayload>
|
export class BeforeUpdateOneObject<T extends UpdateObjectPayload>
|
||||||
@@ -99,47 +98,6 @@ export class BeforeUpdateOneObject<T extends UpdateObjectPayload>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkIfFieldIsEditable(instance.update, objectMetadata);
|
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is temporary until we properly use the MigrationRunner to update column names
|
|
||||||
private checkIfFieldIsEditable(
|
|
||||||
update: UpdateObjectPayload,
|
|
||||||
objectMetadata: ObjectMetadataEntity,
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
update.nameSingular &&
|
|
||||||
update.nameSingular !== objectMetadata.nameSingular
|
|
||||||
) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Object's nameSingular can't be updated. Please create a new object instead",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
update.labelSingular &&
|
|
||||||
update.labelSingular !== objectMetadata.labelSingular
|
|
||||||
) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Object's labelSingular can't be updated. Please create a new object instead",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (update.namePlural && update.namePlural !== objectMetadata.namePlural) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Object's namePlural can't be updated. Please create a new object instead",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
update.labelPlural &&
|
|
||||||
update.labelPlural !== objectMetadata.labelPlural
|
|
||||||
) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Object's labelPlural can't be updated. Please create a new object instead",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
|
|||||||
@Column({ nullable: true, type: 'uuid' })
|
@Column({ nullable: true, type: 'uuid' })
|
||||||
imageIdentifierFieldMetadataId?: string | null;
|
imageIdentifierFieldMetadataId?: string | null;
|
||||||
|
|
||||||
|
@Column({ default: true })
|
||||||
|
shouldSyncLabelAndName: boolean;
|
||||||
|
|
||||||
@Column({ nullable: false, type: 'uuid' })
|
@Column({ nullable: false, type: 'uuid' })
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import console from 'console';
|
|||||||
|
|
||||||
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
|
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
|
||||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||||
import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm';
|
import { isDefined } from 'class-validator';
|
||||||
|
import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||||
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
|
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
|
||||||
import { validateObjectMetadataInputOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
|
import { validateObjectMetadataInputOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
|
||||||
|
import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-sync-label-name.util';
|
||||||
import {
|
import {
|
||||||
RelationMetadataEntity,
|
RelationMetadataEntity,
|
||||||
RelationMetadataType,
|
RelationMetadataType,
|
||||||
@@ -35,6 +37,7 @@ import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-
|
|||||||
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
|
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
|
||||||
import { SearchService } from 'src/engine/metadata-modules/search/search.service';
|
import { SearchService } from 'src/engine/metadata-modules/search/search.service';
|
||||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||||
|
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||||
import {
|
import {
|
||||||
WorkspaceMigrationColumnActionType,
|
WorkspaceMigrationColumnActionType,
|
||||||
@@ -201,34 +204,23 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const objectAlreadyExists = await this.objectMetadataRepository.findOne({
|
if (objectMetadataInput.shouldSyncLabelAndName === true) {
|
||||||
where: [
|
validateNameAndLabelAreSyncOrThrow(
|
||||||
{
|
objectMetadataInput.labelSingular,
|
||||||
nameSingular: objectMetadataInput.nameSingular,
|
objectMetadataInput.nameSingular,
|
||||||
workspaceId: objectMetadataInput.workspaceId,
|
);
|
||||||
},
|
validateNameAndLabelAreSyncOrThrow(
|
||||||
{
|
objectMetadataInput.labelPlural,
|
||||||
nameSingular: objectMetadataInput.namePlural,
|
objectMetadataInput.namePlural,
|
||||||
workspaceId: objectMetadataInput.workspaceId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
namePlural: objectMetadataInput.nameSingular,
|
|
||||||
workspaceId: objectMetadataInput.workspaceId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
namePlural: objectMetadataInput.namePlural,
|
|
||||||
workspaceId: objectMetadataInput.workspaceId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (objectAlreadyExists) {
|
|
||||||
throw new ObjectMetadataException(
|
|
||||||
'Object already exists',
|
|
||||||
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||||
|
objectMetadataNamePlural: objectMetadataInput.namePlural,
|
||||||
|
objectMetadataNameSingular: objectMetadataInput.nameSingular,
|
||||||
|
workspaceId: objectMetadataInput.workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
const isCustom = !objectMetadataInput.isRemote;
|
const isCustom = !objectMetadataInput.isRemote;
|
||||||
|
|
||||||
const createdObjectMetadata = await super.createOne({
|
const createdObjectMetadata = await super.createOne({
|
||||||
@@ -421,12 +413,55 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
): Promise<ObjectMetadataEntity> {
|
): Promise<ObjectMetadataEntity> {
|
||||||
validateObjectMetadataInputOrThrow(input.update);
|
validateObjectMetadataInputOrThrow(input.update);
|
||||||
|
|
||||||
|
const existingObjectMetadata = await this.objectMetadataRepository.findOne({
|
||||||
|
where: { id: input.id, workspaceId: workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingObjectMetadata) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
'Object does not exist',
|
||||||
|
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullObjectMetadataAfterUpdate = {
|
||||||
|
...existingObjectMetadata,
|
||||||
|
...input.update,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.validatesNoOtherObjectWithSameNameExistsOrThrows({
|
||||||
|
objectMetadataNameSingular: fullObjectMetadataAfterUpdate.nameSingular,
|
||||||
|
objectMetadataNamePlural: fullObjectMetadataAfterUpdate.namePlural,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
existingObjectMetadataId: fullObjectMetadataAfterUpdate.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fullObjectMetadataAfterUpdate.shouldSyncLabelAndName) {
|
||||||
|
validateNameAndLabelAreSyncOrThrow(
|
||||||
|
fullObjectMetadataAfterUpdate.labelSingular,
|
||||||
|
fullObjectMetadataAfterUpdate.nameSingular,
|
||||||
|
);
|
||||||
|
validateNameAndLabelAreSyncOrThrow(
|
||||||
|
fullObjectMetadataAfterUpdate.labelPlural,
|
||||||
|
fullObjectMetadataAfterUpdate.namePlural,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const updatedObject = await super.updateOne(input.id, input.update);
|
const updatedObject = await super.updateOne(input.id, input.update);
|
||||||
|
|
||||||
|
await this.handleObjectNameAndLabelUpdates(
|
||||||
|
existingObjectMetadata,
|
||||||
|
fullObjectMetadataAfterUpdate,
|
||||||
|
input,
|
||||||
|
);
|
||||||
|
|
||||||
if (input.update.isActive !== undefined) {
|
if (input.update.isActive !== undefined) {
|
||||||
await this.updateObjectRelationships(input.id, input.update.isActive);
|
await this.updateObjectRelationships(input.id, input.update.isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
if (input.update.labelIdentifierFieldMetadataId) {
|
if (input.update.labelIdentifierFieldMetadataId) {
|
||||||
const labelIdentifierFieldMetadata =
|
const labelIdentifierFieldMetadata =
|
||||||
await this.fieldMetadataRepository.findOneByOrFail({
|
await this.fieldMetadataRepository.findOneByOrFail({
|
||||||
@@ -1375,4 +1410,235 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleObjectNameAndLabelUpdates(
|
||||||
|
existingObjectMetadata: ObjectMetadataEntity,
|
||||||
|
objectMetadataForUpdate: ObjectMetadataEntity,
|
||||||
|
input: UpdateOneObjectInput,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
isDefined(input.update.nameSingular) ||
|
||||||
|
isDefined(input.update.namePlural)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
objectMetadataForUpdate.nameSingular ===
|
||||||
|
objectMetadataForUpdate.namePlural
|
||||||
|
) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
'The singular and plural name cannot be the same for an object',
|
||||||
|
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTargetTableName = computeObjectTargetTable(
|
||||||
|
objectMetadataForUpdate,
|
||||||
|
);
|
||||||
|
const existingTargetTableName = computeObjectTargetTable(
|
||||||
|
existingObjectMetadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!(newTargetTableName === existingTargetTableName)) {
|
||||||
|
await this.createRenameTableMigration(
|
||||||
|
existingObjectMetadata,
|
||||||
|
objectMetadataForUpdate,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.createRelationsUpdatesMigrations(
|
||||||
|
existingObjectMetadata,
|
||||||
|
objectMetadataForUpdate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.update.labelPlural || input.update.icon) {
|
||||||
|
if (
|
||||||
|
!(input.update.labelPlural === existingObjectMetadata.labelPlural) ||
|
||||||
|
!(input.update.icon === existingObjectMetadata.icon)
|
||||||
|
) {
|
||||||
|
await this.updateObjectView(
|
||||||
|
objectMetadataForUpdate,
|
||||||
|
objectMetadataForUpdate.workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createRenameTableMigration(
|
||||||
|
existingObjectMetadata: ObjectMetadataEntity,
|
||||||
|
objectMetadataForUpdate: ObjectMetadataEntity,
|
||||||
|
) {
|
||||||
|
const newTargetTableName = computeObjectTargetTable(
|
||||||
|
objectMetadataForUpdate,
|
||||||
|
);
|
||||||
|
const existingTargetTableName = computeObjectTargetTable(
|
||||||
|
existingObjectMetadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.workspaceMigrationService.createCustomMigration(
|
||||||
|
generateMigrationName(`rename-${existingObjectMetadata.nameSingular}`),
|
||||||
|
objectMetadataForUpdate.workspaceId,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: existingTargetTableName,
|
||||||
|
newName: newTargetTableName,
|
||||||
|
action: WorkspaceMigrationTableActionType.ALTER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createRelationsUpdatesMigrations(
|
||||||
|
existingObjectMetadata: ObjectMetadataEntity,
|
||||||
|
updatedObjectMetadata: ObjectMetadataEntity,
|
||||||
|
) {
|
||||||
|
const existingTableName = computeObjectTargetTable(existingObjectMetadata);
|
||||||
|
const newTableName = computeObjectTargetTable(updatedObjectMetadata);
|
||||||
|
|
||||||
|
if (existingTableName !== newTableName) {
|
||||||
|
const searchCriteria = {
|
||||||
|
isCustom: false,
|
||||||
|
settings: {
|
||||||
|
isForeignKey: true,
|
||||||
|
},
|
||||||
|
name: `${existingObjectMetadata.nameSingular}Id`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldsWihStandardRelation = await this.fieldMetadataRepository.find(
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
isCustom: false,
|
||||||
|
settings: {
|
||||||
|
isForeignKey: true,
|
||||||
|
},
|
||||||
|
name: `${existingObjectMetadata.nameSingular}Id`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.fieldMetadataRepository.update(searchCriteria, {
|
||||||
|
name: `${updatedObjectMetadata.nameSingular}Id`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
fieldsWihStandardRelation.map(async (fieldWihStandardRelation) => {
|
||||||
|
const relatedObject = await this.objectMetadataRepository.findOneBy({
|
||||||
|
id: fieldWihStandardRelation.objectMetadataId,
|
||||||
|
workspaceId: updatedObjectMetadata.workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (relatedObject) {
|
||||||
|
await this.fieldMetadataRepository.update(
|
||||||
|
{
|
||||||
|
name: existingObjectMetadata.nameSingular,
|
||||||
|
label: existingObjectMetadata.labelSingular,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: updatedObjectMetadata.nameSingular,
|
||||||
|
label: updatedObjectMetadata.labelSingular,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const relationTableName = computeObjectTargetTable(relatedObject);
|
||||||
|
const columnName = `${existingObjectMetadata.nameSingular}Id`;
|
||||||
|
const columnType = fieldMetadataTypeToColumnType(
|
||||||
|
fieldWihStandardRelation.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
|
generateMigrationName(
|
||||||
|
`rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObject.nameSingular}`,
|
||||||
|
),
|
||||||
|
updatedObjectMetadata.workspaceId,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: relationTableName,
|
||||||
|
action: WorkspaceMigrationTableActionType.ALTER,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
action: WorkspaceMigrationColumnActionType.ALTER,
|
||||||
|
currentColumnDefinition: {
|
||||||
|
columnName,
|
||||||
|
columnType,
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
alteredColumnDefinition: {
|
||||||
|
columnName: `${updatedObjectMetadata.nameSingular}Id`,
|
||||||
|
columnType,
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateObjectView(
|
||||||
|
updatedObjectMetadata: ObjectMetadataEntity,
|
||||||
|
workspaceId: string,
|
||||||
|
) {
|
||||||
|
const dataSourceMetadata =
|
||||||
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const workspaceDataSource =
|
||||||
|
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||||
|
|
||||||
|
await workspaceDataSource?.query(
|
||||||
|
`UPDATE ${dataSourceMetadata.schema}."view"
|
||||||
|
SET "name"=$1, "icon"=$2 WHERE "objectMetadataId"=$3 AND "key"=$4`,
|
||||||
|
[
|
||||||
|
`All ${updatedObjectMetadata.labelPlural}`,
|
||||||
|
updatedObjectMetadata.icon,
|
||||||
|
updatedObjectMetadata.id,
|
||||||
|
'INDEX',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validatesNoOtherObjectWithSameNameExistsOrThrows = async ({
|
||||||
|
objectMetadataNameSingular,
|
||||||
|
objectMetadataNamePlural,
|
||||||
|
workspaceId,
|
||||||
|
existingObjectMetadataId,
|
||||||
|
}: {
|
||||||
|
objectMetadataNameSingular: string;
|
||||||
|
objectMetadataNamePlural: string;
|
||||||
|
workspaceId: string;
|
||||||
|
existingObjectMetadataId?: string;
|
||||||
|
}): Promise<void> => {
|
||||||
|
const baseWhereConditions = [
|
||||||
|
{ nameSingular: objectMetadataNameSingular, workspaceId },
|
||||||
|
{ nameSingular: objectMetadataNamePlural, workspaceId },
|
||||||
|
{ namePlural: objectMetadataNameSingular, workspaceId },
|
||||||
|
{ namePlural: objectMetadataNamePlural, workspaceId },
|
||||||
|
];
|
||||||
|
|
||||||
|
const whereConditions = baseWhereConditions.map((condition) => {
|
||||||
|
return {
|
||||||
|
...condition,
|
||||||
|
...(isDefined(existingObjectMetadataId)
|
||||||
|
? { id: Not(In([existingObjectMetadataId])) }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectAlreadyExists = await this.objectMetadataRepository.findOne({
|
||||||
|
where: whereConditions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (objectAlreadyExists) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
'Object already exists',
|
||||||
|
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import toCamelCase from 'lodash.camelcase';
|
||||||
|
import { slugify, transliterate } from 'transliteration';
|
||||||
|
|
||||||
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||||
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||||
import {
|
import {
|
||||||
@@ -40,6 +43,8 @@ const reservedKeywords = [
|
|||||||
'addresses',
|
'addresses',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const METADATA_NAME_VALID_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
||||||
|
|
||||||
export const validateObjectMetadataInputOrThrow = <
|
export const validateObjectMetadataInputOrThrow = <
|
||||||
T extends UpdateObjectPayload | CreateObjectInput,
|
T extends UpdateObjectPayload | CreateObjectInput,
|
||||||
>(
|
>(
|
||||||
@@ -58,6 +63,30 @@ export const validateObjectMetadataInputOrThrow = <
|
|||||||
validateNameIsNotTooLongThrow(objectMetadataInput.namePlural);
|
validateNameIsNotTooLongThrow(objectMetadataInput.namePlural);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const transliterateAndFormatOrThrow = (string?: string): string => {
|
||||||
|
if (!string) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
'Name is required',
|
||||||
|
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let formattedString = string;
|
||||||
|
|
||||||
|
if (formattedString.match(METADATA_NAME_VALID_PATTERN) !== null) {
|
||||||
|
return toCamelCase(formattedString);
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedString = toCamelCase(
|
||||||
|
slugify(transliterate(formattedString, { trim: true })),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!formattedString.match(METADATA_NAME_VALID_PATTERN)) {
|
||||||
|
throw new Error(`"${string}" is not a valid name`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedString;
|
||||||
|
};
|
||||||
|
|
||||||
const validateNameIsNotReservedKeywordOrThrow = (name?: string) => {
|
const validateNameIsNotReservedKeywordOrThrow = (name?: string) => {
|
||||||
if (name) {
|
if (name) {
|
||||||
if (reservedKeywords.includes(name)) {
|
if (reservedKeywords.includes(name)) {
|
||||||
@@ -107,3 +136,9 @@ const validateNameCharactersOrThrow = (name?: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const computeMetadataNameFromLabelOrThrow = (label: string): string => {
|
||||||
|
const formattedString = transliterateAndFormatOrThrow(label);
|
||||||
|
|
||||||
|
return formattedString;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
ObjectMetadataException,
|
||||||
|
ObjectMetadataExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||||
|
import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
|
||||||
|
|
||||||
|
export const validateNameAndLabelAreSyncOrThrow = (
|
||||||
|
label: string,
|
||||||
|
name: string,
|
||||||
|
) => {
|
||||||
|
const computedName = computeMetadataNameFromLabelOrThrow(label);
|
||||||
|
|
||||||
|
if (name !== computedName) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
`Name is not synced with label. Expected name: "${computedName}", got ${name}`,
|
||||||
|
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user