Răsfoiți Sursa

feat: Make Country entity translatable

Michael Bromley 7 ani în urmă
părinte
comite
0f01138a81
25 a modificat fișierele cu 475 adăugiri și 155 ștergeri
  1. 13 11
      admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.ts
  2. 11 15
      admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts
  3. 4 0
      admin-ui/src/app/common/base-detail.component.ts
  4. 59 8
      admin-ui/src/app/common/utilities/create-updated-translatable.spec.ts
  5. 13 6
      admin-ui/src/app/common/utilities/create-updated-translatable.ts
  6. 5 0
      admin-ui/src/app/data/definitions/settings-definitions.ts
  7. 3 2
      admin-ui/src/app/data/providers/settings-data.service.ts
  8. 5 1
      admin-ui/src/app/settings/components/country-detail/country-detail.component.html
  9. 58 40
      admin-ui/src/app/settings/components/country-detail/country-detail.component.ts
  10. 1 0
      admin-ui/src/app/settings/providers/routing/country-resolver.ts
  11. 0 0
      schema.json
  12. 44 1
      server/mock-data/mock-data.service.ts
  13. 1 0
      server/mock-data/populate.ts
  14. 40 9
      server/src/api/resolvers/country.resolver.ts
  15. 16 10
      server/src/api/resolvers/zone.resolver.ts
  16. 7 6
      server/src/api/types/country.api.graphql
  17. 22 0
      server/src/entity/country/country-translation.entity.ts
  18. 9 3
      server/src/entity/country/country.entity.ts
  19. 18 2
      server/src/entity/country/country.graphql
  20. 2 0
      server/src/entity/entities.ts
  21. 1 1
      server/src/entity/facet/facet-translation.entity.ts
  22. 2 2
      server/src/service/helpers/translatable-saver/translatable-saver.ts
  23. 36 20
      server/src/service/services/country.service.ts
  24. 40 16
      server/src/service/services/zone.service.ts
  25. 65 2
      shared/generated-types.ts

+ 13 - 11
admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.ts

@@ -82,10 +82,6 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
         }
     }
 
-    setLanguage(code: LanguageCode) {
-        this.setQueryParam('lang', code);
-    }
-
     customFieldIsSet(name: string): boolean {
         return !!this.facetForm.get(['facet', 'customFields', name]);
     }
@@ -272,9 +268,15 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
         facetFormGroup: FormGroup,
         languageCode: LanguageCode,
     ): any {
-        return createUpdatedTranslatable(facet, facetFormGroup.value, this.customFields, languageCode, {
+        return createUpdatedTranslatable({
+            translatable: facet,
+            updatedFields: facetFormGroup.value,
+            customFieldConfig: this.customFields,
             languageCode,
-            name: facet.name || '',
+            defaultTranslation: {
+                languageCode,
+                name: facet.name || '',
+            },
         });
     }
 
@@ -300,12 +302,12 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
         }
         return dirtyValues
             .map((value, i) => {
-                return createUpdatedTranslatable(
-                    value,
-                    dirtyValueValues[i],
-                    this.customValueFields,
+                return createUpdatedTranslatable({
+                    translatable: value,
+                    updatedFields: dirtyValueValues[i],
+                    customFieldConfig: this.customValueFields,
                     languageCode,
-                );
+                });
             })
             .filter(notNullOrUndefined);
     }

+ 11 - 15
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -78,10 +78,6 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         this.destroy();
     }
 
-    setLanguage(code: LanguageCode) {
-        this.setQueryParam('lang', code);
-    }
-
     customFieldIsSet(name: string): boolean {
         return !!this.productForm.get(['product', 'customFields', name]);
     }
@@ -269,18 +265,18 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         productFormGroup: FormGroup,
         languageCode: LanguageCode,
     ): UpdateProductInput | CreateProductInput {
-        const updatedProduct = createUpdatedTranslatable(
-            product,
-            productFormGroup.value,
-            this.customFields,
+        const updatedProduct = createUpdatedTranslatable({
+            translatable: product,
+            updatedFields: productFormGroup.value,
+            customFieldConfig: this.customFields,
             languageCode,
-            {
+            defaultTranslation: {
                 languageCode,
                 name: product.name || '',
                 slug: product.slug || '',
                 description: product.description || '',
             },
-        );
+        });
         return { ...updatedProduct, ...this.assetChanges } as UpdateProductInput | CreateProductInput;
     }
 
@@ -304,12 +300,12 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         }
         return dirtyVariants
             .map((variant, i) => {
-                const updated = createUpdatedTranslatable(
-                    variant,
-                    dirtyVariantValues[i],
-                    this.customVariantFields,
+                const updated = createUpdatedTranslatable({
+                    translatable: variant,
+                    updatedFields: dirtyVariantValues[i],
+                    customFieldConfig: this.customVariantFields,
                     languageCode,
-                ) as UpdateProductVariantInput;
+                }) as UpdateProductVariantInput;
                 updated.taxCategoryId = dirtyVariantValues[i].taxCategoryId;
                 return updated;
             })

+ 4 - 0
admin-ui/src/app/common/base-detail.component.ts

@@ -44,6 +44,10 @@ export abstract class BaseDetailComponent<Entity extends { id: string }> {
         this.destroy$.complete();
     }
 
+    setLanguage(code: LanguageCode) {
+        this.setQueryParam('lang', code);
+    }
+
     protected abstract setFormValues(entity: Entity, languageCode: LanguageCode): void;
 
     protected getCustomFieldConfig(key: keyof CustomFields): CustomFieldConfig[] {

+ 59 - 8
admin-ui/src/app/common/utilities/create-updated-translatable.spec.ts

@@ -23,7 +23,11 @@ describe('createUpdatedTranslatable()', () => {
 
     it('returns a clone', () => {
         const formValue = {};
-        const result = createUpdatedTranslatable(product, formValue, [], LanguageCode.en);
+        const result = createUpdatedTranslatable({
+            translatable: product,
+            updatedFields: formValue,
+            languageCode: LanguageCode.en,
+        });
 
         expect(result).not.toBe(product);
     });
@@ -32,9 +36,14 @@ describe('createUpdatedTranslatable()', () => {
         const formValue = {
             name: 'New Name AA',
         };
-        const result = createUpdatedTranslatable(product, formValue, [], LanguageCode.aa, {
+        const result = createUpdatedTranslatable({
+            translatable: product,
+            updatedFields: formValue,
             languageCode: LanguageCode.aa,
-            name: product.name || '',
+            defaultTranslation: {
+                languageCode: LanguageCode.aa,
+                name: product.name || '',
+            },
         });
 
         expect(result.translations[2]).toEqual({
@@ -48,7 +57,11 @@ describe('createUpdatedTranslatable()', () => {
             image: 'new-image.jpg',
         };
 
-        const result = createUpdatedTranslatable(product, formValue, [], LanguageCode.en);
+        const result = createUpdatedTranslatable({
+            translatable: product,
+            updatedFields: formValue,
+            languageCode: LanguageCode.en,
+        });
 
         if (!result) {
             fail('Expected result to be truthy');
@@ -63,7 +76,11 @@ describe('createUpdatedTranslatable()', () => {
             name: 'New Name EN',
         };
 
-        const result = createUpdatedTranslatable(product, formValue, [], LanguageCode.en);
+        const result = createUpdatedTranslatable({
+            translatable: product,
+            updatedFields: formValue,
+            languageCode: LanguageCode.en,
+        });
 
         if (!result) {
             fail('Expected result to be truthy');
@@ -75,6 +92,25 @@ describe('createUpdatedTranslatable()', () => {
         expect(result.translations[1]!.name).toBe('Old Name DE');
     });
 
+    it('omits the customFields property if the customFieldConfig is not defined', () => {
+        const formValue = {
+            name: 'New Name EN',
+        };
+
+        const result = createUpdatedTranslatable({
+            translatable: product,
+            updatedFields: formValue,
+            languageCode: LanguageCode.en,
+        });
+
+        if (!result) {
+            fail('Expected result to be truthy');
+            return;
+        }
+
+        expect(result.customFields).toBeUndefined();
+    });
+
     it('updates custom fields correctly', () => {
         const customFieldConfig: CustomFieldConfig[] = [
             { name: 'available', type: 'boolean' },
@@ -93,7 +129,12 @@ describe('createUpdatedTranslatable()', () => {
             },
         };
 
-        const result = createUpdatedTranslatable(product, formValue, customFieldConfig, LanguageCode.en);
+        const result = createUpdatedTranslatable({
+            translatable: product,
+            updatedFields: formValue,
+            customFieldConfig,
+            languageCode: LanguageCode.en,
+        });
 
         if (!result) {
             fail('Expected result to be truthy');
@@ -121,7 +162,12 @@ describe('createUpdatedTranslatable()', () => {
             },
         };
 
-        const result = createUpdatedTranslatable(product, formValue, customFieldConfig, LanguageCode.en);
+        const result = createUpdatedTranslatable({
+            translatable: product,
+            updatedFields: formValue,
+            customFieldConfig,
+            languageCode: LanguageCode.en,
+        });
 
         if (!result) {
             fail('Expected result to be truthy');
@@ -155,7 +201,12 @@ describe('createUpdatedTranslatable()', () => {
             },
         };
 
-        const result = createUpdatedTranslatable(product, formValue, customFieldConfig, LanguageCode.en);
+        const result = createUpdatedTranslatable({
+            translatable: product,
+            updatedFields: formValue,
+            customFieldConfig,
+            languageCode: LanguageCode.en,
+        });
 
         expect(result.customFields.a).toBe(false);
         expect(result.customFields.b).toBe(0);

+ 13 - 6
admin-ui/src/app/common/utilities/create-updated-translatable.ts

@@ -8,18 +8,23 @@ import {
 } from 'shared/shared-types';
 import { assertNever } from 'shared/shared-utils';
 
+export interface TranslatableUpdateOptions<T extends { translations: any[] } & MayHaveCustomFields> {
+    translatable: T;
+    updatedFields: { [key: string]: any };
+    languageCode: LanguageCode;
+    customFieldConfig?: CustomFieldConfig[];
+    defaultTranslation?: Partial<T['translations'][number]>;
+}
+
 /**
  * When updating an entity which has translations, the value from the form will pertain to the current
  * languageCode. This function ensures that the "translations" array is correctly set based on the
  * existing languages and the updated values in the specified language.
  */
 export function createUpdatedTranslatable<T extends { translations: any[] } & MayHaveCustomFields>(
-    translatable: T,
-    updatedFields: { [key: string]: any },
-    customFieldConfig: CustomFieldConfig[],
-    languageCode: LanguageCode,
-    defaultTranslation?: Partial<T['translations'][number]>,
+    options: TranslatableUpdateOptions<T>,
 ): T {
+    const { translatable, updatedFields, languageCode, customFieldConfig, defaultTranslation } = options;
     const currentTranslation =
         translatable.translations.find(t => t.languageCode === languageCode) || defaultTranslation;
     const index = translatable.translations.indexOf(currentTranslation);
@@ -41,8 +46,10 @@ export function createUpdatedTranslatable<T extends { translations: any[] } & Ma
     const newTranslatable = {
         ...(patchObject(translatable, updatedFields) as any),
         ...{ translations: translatable.translations.slice() },
-        customFields: newCustomFields,
     };
+    if (customFieldConfig) {
+        newTranslatable.customFields = newCustomFields;
+    }
     if (index !== -1) {
         newTranslatable.translations.splice(index, 1, newTranslation);
     } else {

+ 5 - 0
admin-ui/src/app/data/definitions/settings-definitions.ts

@@ -6,6 +6,11 @@ export const COUNTRY_FRAGMENT = gql`
         code
         name
         enabled
+        translations {
+            id
+            languageCode
+            name
+        }
     }
 `;
 

+ 3 - 2
admin-ui/src/app/data/providers/settings-data.service.ts

@@ -33,6 +33,7 @@ import {
     UpdateZone,
     UpdateZoneInput,
 } from 'shared/generated-types';
+import { pick } from 'shared/pick';
 
 import {
     ADD_MEMBERS_TO_ZONE,
@@ -79,13 +80,13 @@ export class SettingsDataService {
 
     createCountry(input: CreateCountryInput) {
         return this.baseDataService.mutate<CreateCountry.Mutation, CreateCountry.Variables>(CREATE_COUNTRY, {
-            input,
+            input: pick(input, ['code', 'enabled', 'translations']),
         });
     }
 
     updateCountry(input: UpdateCountryInput) {
         return this.baseDataService.mutate<UpdateCountry.Mutation, UpdateCountry.Variables>(UPDATE_COUNTRY, {
-            input,
+            input: pick(input, ['id', 'code', 'enabled', 'translations']),
         });
     }
 

+ 5 - 1
admin-ui/src/app/settings/components/country-detail/country-detail.component.html

@@ -1,5 +1,9 @@
 <vdr-action-bar>
-    <vdr-ab-left></vdr-ab-left>
+    <vdr-ab-left>
+        <vdr-language-selector [availableLanguageCodes]="availableLanguages$ | async"
+                               [currentLanguageCode]="languageCode$ | async"
+                               (languageCodeChange)="setLanguage($event)"></vdr-language-selector>
+    </vdr-ab-left>
     <vdr-ab-right>
         <button class="btn btn-primary"
                 *ngIf="isNew$ | async; else updateButton"

+ 58 - 40
admin-ui/src/app/settings/components/country-detail/country-detail.component.ts

@@ -1,11 +1,12 @@
 import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
-import { Observable } from 'rxjs';
+import { combineLatest, Observable } from 'rxjs';
 import { mergeMap, take } from 'rxjs/operators';
-import { Country, CreateCountryInput, UpdateCountryInput } from 'shared/generated-types';
+import { Country, CreateCountryInput, LanguageCode, UpdateCountryInput } from 'shared/generated-types';
 
 import { BaseDetailComponent } from '../../../common/base-detail.component';
+import { createUpdatedTranslatable } from '../../../common/utilities/create-updated-translatable';
 import { _ } from '../../../core/providers/i18n/mark-for-extraction';
 import { NotificationService } from '../../../core/providers/notification/notification.service';
 import { DataService } from '../../../data/providers/data.service';
@@ -16,8 +17,9 @@ import { ServerConfigService } from '../../../data/server-config';
     templateUrl: './country-detail.component.html',
     styleUrls: ['./country-detail.component.scss'],
 })
-export class CountryDetailComponent extends BaseDetailComponent<Country> implements OnInit, OnDestroy {
-    country$: Observable<Country>;
+export class CountryDetailComponent extends BaseDetailComponent<Country.Fragment>
+    implements OnInit, OnDestroy {
+    country$: Observable<Country.Fragment>;
     countryForm: FormGroup;
 
     constructor(
@@ -47,42 +49,55 @@ export class CountryDetailComponent extends BaseDetailComponent<Country> impleme
     }
 
     create() {
-        const formValue = this.countryForm.value;
-        const country: CreateCountryInput = {
-            code: formValue.code,
-            name: formValue.name,
-            enabled: formValue.enabled,
-        };
-        this.dataService.settings.createCountry(country).subscribe(
-            data => {
-                this.notificationService.success(_('common.notify-create-success'), {
-                    entity: 'Country',
-                });
-                this.countryForm.markAsPristine();
-                this.changeDetector.markForCheck();
-                this.router.navigate(['../', data.createCountry.id], { relativeTo: this.route });
-            },
-            err => {
-                this.notificationService.error(_('common.notify-create-error'), {
-                    entity: 'Country',
-                });
-            },
-        );
+        if (!this.countryForm.dirty) {
+            return;
+        }
+        combineLatest(this.country$, this.languageCode$)
+            .pipe(
+                take(1),
+                mergeMap(([country, languageCode]) => {
+                    const formValue = this.countryForm.value;
+                    const input: CreateCountryInput = createUpdatedTranslatable({
+                        translatable: country,
+                        updatedFields: formValue,
+                        languageCode,
+                        defaultTranslation: {
+                            name: formValue.name,
+                            languageCode,
+                        },
+                    });
+                    return this.dataService.settings.createCountry(input);
+                }),
+            )
+            .subscribe(
+                data => {
+                    this.notificationService.success(_('common.notify-create-success'), {
+                        entity: 'Country',
+                    });
+                    this.countryForm.markAsPristine();
+                    this.changeDetector.markForCheck();
+                    this.router.navigate(['../', data.createCountry.id], { relativeTo: this.route });
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-create-error'), {
+                        entity: 'Country',
+                    });
+                },
+            );
     }
 
     save() {
-        this.country$
+        combineLatest(this.country$, this.languageCode$)
             .pipe(
                 take(1),
-                mergeMap(({ id }) => {
+                mergeMap(([country, languageCode]) => {
                     const formValue = this.countryForm.value;
-                    const country: UpdateCountryInput = {
-                        id,
-                        code: formValue.code,
-                        name: formValue.name,
-                        enabled: formValue.enabled,
-                    };
-                    return this.dataService.settings.updateCountry(country);
+                    const input: UpdateCountryInput = createUpdatedTranslatable({
+                        translatable: country,
+                        updatedFields: formValue,
+                        languageCode,
+                    });
+                    return this.dataService.settings.updateCountry(input);
                 }),
             )
             .subscribe(
@@ -101,11 +116,14 @@ export class CountryDetailComponent extends BaseDetailComponent<Country> impleme
             );
     }
 
-    protected setFormValues(country: Country): void {
-        this.countryForm.patchValue({
-            code: country.code,
-            name: country.name,
-            enabled: country.enabled,
-        });
+    protected setFormValues(country: Country, languageCode: LanguageCode): void {
+        const currentTranslation = country.translations.find(t => t.languageCode === languageCode);
+        if (currentTranslation) {
+            this.countryForm.patchValue({
+                code: country.code,
+                name: currentTranslation.name,
+                enabled: country.enabled,
+            });
+        }
     }
 }

+ 1 - 0
admin-ui/src/app/settings/providers/routing/country-resolver.ts

@@ -17,6 +17,7 @@ export class CountryResolver extends BaseEntityResolver<Country.Fragment> {
                 code: '',
                 name: '',
                 enabled: false,
+                translations: [],
             },
             id => this.dataService.settings.getCountry(id).mapStream(data => data.country),
         );

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
schema.json


+ 44 - 1
server/mock-data/mock-data.service.ts

@@ -14,6 +14,7 @@ import {
     CreateFacetValueWithFacetInput,
     CreateProduct,
     CreateProductOptionGroup,
+    CreateShippingMethod,
     CreateTaxRate,
     CreateZone,
     GenerateProductVariants,
@@ -42,6 +43,9 @@ import {
     GET_CHANNELS,
     UPDATE_CHANNEL,
 } from '../../admin-ui/src/app/data/definitions/settings-definitions';
+import { CREATE_SHIPPING_METHOD } from '../../admin-ui/src/app/data/definitions/shipping-definitions';
+import { defaultShippingCalculator } from '../src/config/shipping-method/default-shipping-calculator';
+import { defaultShippingEligibilityChecker } from '../src/config/shipping-method/default-shipping-eligibility-checker';
 import { Customer } from '../src/entity/customer/customer.entity';
 
 import { SimpleGraphQLClient } from './simple-graphql-client';
@@ -92,7 +96,7 @@ export class MockDataService {
                 {
                     input: {
                         code: country['alpha-2'],
-                        name: country.name,
+                        translations: [{ languageCode: LanguageCode.en, name: country.name }],
                         enabled: true,
                     },
                 },
@@ -212,6 +216,45 @@ export class MockDataService {
         return createdTaxCategories;
     }
 
+    async populateShippingMethods() {
+        await this.client.query<CreateShippingMethod.Mutation, CreateShippingMethod.Variables>(
+            CREATE_SHIPPING_METHOD,
+            {
+                input: {
+                    code: 'standard-flat-rate',
+                    description: 'Standard Shipping',
+                    checker: {
+                        code: defaultShippingEligibilityChecker.code,
+                        arguments: [],
+                    },
+                    calculator: {
+                        code: defaultShippingCalculator.code,
+                        arguments: [{ name: 'rate', value: '500' }],
+                    },
+                },
+            },
+        );
+        this.log(`Created standard ShippingMethod`);
+        await this.client.query<CreateShippingMethod.Mutation, CreateShippingMethod.Variables>(
+            CREATE_SHIPPING_METHOD,
+            {
+                input: {
+                    code: 'express-flat-rate',
+                    description: 'Express Shipping',
+                    checker: {
+                        code: defaultShippingEligibilityChecker.code,
+                        arguments: [],
+                    },
+                    calculator: {
+                        code: defaultShippingCalculator.code,
+                        arguments: [{ name: 'rate', value: '1000' }],
+                    },
+                },
+            },
+        );
+        this.log(`Created express ShippingMethod`);
+    }
+
     async populateCustomers(count: number = 5): Promise<any> {
         for (let i = 0; i < count; i++) {
             const firstName = faker.name.firstName();

+ 1 - 0
server/mock-data/populate.ts

@@ -40,6 +40,7 @@ export async function populate(
     }
     const zones = await mockDataService.populateCountries();
     await mockDataService.setChannelDefaultZones(zones);
+    await mockDataService.populateShippingMethods();
     const assets = await mockDataService.populateAssets();
     const optionGroupId = await mockDataService.populateOptions();
     const taxCategories = await mockDataService.populateTaxCategories(zones);

+ 40 - 9
server/src/api/resolvers/country.resolver.ts

@@ -8,8 +8,8 @@ import {
 } from 'shared/generated-types';
 import { PaginatedList } from 'shared/shared-types';
 
+import { Translated } from '../../common/types/locale-types';
 import { Country } from '../../entity/country/country.entity';
-import { Facet } from '../../entity/facet/facet.entity';
 import { CountryService } from '../../service/services/country.service';
 import { RequestContext } from '../common/request-context';
 import { Allow } from '../decorators/allow.decorator';
@@ -21,25 +21,56 @@ export class CountryResolver {
 
     @Query()
     @Allow(Permission.ReadSettings)
-    countries(@Ctx() ctx: RequestContext, @Args() args: CountriesQueryArgs): Promise<PaginatedList<Country>> {
-        return this.countryService.findAll(args.options || undefined);
+    countries(
+        @Ctx() ctx: RequestContext,
+        @Args() args: CountriesQueryArgs,
+    ): Promise<PaginatedList<Translated<Country>>> {
+        return this.countryService.findAll(ctx, args.options || undefined);
+    }
+
+    @Query()
+    @Allow(Permission.Public)
+    availableCountries(
+        @Ctx() ctx: RequestContext,
+        @Args() args: CountriesQueryArgs,
+    ): Promise<Array<Translated<Country>>> {
+        return this.countryService
+            .findAll(ctx, {
+                filter: {
+                    enabled: {
+                        eq: true,
+                    },
+                },
+                skip: 0,
+                take: 99999,
+            })
+            .then(data => data.items);
     }
 
     @Query()
     @Allow(Permission.ReadSettings)
-    async country(@Ctx() ctx: RequestContext, @Args() args: CountryQueryArgs): Promise<Country | undefined> {
-        return this.countryService.findOne(args.id);
+    async country(
+        @Ctx() ctx: RequestContext,
+        @Args() args: CountryQueryArgs,
+    ): Promise<Translated<Country> | undefined> {
+        return this.countryService.findOne(ctx, args.id);
     }
 
     @Mutation()
     @Allow(Permission.CreateSettings)
-    async createCountry(@Args() args: CreateCountryMutationArgs): Promise<Country> {
-        return this.countryService.create(args.input);
+    async createCountry(
+        @Ctx() ctx: RequestContext,
+        @Args() args: CreateCountryMutationArgs,
+    ): Promise<Translated<Country>> {
+        return this.countryService.create(ctx, args.input);
     }
 
     @Mutation()
     @Allow(Permission.UpdateSettings)
-    async updateCountry(@Args() args: UpdateCountryMutationArgs): Promise<Country> {
-        return this.countryService.update(args.input);
+    async updateCountry(
+        @Ctx() ctx: RequestContext,
+        @Args() args: UpdateCountryMutationArgs,
+    ): Promise<Translated<Country>> {
+        return this.countryService.update(ctx, args.input);
     }
 }

+ 16 - 10
server/src/api/resolvers/zone.resolver.ts

@@ -22,39 +22,45 @@ export class ZoneResolver {
     @Query()
     @Allow(Permission.ReadSettings)
     zones(@Ctx() ctx: RequestContext): Promise<Zone[]> {
-        return this.zoneService.findAll();
+        return this.zoneService.findAll(ctx);
     }
 
     @Query()
     @Allow(Permission.ReadSettings)
     async zone(@Ctx() ctx: RequestContext, @Args() args: ZoneQueryArgs): Promise<Zone | undefined> {
-        return this.zoneService.findOne(args.id);
+        return this.zoneService.findOne(ctx, args.id);
     }
 
     @Mutation()
     @Allow(Permission.CreateSettings)
     @Decode('memberIds')
-    async createZone(@Args() args: CreateZoneMutationArgs): Promise<Zone> {
-        return this.zoneService.create(args.input);
+    async createZone(@Ctx() ctx: RequestContext, @Args() args: CreateZoneMutationArgs): Promise<Zone> {
+        return this.zoneService.create(ctx, args.input);
     }
 
     @Mutation()
     @Allow(Permission.UpdateSettings)
-    async updateZone(@Args() args: UpdateZoneMutationArgs): Promise<Zone> {
-        return this.zoneService.update(args.input);
+    async updateZone(@Ctx() ctx: RequestContext, @Args() args: UpdateZoneMutationArgs): Promise<Zone> {
+        return this.zoneService.update(ctx, args.input);
     }
 
     @Mutation()
     @Allow(Permission.UpdateSettings)
     @Decode('zoneId', 'memberIds')
-    async addMembersToZone(@Args() args: AddMembersToZoneMutationArgs): Promise<Zone> {
-        return this.zoneService.addMembersToZone(args);
+    async addMembersToZone(
+        @Ctx() ctx: RequestContext,
+        @Args() args: AddMembersToZoneMutationArgs,
+    ): Promise<Zone> {
+        return this.zoneService.addMembersToZone(ctx, args);
     }
 
     @Mutation()
     @Allow(Permission.UpdateSettings)
     @Decode('zoneId', 'memberIds')
-    async removeMembersFromZone(@Args() args: RemoveMembersFromZoneMutationArgs): Promise<Zone> {
-        return this.zoneService.removeMembersFromZone(args);
+    async removeMembersFromZone(
+        @Ctx() ctx: RequestContext,
+        @Args() args: RemoveMembersFromZoneMutationArgs,
+    ): Promise<Zone> {
+        return this.zoneService.removeMembersFromZone(ctx, args);
     }
 }

+ 7 - 6
server/src/api/types/country.api.graphql

@@ -1,13 +1,14 @@
 type Query {
-  countries(options: CountryListOptions): CountryList!
-  country(id: ID!): Country
+    countries(options: CountryListOptions): CountryList!
+    country(id: ID!): Country
+    availableCountries: [Country!]!
 }
 
 type Mutation {
-  "Create a new Country"
-  createCountry(input: CreateCountryInput!): Country!
-  "Update an existing Country"
-  updateCountry(input: UpdateCountryInput!): Country!
+    "Create a new Country"
+    createCountry(input: CreateCountryInput!): Country!
+    "Update an existing Country"
+    updateCountry(input: UpdateCountryInput!): Country!
 }
 
 type CountryList implements PaginatedList {

+ 22 - 0
server/src/entity/country/country-translation.entity.ts

@@ -0,0 +1,22 @@
+import { LanguageCode } from 'shared/generated-types';
+import { DeepPartial } from 'shared/shared-types';
+import { Column, Entity, ManyToOne } from 'typeorm';
+
+import { Translation } from '../../common/types/locale-types';
+import { VendureEntity } from '../base/base.entity';
+
+import { Country } from './country.entity';
+
+@Entity()
+export class CountryTranslation extends VendureEntity implements Translation<Country> {
+    constructor(input?: DeepPartial<Translation<CountryTranslation>>) {
+        super(input);
+    }
+
+    @Column('varchar') languageCode: LanguageCode;
+
+    @Column() name: string;
+
+    @ManyToOne(type => Country, base => base.translations)
+    base: Country;
+}

+ 9 - 3
server/src/entity/country/country.entity.ts

@@ -1,17 +1,23 @@
 import { DeepPartial } from 'shared/shared-types';
-import { Column, Entity } from 'typeorm';
+import { Column, Entity, OneToMany } from 'typeorm';
 
+import { LocaleString, Translatable, Translation } from '../../common/types/locale-types';
 import { VendureEntity } from '../base/base.entity';
 
+import { CountryTranslation } from './country-translation.entity';
+
 @Entity()
-export class Country extends VendureEntity {
+export class Country extends VendureEntity implements Translatable {
     constructor(input?: DeepPartial<Country>) {
         super(input);
     }
 
     @Column() code: string;
 
-    @Column() name: string;
+    name: LocaleString;
 
     @Column() enabled: boolean;
+
+    @OneToMany(type => CountryTranslation, translation => translation.base, { eager: true })
+    translations: Array<Translation<Country>>;
 }

+ 18 - 2
server/src/entity/country/country.graphql

@@ -1,13 +1,29 @@
 type Country implements Node {
     id: ID!
+    languageCode: LanguageCode!
     code: String!
     name: String!
     enabled: Boolean!
+    translations: [CountryTranslation!]!
+}
+
+type CountryTranslation {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    languageCode: LanguageCode!
+    name: String!
+}
+
+input CountryTranslationInput {
+    id: ID
+    languageCode: LanguageCode!
+    name: String
 }
 
 input CreateCountryInput {
     code: String!
-    name: String!
+    translations: [CountryTranslationInput!]!
     enabled: Boolean!
 }
 
@@ -15,6 +31,6 @@ input CreateCountryInput {
 input UpdateCountryInput {
     id: ID!
     code: String
-    name: String
+    translations: [CountryTranslationInput!]
     enabled: Boolean
 }

+ 2 - 0
server/src/entity/entities.ts

@@ -2,6 +2,7 @@ import { Address } from './address/address.entity';
 import { Administrator } from './administrator/administrator.entity';
 import { Asset } from './asset/asset.entity';
 import { Channel } from './channel/channel.entity';
+import { CountryTranslation } from './country/country-translation.entity';
 import { Country } from './country/country.entity';
 import { CustomerGroup } from './customer-group/customer-group.entity';
 import { Customer } from './customer/customer.entity';
@@ -43,6 +44,7 @@ export const coreEntitiesMap = {
     AuthenticatedSession,
     Channel,
     Country,
+    CountryTranslation,
     Customer,
     CustomerGroup,
     Facet,

+ 1 - 1
server/src/entity/facet/facet-translation.entity.ts

@@ -10,7 +10,7 @@ import { Facet } from './facet.entity';
 
 @Entity()
 export class FacetTranslation extends VendureEntity implements Translation<Facet>, HasCustomFields {
-    constructor(input?: DeepPartial<Translation<Facet>>) {
+    constructor(input?: DeepPartial<Translation<FacetTranslation>>) {
         super(input);
     }
 

+ 2 - 2
server/src/service/helpers/translatable-saver/translatable-saver.ts

@@ -9,7 +9,7 @@ import { patchEntity } from '../utils/patch-entity';
 
 import { TranslationDiffer } from './translation-differ';
 
-export interface CreateTranslatableOptions<T> {
+export interface CreateTranslatableOptions<T extends Translatable> {
     entityType: Type<T>;
     translationType: Type<Translation<T>>;
     input: TranslatedInput<T>;
@@ -17,7 +17,7 @@ export interface CreateTranslatableOptions<T> {
     typeOrmSubscriberData?: any;
 }
 
-export interface UpdateTranslatableOptions<T> extends CreateTranslatableOptions<T> {
+export interface UpdateTranslatableOptions<T extends Translatable> extends CreateTranslatableOptions<T> {
     input: TranslatedInput<T> & { id: ID };
 }
 

+ 36 - 20
server/src/service/services/country.service.ts

@@ -4,46 +4,62 @@ import { CreateCountryInput, UpdateCountryInput } from 'shared/generated-types';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
+import { RequestContext } from '../../api/common/request-context';
 import { ListQueryOptions } from '../../common/types/common-types';
+import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
+import { CountryTranslation } from '../../entity/country/country-translation.entity';
 import { Country } from '../../entity/country/country.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
-import { patchEntity } from '../helpers/utils/patch-entity';
+import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { translateDeep } from '../helpers/utils/translate-entity';
 
 @Injectable()
 export class CountryService {
     constructor(
         @InjectConnection() private connection: Connection,
         private listQueryBuilder: ListQueryBuilder,
+        private translatableSaver: TranslatableSaver,
     ) {}
 
-    findAll(options?: ListQueryOptions<Country>): Promise<PaginatedList<Country>> {
+    findAll(
+        ctx: RequestContext,
+        options?: ListQueryOptions<Country>,
+    ): Promise<PaginatedList<Translated<Country>>> {
         return this.listQueryBuilder
             .build(Country, options)
             .getManyAndCount()
-            .then(([items, totalItems]) => ({
-                items,
-                totalItems,
-            }));
+            .then(([countries, totalItems]) => {
+                const items = countries.map(country => translateDeep(country, ctx.languageCode));
+                return {
+                    items,
+                    totalItems,
+                };
+            });
     }
 
-    findOne(countryId: ID): Promise<Country | undefined> {
-        return this.connection.getRepository(Country).findOne(countryId);
+    findOne(ctx: RequestContext, countryId: ID): Promise<Translated<Country> | undefined> {
+        return this.connection
+            .getRepository(Country)
+            .findOne(countryId)
+            .then(country => country && translateDeep(country, ctx.languageCode));
     }
 
-    async create(input: CreateCountryInput): Promise<Country> {
-        const country = new Country(input);
-        return this.connection.getRepository(Country).save(country);
+    async create(ctx: RequestContext, input: CreateCountryInput): Promise<Translated<Country>> {
+        const country = await this.translatableSaver.create({
+            input,
+            entityType: Country,
+            translationType: CountryTranslation,
+        });
+        return assertFound(this.findOne(ctx, country.id));
     }
 
-    async update(input: UpdateCountryInput): Promise<Country> {
-        const country = await this.findOne(input.id);
-        if (!country) {
-            throw new I18nError(`error.entity-with-id-not-found`, { entityName: 'Country', id: input.id });
-        }
-        const updatedCountry = patchEntity(country, input);
-        await this.connection.getRepository(Country).save(updatedCountry);
-        return assertFound(this.findOne(country.id));
+    async update(ctx: RequestContext, input: UpdateCountryInput): Promise<Translated<Country>> {
+        const country = await this.translatableSaver.update({
+            input,
+            entityType: Country,
+            translationType: CountryTranslation,
+        });
+        return assertFound(this.findOne(ctx, country.id));
     }
 }

+ 40 - 16
server/src/service/services/zone.service.ts

@@ -10,58 +10,82 @@ import { ID } from 'shared/shared-types';
 import { unique } from 'shared/unique';
 import { Connection } from 'typeorm';
 
+import { RequestContext } from '../../api/common/request-context';
 import { assertFound } from '../../common/utils';
 import { Country } from '../../entity/country/country.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
+import { translateDeep } from '../helpers/utils/translate-entity';
 
 @Injectable()
 export class ZoneService {
     constructor(@InjectConnection() private connection: Connection) {}
 
-    findAll(): Promise<Zone[]> {
-        return this.connection.getRepository(Zone).find({
-            relations: ['members'],
-        });
+    findAll(ctx: RequestContext): Promise<Zone[]> {
+        return this.connection
+            .getRepository(Zone)
+            .find({
+                relations: ['members'],
+            })
+            .then(zones => {
+                zones.forEach(
+                    zone =>
+                        (zone.members = zone.members.map(country =>
+                            translateDeep(country, ctx.languageCode),
+                        )),
+                );
+                return zones;
+            });
     }
 
-    findOne(zoneId: ID): Promise<Zone | undefined> {
-        return this.connection.getRepository(Zone).findOne(zoneId, {
-            relations: ['members'],
-        });
+    findOne(ctx: RequestContext, zoneId: ID): Promise<Zone | undefined> {
+        return this.connection
+            .getRepository(Zone)
+            .findOne(zoneId, {
+                relations: ['members'],
+            })
+            .then(zone => {
+                if (zone) {
+                    zone.members = zone.members.map(country => translateDeep(country, ctx.languageCode));
+                    return zone;
+                }
+            });
     }
 
-    async create(input: CreateZoneInput): Promise<Zone> {
+    async create(ctx: RequestContext, input: CreateZoneInput): Promise<Zone> {
         const zone = new Zone(input);
         if (input.memberIds) {
             zone.members = await this.getCountriesFromIds(input.memberIds);
         }
         const newZone = await this.connection.getRepository(Zone).save(zone);
-        return assertFound(this.findOne(newZone.id));
+        return assertFound(this.findOne(ctx, newZone.id));
     }
 
-    async update(input: UpdateZoneInput): Promise<Zone> {
+    async update(ctx: RequestContext, input: UpdateZoneInput): Promise<Zone> {
         const zone = await getEntityOrThrow(this.connection, Zone, input.id);
         const updatedZone = patchEntity(zone, input);
         await this.connection.getRepository(Zone).save(updatedZone);
-        return assertFound(this.findOne(zone.id));
+        return assertFound(this.findOne(ctx, zone.id));
     }
 
-    async addMembersToZone(input: AddMembersToZoneMutationArgs): Promise<Zone> {
+    async addMembersToZone(ctx: RequestContext, input: AddMembersToZoneMutationArgs): Promise<Zone> {
         const countries = await this.getCountriesFromIds(input.memberIds);
         const zone = await getEntityOrThrow(this.connection, Zone, input.zoneId);
         const members = unique(zone.members.concat(countries), 'id');
         zone.members = members;
         await this.connection.getRepository(Zone).save(zone);
-        return zone;
+        return assertFound(this.findOne(ctx, zone.id));
     }
 
-    async removeMembersFromZone(input: RemoveMembersFromZoneMutationArgs): Promise<Zone> {
+    async removeMembersFromZone(
+        ctx: RequestContext,
+        input: RemoveMembersFromZoneMutationArgs,
+    ): Promise<Zone> {
         const zone = await getEntityOrThrow(this.connection, Zone, input.zoneId);
         zone.members = zone.members.filter(country => !input.memberIds.includes(country.id as string));
         await this.connection.getRepository(Zone).save(zone);
-        return zone;
+        return assertFound(this.findOne(ctx, zone.id));
     }
 
     private getCountriesFromIds(ids: ID[]): Promise<Country[]> {

+ 65 - 2
shared/generated-types.ts

@@ -50,6 +50,7 @@ export interface Query {
     config: Config;
     countries: CountryList;
     country?: Country | null;
+    availableCountries: Country[];
     customerGroups: CustomerGroup[];
     customerGroup?: CustomerGroup | null;
     customers: CustomerList;
@@ -143,9 +144,19 @@ export interface Zone extends Node {
 
 export interface Country extends Node {
     id: string;
+    languageCode: LanguageCode;
     code: string;
     name: string;
     enabled: boolean;
+    translations: CountryTranslation[];
+}
+
+export interface CountryTranslation {
+    id: string;
+    createdAt: DateTime;
+    updatedAt: DateTime;
+    languageCode: LanguageCode;
+    name: string;
 }
 
 export interface AssetList extends PaginatedList {
@@ -909,14 +920,20 @@ export interface UpdateChannelInput {
 
 export interface CreateCountryInput {
     code: string;
-    name: string;
+    translations: CountryTranslationInput[];
     enabled: boolean;
 }
 
+export interface CountryTranslationInput {
+    id?: string | null;
+    languageCode: LanguageCode;
+    name?: string | null;
+}
+
 export interface UpdateCountryInput {
     id: string;
     code?: string | null;
-    name?: string | null;
+    translations?: CountryTranslationInput[] | null;
     enabled?: boolean | null;
 }
 
@@ -1751,6 +1768,7 @@ export namespace QueryResolvers {
         config?: ConfigResolver<Config, any, Context>;
         countries?: CountriesResolver<CountryList, any, Context>;
         country?: CountryResolver<Country | null, any, Context>;
+        availableCountries?: AvailableCountriesResolver<Country[], any, Context>;
         customerGroups?: CustomerGroupsResolver<CustomerGroup[], any, Context>;
         customerGroup?: CustomerGroupResolver<CustomerGroup | null, any, Context>;
         customers?: CustomersResolver<CustomerList, any, Context>;
@@ -1872,6 +1890,11 @@ export namespace QueryResolvers {
         id: string;
     }
 
+    export type AvailableCountriesResolver<R = Country[], Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
     export type CustomerGroupsResolver<R = CustomerGroup[], Parent = any, Context = any> = Resolver<
         R,
         Parent,
@@ -2288,15 +2311,47 @@ export namespace ZoneResolvers {
 export namespace CountryResolvers {
     export interface Resolvers<Context = any> {
         id?: IdResolver<string, any, Context>;
+        languageCode?: LanguageCodeResolver<LanguageCode, any, Context>;
         code?: CodeResolver<string, any, Context>;
         name?: NameResolver<string, any, Context>;
         enabled?: EnabledResolver<boolean, any, Context>;
+        translations?: TranslationsResolver<CountryTranslation[], any, Context>;
     }
 
     export type IdResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type LanguageCodeResolver<R = LanguageCode, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
     export type CodeResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type NameResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type EnabledResolver<R = boolean, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type TranslationsResolver<R = CountryTranslation[], Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
+}
+
+export namespace CountryTranslationResolvers {
+    export interface Resolvers<Context = any> {
+        id?: IdResolver<string, any, Context>;
+        createdAt?: CreatedAtResolver<DateTime, any, Context>;
+        updatedAt?: UpdatedAtResolver<DateTime, any, Context>;
+        languageCode?: LanguageCodeResolver<LanguageCode, any, Context>;
+        name?: NameResolver<string, any, Context>;
+    }
+
+    export type IdResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type CreatedAtResolver<R = DateTime, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type UpdatedAtResolver<R = DateTime, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type LanguageCodeResolver<R = LanguageCode, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
+    export type NameResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
 }
 
 export namespace AssetListResolvers {
@@ -5557,6 +5612,14 @@ export namespace Country {
         code: string;
         name: string;
         enabled: boolean;
+        translations: Translations[];
+    };
+
+    export type Translations = {
+        __typename?: 'CountryTranslation';
+        id: string;
+        languageCode: LanguageCode;
+        name: string;
     };
 }
 

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff