diff --git a/packages/core/e2e/channel.e2e-spec.ts b/packages/core/e2e/channel.e2e-spec.ts index 80adf4509a..322b176537 100644 --- a/packages/core/e2e/channel.e2e-spec.ts +++ b/packages/core/e2e/channel.e2e-spec.ts @@ -22,6 +22,8 @@ import { Me, Permission, RemoveProductsFromChannel, + UpdateChannel, + UpdateGlobalSettings, } from './graphql/generated-e2e-admin-types'; import { ASSIGN_PRODUCT_TO_CHANNEL, @@ -100,8 +102,8 @@ describe('Channels', () => { expect(me!.channels.length).toBe(2); - const secondChannelData = me!.channels.find(c => c.token === SECOND_CHANNEL_TOKEN); - const nonOwnerPermissions = Object.values(Permission).filter(p => p !== Permission.Owner); + const secondChannelData = me!.channels.find((c) => c.token === SECOND_CHANNEL_TOKEN); + const nonOwnerPermissions = Object.values(Permission).filter((p) => p !== Permission.Owner); expect(secondChannelData!.permissions).toEqual(nonOwnerPermissions); }); @@ -111,7 +113,7 @@ describe('Channels', () => { expect(me!.channels.length).toBe(2); - const secondChannelData = me!.channels.find(c => c.token === SECOND_CHANNEL_TOKEN); + const secondChannelData = me!.channels.find((c) => c.token === SECOND_CHANNEL_TOKEN); expect(me!.channels).toEqual([ { code: DEFAULT_CHANNEL_CODE, @@ -170,7 +172,7 @@ describe('Channels', () => { }, }); - expect(createAdministrator.user.roles.map(r => r.description)).toEqual(['second channel admin']); + expect(createAdministrator.user.roles.map((r) => r.description)).toEqual(['second channel admin']); }); it( @@ -288,7 +290,7 @@ describe('Channels', () => { }, }); - expect(assignProductsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']); + expect(assignProductsToChannel[0].channels.map((c) => c.id).sort()).toEqual(['T_1', 'T_2']); await adminClient.setChannelToken(SECOND_CHANNEL_TOKEN); const { product } = await adminClient.query< GetProductWithVariants.Query, @@ -297,12 +299,12 @@ describe('Channels', () => { id: product1.id, }); - expect(product!.variants.map(v => v.price)).toEqual( - product1.variants.map(v => v.price * PRICE_FACTOR), + expect(product!.variants.map((v) => v.price)).toEqual( + product1.variants.map((v) => v.price * PRICE_FACTOR), ); // Second Channel is configured to include taxes in price, so they should be the same. - expect(product!.variants.map(v => v.priceWithTax)).toEqual( - product1.variants.map(v => v.price * PRICE_FACTOR), + expect(product!.variants.map((v) => v.priceWithTax)).toEqual( + product1.variants.map((v) => v.price * PRICE_FACTOR), ); }); @@ -317,7 +319,7 @@ describe('Channels', () => { }, }); - expect(assignProductsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']); + expect(assignProductsToChannel[0].channels.map((c) => c.id).sort()).toEqual(['T_1', 'T_2']); }); it( @@ -348,7 +350,44 @@ describe('Channels', () => { }, }); - expect(removeProductsFromChannel[0].channels.map(c => c.id)).toEqual(['T_1']); + expect(removeProductsFromChannel[0].channels.map((c) => c.id)).toEqual(['T_1']); + }); + }); + + describe('setting defaultLanguage', () => { + it( + 'throws if languageCode not in availableLanguages', + assertThrowsWithMessage(async () => { + await adminClient.query(UPDATE_CHANNEL, { + input: { + id: 'T_1', + defaultLanguageCode: LanguageCode.zh, + }, + }); + }, 'Language "zh" is not available. First enable it via GlobalSettings and try again.'), + ); + + it('allows setting to an available language', async () => { + await adminClient.query( + UPDATE_GLOBAL_SETTINGS, + { + input: { + availableLanguages: [LanguageCode.en, LanguageCode.zh], + }, + }, + ); + + const { updateChannel } = await adminClient.query< + UpdateChannel.Mutation, + UpdateChannel.Variables + >(UPDATE_CHANNEL, { + input: { + id: 'T_1', + defaultLanguageCode: LanguageCode.zh, + }, + }); + + expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.zh); }); }); @@ -364,7 +403,7 @@ describe('Channels', () => { productIds: [PROD_ID], }, }); - expect(assignProductsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']); + expect(assignProductsToChannel[0].channels.map((c) => c.id).sort()).toEqual(['T_1', 'T_2']); const { deleteChannel } = await adminClient.query( DELETE_CHANNEL, @@ -376,7 +415,7 @@ describe('Channels', () => { expect(deleteChannel.result).toBe(DeletionResult.DELETED); const { channels } = await adminClient.query(GET_CHANNELS); - expect(channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']); + expect(channels.map((c) => c.id).sort()).toEqual(['T_1', 'T_3']); const { product } = await adminClient.query< GetProductWithVariants.Query, @@ -384,7 +423,7 @@ describe('Channels', () => { >(GET_PRODUCT_WITH_VARIANTS, { id: PROD_ID, }); - expect(product!.channels.map(c => c.id)).toEqual(['T_1']); + expect(product!.channels.map((c) => c.id)).toEqual(['T_1']); }); }); @@ -398,6 +437,17 @@ const GET_CHANNELS = gql` } `; +const UPDATE_CHANNEL = gql` + mutation UpdateChannel($input: UpdateChannelInput!) { + updateChannel(input: $input) { + id + code + defaultLanguageCode + currencyCode + } + } +`; + const DELETE_CHANNEL = gql` mutation DeleteChannel($id: ID!) { deleteChannel(id: $id) { @@ -406,3 +456,12 @@ const DELETE_CHANNEL = gql` } } `; + +const UPDATE_GLOBAL_SETTINGS = gql` + mutation UpdateGlobalSettings($input: UpdateGlobalSettingsInput!) { + updateGlobalSettings(input: $input) { + id + availableLanguages + } + } +`; diff --git a/packages/core/e2e/graphql/generated-e2e-admin-types.ts b/packages/core/e2e/graphql/generated-e2e-admin-types.ts index ac63a80ad5..91dc4dd4b6 100644 --- a/packages/core/e2e/graphql/generated-e2e-admin-types.ts +++ b/packages/core/e2e/graphql/generated-e2e-admin-types.ts @@ -3585,6 +3585,17 @@ export type GetChannelsQuery = { __typename?: 'Query' } & { channels: Array<{ __typename?: 'Channel' } & Pick>; }; +export type UpdateChannelMutationVariables = { + input: UpdateChannelInput; +}; + +export type UpdateChannelMutation = { __typename?: 'Mutation' } & { + updateChannel: { __typename?: 'Channel' } & Pick< + Channel, + 'id' | 'code' | 'defaultLanguageCode' | 'currencyCode' + >; +}; + export type DeleteChannelMutationVariables = { id: Scalars['ID']; }; @@ -3593,6 +3604,17 @@ export type DeleteChannelMutation = { __typename?: 'Mutation' } & { deleteChannel: { __typename?: 'DeletionResponse' } & Pick; }; +export type UpdateGlobalSettingsMutationVariables = { + input: UpdateGlobalSettingsInput; +}; + +export type UpdateGlobalSettingsMutation = { __typename?: 'Mutation' } & { + updateGlobalSettings: { __typename?: 'GlobalSettings' } & Pick< + GlobalSettings, + 'id' | 'availableLanguages' + >; +}; + export type GetCollectionsWithAssetsQueryVariables = {}; export type GetCollectionsWithAssetsQuery = { __typename?: 'Query' } & { @@ -5454,12 +5476,24 @@ export namespace GetChannels { export type Channels = NonNullable; } +export namespace UpdateChannel { + export type Variables = UpdateChannelMutationVariables; + export type Mutation = UpdateChannelMutation; + export type UpdateChannel = UpdateChannelMutation['updateChannel']; +} + export namespace DeleteChannel { export type Variables = DeleteChannelMutationVariables; export type Mutation = DeleteChannelMutation; export type DeleteChannel = DeleteChannelMutation['deleteChannel']; } +export namespace UpdateGlobalSettings { + export type Variables = UpdateGlobalSettingsMutationVariables; + export type Mutation = UpdateGlobalSettingsMutation; + export type UpdateGlobalSettings = UpdateGlobalSettingsMutation['updateGlobalSettings']; +} + export namespace GetCollectionsWithAssets { export type Variables = GetCollectionsWithAssetsQueryVariables; export type Query = GetCollectionsWithAssetsQuery; diff --git a/packages/core/src/i18n/messages/en.json b/packages/core/src/i18n/messages/en.json index b7d184270d..6ccbd31afd 100644 --- a/packages/core/src/i18n/messages/en.json +++ b/packages/core/src/i18n/messages/en.json @@ -39,6 +39,7 @@ "identifier-change-token-not-recognized": "Identifier change token not recognized", "identifier-change-token-has-expired": "Identifier change token has expired", "invalid-sort-field": "The sort field '{ fieldName }' is invalid. Valid fields are: { validFields }", + "language-not-available-in-global-settings": "Language \"{code}\" is not available. First enable it via GlobalSettings and try again.", "missing-password-on-registration": "A password must be provided when `authOptions.requireVerification` is set to \"false\"", "no-search-plugin-configured": "No search plugin has been configured", "no-valid-channel-specified": "No valid channel was specified (ensure the 'vendure-token' header was specified in the request)", diff --git a/packages/core/src/service/services/channel.service.ts b/packages/core/src/service/services/channel.service.ts index 078a6efde1..8b1a6f53c8 100644 --- a/packages/core/src/service/services/channel.service.ts +++ b/packages/core/src/service/services/channel.service.ts @@ -13,7 +13,12 @@ import { unique } from '@vendure/common/lib/unique'; import { Connection } from 'typeorm'; import { RequestContext } from '../../api/common/request-context'; -import { ChannelNotFoundError, EntityNotFoundError, InternalServerError } from '../../common/error/errors'; +import { + ChannelNotFoundError, + EntityNotFoundError, + InternalServerError, + UserInputError, +} from '../../common/error/errors'; import { ChannelAware } from '../../common/types/common-types'; import { assertFound, idsAreEqual } from '../../common/utils'; import { ConfigService } from '../../config/config.service'; @@ -24,11 +29,17 @@ import { Zone } from '../../entity/zone/zone.entity'; import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw'; import { patchEntity } from '../helpers/utils/patch-entity'; +import { GlobalSettingsService } from './global-settings.service'; + @Injectable() export class ChannelService { private allChannels: Channel[] = []; - constructor(@InjectConnection() private connection: Connection, private configService: ConfigService) {} + constructor( + @InjectConnection() private connection: Connection, + private configService: ConfigService, + private globalSettingsService: GlobalSettingsService, + ) {} /** * When the app is bootstrapped, ensure a default Channel exists and populate the @@ -147,6 +158,16 @@ export class ChannelService { if (!channel) { throw new EntityNotFoundError('Channel', input.id); } + if (input.defaultLanguageCode) { + const availableLanguageCodes = await this.globalSettingsService + .getSettings() + .then((s) => s.availableLanguages); + if (!availableLanguageCodes.includes(input.defaultLanguageCode)) { + throw new UserInputError('error.language-not-available-in-global-settings', { + code: input.defaultLanguageCode, + }); + } + } const updatedChannel = patchEntity(channel, input); if (input.defaultTaxZoneId) { updatedChannel.defaultTaxZone = await getEntityOrThrow(