Overwrite standard translations (#11134)
Manage overwriting translations for standard fields and standard objects properties
This commit is contained in:
@ -1,6 +1,8 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { IsJSON, IsOptional, IsString } from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
|
||||
@ObjectType('StandardOverrides')
|
||||
export class FieldStandardOverridesDTO {
|
||||
@ -18,4 +20,19 @@ export class FieldStandardOverridesDTO {
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
icon?: string | null;
|
||||
|
||||
@IsJSON()
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, {
|
||||
nullable: true,
|
||||
})
|
||||
translations?: Partial<
|
||||
Record<
|
||||
keyof typeof APP_LOCALES,
|
||||
{
|
||||
label?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
>
|
||||
> | null;
|
||||
}
|
||||
|
||||
@ -108,12 +108,13 @@ export class FieldMetadataResolver {
|
||||
async updateOneField(
|
||||
@Args('input') input: UpdateOneFieldMetadataInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@Context() context: I18nContext,
|
||||
) {
|
||||
try {
|
||||
const updatedInput = (await this.beforeUpdateOneField.run(
|
||||
input,
|
||||
const updatedInput = (await this.beforeUpdateOneField.run(input, {
|
||||
workspaceId,
|
||||
)) as UpdateOneFieldMetadataInput;
|
||||
locale: context.req.headers['x-locale'],
|
||||
})) as UpdateOneFieldMetadataInput;
|
||||
|
||||
return await this.fieldMetadataService.updateOne(updatedInput.id, {
|
||||
...updatedInput.update,
|
||||
|
||||
@ -4,7 +4,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, FindOneOptions, In, Repository } from 'typeorm';
|
||||
@ -618,15 +618,22 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
return fieldMetadata[labelKey] ?? '';
|
||||
}
|
||||
|
||||
if (!locale) {
|
||||
if (!locale || locale === SOURCE_LOCALE) {
|
||||
if (
|
||||
fieldMetadata.standardOverrides &&
|
||||
isDefined(fieldMetadata.standardOverrides[labelKey])
|
||||
) {
|
||||
return fieldMetadata.standardOverrides[labelKey] as string;
|
||||
}
|
||||
|
||||
return fieldMetadata[labelKey] ?? '';
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMetadata.standardOverrides &&
|
||||
isDefined(fieldMetadata.standardOverrides[labelKey])
|
||||
) {
|
||||
return fieldMetadata.standardOverrides[labelKey] as string;
|
||||
const translationValue =
|
||||
fieldMetadata.standardOverrides?.translations?.[locale]?.[labelKey];
|
||||
|
||||
if (isDefined(translationValue)) {
|
||||
return translationValue;
|
||||
}
|
||||
|
||||
const messageId = generateMessageId(fieldMetadata[labelKey] ?? '');
|
||||
|
||||
@ -0,0 +1,554 @@
|
||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { UpdateOneInputType } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
||||
import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook';
|
||||
|
||||
jest.mock('@lingui/core', () => ({
|
||||
i18n: {
|
||||
_: jest.fn().mockImplementation((messageId) => `translated:${messageId}`),
|
||||
},
|
||||
}));
|
||||
|
||||
// Create a type that omits id and workspaceId from UpdateFieldInput
|
||||
type UpdateFieldInputForTest = Omit<UpdateFieldInput, 'id' | 'workspaceId'>;
|
||||
|
||||
describe('BeforeUpdateOneField', () => {
|
||||
let hook: BeforeUpdateOneField<UpdateFieldInput>;
|
||||
let fieldMetadataService: FieldMetadataService;
|
||||
|
||||
const mockWorkspaceId = 'workspace-id';
|
||||
const mockFieldId = 'field-id';
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
BeforeUpdateOneField,
|
||||
{
|
||||
provide: FieldMetadataService,
|
||||
useValue: {
|
||||
findOneWithinWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
hook =
|
||||
module.get<BeforeUpdateOneField<UpdateFieldInput>>(BeforeUpdateOneField);
|
||||
fieldMetadataService =
|
||||
module.get<FieldMetadataService>(FieldMetadataService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException if workspaceId is not provided', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: true,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
|
||||
workspaceId: '',
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if field does not exist', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: true,
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(null as unknown as FieldMetadataEntity);
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should not affect custom fields', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: true,
|
||||
name: 'newName', // Custom fields can update all properties
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual(instance);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when trying to update non-updatable fields on standard fields', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
name: 'newName', // Not allowed for standard fields
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when trying to update label when it is synced with name', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
label: 'New Label',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: true,
|
||||
label: 'Old Label',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
await expect(
|
||||
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should handle isActive updates for standard fields', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: false,
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: false,
|
||||
standardOverrides: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle isLabelSyncedWithName updates for standard fields', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isLabelSyncedWithName: true,
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
standardOverrides: {
|
||||
label: 'Custom Label',
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isLabelSyncedWithName: true,
|
||||
standardOverrides: {
|
||||
label: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle label override when not synced with name', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
label: 'New Label',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'Default Label',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
label: 'New Label',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should reset label override when it matches the original value', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
label: 'Default Label',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'Default Label',
|
||||
standardOverrides: {
|
||||
label: 'Custom Label',
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
label: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle description override', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
description: 'New Description',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
description: 'Default Description',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
description: 'New Description',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle icon override', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
icon: 'IconStar',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
icon: 'IconCircle',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
icon: 'IconStar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle locale-specific translations', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
label: 'Étiquette',
|
||||
description: 'Description en français',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'Label',
|
||||
description: 'Description',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: 'fr-FR',
|
||||
},
|
||||
);
|
||||
|
||||
// The expected result returned by the hook
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
label: 'Étiquette',
|
||||
description: 'Description en français',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should reset locale-specific translations when they match translated defaults', async () => {
|
||||
const translatedLabel = 'translated:msg-label';
|
||||
|
||||
(i18n._ as jest.Mock).mockImplementation(() => translatedLabel);
|
||||
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
label: translatedLabel,
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'Label',
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
label: 'Ancienne Étiquette',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: 'fr-FR',
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
label: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle multiple updates together', async () => {
|
||||
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: false,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'New Label',
|
||||
icon: 'IconStar',
|
||||
description: 'New Description',
|
||||
},
|
||||
};
|
||||
|
||||
const mockField: Partial<FieldMetadataEntity> = {
|
||||
id: mockFieldId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isLabelSyncedWithName: false,
|
||||
label: 'Default Label',
|
||||
icon: 'IconCircle',
|
||||
description: 'Default Description',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(mockField as FieldMetadataEntity);
|
||||
|
||||
const result = await hook.run(
|
||||
instance as UpdateOneInputType<UpdateFieldInput>,
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
locale: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
id: mockFieldId,
|
||||
update: {
|
||||
isActive: false,
|
||||
isLabelSyncedWithName: false,
|
||||
standardOverrides: {
|
||||
label: 'New Label',
|
||||
icon: 'IconStar',
|
||||
description: 'New Description',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
@ -4,12 +4,15 @@ import {
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import {
|
||||
BeforeUpdateOneHook,
|
||||
UpdateOneInputType,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
|
||||
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@ -27,7 +30,13 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
|
||||
async run(
|
||||
instance: UpdateOneInputType<T>,
|
||||
workspaceId: string,
|
||||
{
|
||||
workspaceId,
|
||||
locale,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
locale: keyof typeof APP_LOCALES | undefined;
|
||||
},
|
||||
): Promise<UpdateOneInputType<T>> {
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException();
|
||||
@ -36,7 +45,7 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
const fieldMetadata = await this.getFieldMetadata(instance, workspaceId);
|
||||
|
||||
if (!fieldMetadata.isCustom) {
|
||||
return this.handleStandardFieldUpdate(instance, fieldMetadata);
|
||||
return this.handleStandardFieldUpdate(instance, fieldMetadata, locale);
|
||||
}
|
||||
|
||||
return instance;
|
||||
@ -63,12 +72,13 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
private handleStandardFieldUpdate(
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): UpdateOneInputType<T> {
|
||||
const update: StandardFieldUpdate = {};
|
||||
const updatableFields = ['isActive', 'isLabelSyncedWithName'];
|
||||
const overridableFields = ['label', 'icon', 'description'];
|
||||
|
||||
const hasNonUpdatableFields = Object.keys(instance.update).some(
|
||||
const nonUpdatableFields = Object.keys(instance.update).filter(
|
||||
(key) =>
|
||||
!updatableFields.includes(key) && !overridableFields.includes(key),
|
||||
);
|
||||
@ -85,9 +95,9 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasNonUpdatableFields) {
|
||||
if (nonUpdatableFields.length > 0) {
|
||||
throw new BadRequestException(
|
||||
'Only isActive, isLabelSyncedWithName, label, icon and description fields can be updated for standard fields',
|
||||
`Only isActive, isLabelSyncedWithName, label, icon and description fields can be updated for standard fields. Invalid fields: ${nonUpdatableFields.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -98,7 +108,7 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
|
||||
this.handleActiveField(instance, update);
|
||||
this.handleLabelSyncedWithNameField(instance, update);
|
||||
this.handleStandardOverrides(instance, fieldMetadata, update);
|
||||
this.handleStandardOverrides(instance, fieldMetadata, update, locale);
|
||||
|
||||
return {
|
||||
id: instance.id,
|
||||
@ -139,6 +149,7 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
update: StandardFieldUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
const hasStandardOverrides =
|
||||
isDefined(instance.update.description) ||
|
||||
@ -151,29 +162,158 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
|
||||
this.handleDescriptionOverride(instance, fieldMetadata, update);
|
||||
this.handleDescriptionOverride(instance, fieldMetadata, update, locale);
|
||||
this.handleIconOverride(instance, fieldMetadata, update);
|
||||
this.handleLabelOverride(instance, fieldMetadata, update);
|
||||
this.handleLabelOverride(instance, fieldMetadata, update, locale);
|
||||
}
|
||||
|
||||
private resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey,
|
||||
newValue,
|
||||
originalValue,
|
||||
locale,
|
||||
}: {
|
||||
update: StandardFieldUpdate;
|
||||
overrideKey: 'label' | 'description' | 'icon';
|
||||
newValue: string;
|
||||
originalValue: string;
|
||||
locale?: keyof typeof APP_LOCALES | undefined;
|
||||
}): boolean {
|
||||
// Handle localized overrides
|
||||
if (locale && locale !== SOURCE_LOCALE) {
|
||||
const wasOverrideReset = this.resetLocalizedOverride(
|
||||
update,
|
||||
overrideKey,
|
||||
newValue,
|
||||
originalValue,
|
||||
locale,
|
||||
);
|
||||
|
||||
return wasOverrideReset;
|
||||
}
|
||||
|
||||
// Handle default language overrides
|
||||
const wasOverrideReset = this.resetDefaultOverride(
|
||||
update,
|
||||
overrideKey,
|
||||
newValue,
|
||||
originalValue,
|
||||
);
|
||||
|
||||
return wasOverrideReset;
|
||||
}
|
||||
|
||||
private resetLocalizedOverride(
|
||||
update: StandardFieldUpdate,
|
||||
overrideKey: 'label' | 'description' | 'icon',
|
||||
newValue: string,
|
||||
originalValue: string,
|
||||
locale: keyof typeof APP_LOCALES,
|
||||
): boolean {
|
||||
const messageId = generateMessageId(originalValue ?? '');
|
||||
const translatedMessage = i18n._(messageId);
|
||||
|
||||
if (newValue !== translatedMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize the translations structure if needed
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides.translations =
|
||||
update.standardOverrides.translations || {};
|
||||
update.standardOverrides.translations[locale] =
|
||||
update.standardOverrides.translations[locale] || {};
|
||||
|
||||
// Reset the override by setting it to null
|
||||
const localeTranslations = update.standardOverrides.translations[locale];
|
||||
|
||||
(localeTranslations as Record<string, any>)[overrideKey] = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private resetDefaultOverride(
|
||||
update: StandardFieldUpdate,
|
||||
overrideKey: 'label' | 'description' | 'icon',
|
||||
newValue: string,
|
||||
originalValue: string,
|
||||
): boolean {
|
||||
if (newValue !== originalValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides[overrideKey] = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private setOverrideValue(
|
||||
update: StandardFieldUpdate,
|
||||
overrideKey: 'label' | 'description' | 'icon',
|
||||
value: string,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
|
||||
const shouldSetLocalizedOverride =
|
||||
locale && locale !== SOURCE_LOCALE && overrideKey !== 'icon';
|
||||
|
||||
if (!shouldSetLocalizedOverride) {
|
||||
update.standardOverrides[overrideKey] = value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLocalizedOverrideValue(update, overrideKey, value, locale);
|
||||
}
|
||||
|
||||
private setLocalizedOverrideValue(
|
||||
update: StandardFieldUpdate,
|
||||
overrideKey: 'label' | 'description' | 'icon',
|
||||
value: string,
|
||||
locale: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
update.standardOverrides.translations =
|
||||
update.standardOverrides.translations || {};
|
||||
update.standardOverrides.translations[locale] =
|
||||
update.standardOverrides.translations[locale] || {};
|
||||
|
||||
const localeTranslations = update.standardOverrides.translations[locale];
|
||||
|
||||
(localeTranslations as Record<string, any>)[overrideKey] = value;
|
||||
}
|
||||
|
||||
private handleDescriptionOverride(
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
update: StandardFieldUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
if (!isDefined(instance.update.description)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
|
||||
if (instance.update.description === fieldMetadata.description) {
|
||||
update.standardOverrides.description = null;
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'description',
|
||||
newValue: instance.update.description,
|
||||
originalValue: fieldMetadata.description,
|
||||
locale,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.standardOverrides.description = instance.update.description;
|
||||
this.setOverrideValue(
|
||||
update,
|
||||
'description',
|
||||
instance.update.description,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
private handleIconOverride(
|
||||
@ -185,21 +325,26 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
return;
|
||||
}
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
|
||||
if (instance.update.icon === fieldMetadata.icon) {
|
||||
update.standardOverrides.icon = null;
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'icon',
|
||||
newValue: instance.update.icon,
|
||||
originalValue: fieldMetadata.icon,
|
||||
locale: undefined,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.standardOverrides.icon = instance.update.icon;
|
||||
this.setOverrideValue(update, 'icon', instance.update.icon);
|
||||
}
|
||||
|
||||
private handleLabelOverride(
|
||||
instance: UpdateOneInputType<T>,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
update: StandardFieldUpdate,
|
||||
locale?: keyof typeof APP_LOCALES,
|
||||
): void {
|
||||
if (
|
||||
fieldMetadata.isLabelSyncedWithName ||
|
||||
@ -212,14 +357,18 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
||||
return;
|
||||
}
|
||||
|
||||
update.standardOverrides = update.standardOverrides || {};
|
||||
|
||||
if (instance.update.label === fieldMetadata.label) {
|
||||
update.standardOverrides.label = null;
|
||||
|
||||
if (
|
||||
this.resetOverrideIfMatchesOriginal({
|
||||
update,
|
||||
overrideKey: 'label',
|
||||
newValue: instance.update.label,
|
||||
originalValue: fieldMetadata.label,
|
||||
locale,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
update.standardOverrides.label = instance.update.label;
|
||||
this.setOverrideValue(update, 'label', instance.update.label, locale);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user