Browse Source

feat(admin-ui): Add support for translatable Promotions

Relates to #1990
Michael Bromley 2 years ago
parent
commit
00bd433235

+ 29 - 7
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -804,9 +804,9 @@ export type CreatePromotionInput = {
   customFields?: InputMaybe<Scalars['JSON']>;
   enabled: Scalars['Boolean'];
   endsAt?: InputMaybe<Scalars['DateTime']>;
-  name: Scalars['String'];
   perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
   startsAt?: InputMaybe<Scalars['DateTime']>;
+  translations: Array<PromotionTranslationInput>;
 };
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4487,18 +4487,21 @@ export type Promotion = Node & {
   couponCode?: Maybe<Scalars['String']>;
   createdAt: Scalars['DateTime'];
   customFields?: Maybe<Scalars['JSON']>;
+  description: Scalars['String'];
   enabled: Scalars['Boolean'];
   endsAt?: Maybe<Scalars['DateTime']>;
   id: Scalars['ID'];
   name: Scalars['String'];
   perCustomerUsageLimit?: Maybe<Scalars['Int']>;
   startsAt?: Maybe<Scalars['DateTime']>;
+  translations: Array<PromotionTranslation>;
   updatedAt: Scalars['DateTime'];
 };
 
 export type PromotionFilterParameter = {
   couponCode?: InputMaybe<StringOperators>;
   createdAt?: InputMaybe<DateOperators>;
+  description?: InputMaybe<StringOperators>;
   enabled?: InputMaybe<BooleanOperators>;
   endsAt?: InputMaybe<DateOperators>;
   id?: InputMaybe<IdOperators>;
@@ -4530,6 +4533,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
   couponCode?: InputMaybe<SortOrder>;
   createdAt?: InputMaybe<SortOrder>;
+  description?: InputMaybe<SortOrder>;
   endsAt?: InputMaybe<SortOrder>;
   id?: InputMaybe<SortOrder>;
   name?: InputMaybe<SortOrder>;
@@ -4538,6 +4542,24 @@ export type PromotionSortParameter = {
   updatedAt?: InputMaybe<SortOrder>;
 };
 
+export type PromotionTranslation = {
+  __typename?: 'PromotionTranslation';
+  createdAt: Scalars['DateTime'];
+  description: Scalars['String'];
+  id: Scalars['ID'];
+  languageCode: LanguageCode;
+  name: Scalars['String'];
+  updatedAt: Scalars['DateTime'];
+};
+
+export type PromotionTranslationInput = {
+  customFields?: InputMaybe<Scalars['JSON']>;
+  description?: InputMaybe<Scalars['String']>;
+  id?: InputMaybe<Scalars['ID']>;
+  languageCode: LanguageCode;
+  name?: InputMaybe<Scalars['String']>;
+};
+
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 export type QuantityTooGreatError = ErrorResult & {
   __typename?: 'QuantityTooGreatError';
@@ -5878,9 +5900,9 @@ export type UpdatePromotionInput = {
   enabled?: InputMaybe<Scalars['Boolean']>;
   endsAt?: InputMaybe<Scalars['DateTime']>;
   id: Scalars['ID'];
-  name?: InputMaybe<Scalars['String']>;
   perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
   startsAt?: InputMaybe<Scalars['DateTime']>;
+  translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 
 export type UpdatePromotionResult = MissingConditionsError | Promotion;
@@ -7088,21 +7110,21 @@ export type DeleteTagMutationVariables = Exact<{
 
 export type DeleteTagMutation = { deleteTag: { __typename?: 'DeletionResponse', message?: string | null, result: DeletionResult } };
 
-export type PromotionFragment = { __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }> };
+export type PromotionFragment = { __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, description: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, translations: Array<{ __typename?: 'PromotionTranslation', id: string, languageCode: LanguageCode, name: string, description: string }> };
 
 export type GetPromotionListQueryVariables = Exact<{
   options?: InputMaybe<PromotionListOptions>;
 }>;
 
 
-export type GetPromotionListQuery = { promotions: { __typename?: 'PromotionList', totalItems: number, items: Array<{ __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }> }> } };
+export type GetPromotionListQuery = { promotions: { __typename?: 'PromotionList', totalItems: number, items: Array<{ __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, description: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, translations: Array<{ __typename?: 'PromotionTranslation', id: string, languageCode: LanguageCode, name: string, description: string }> }> } };
 
 export type GetPromotionQueryVariables = Exact<{
   id: Scalars['ID'];
 }>;
 
 
-export type GetPromotionQuery = { promotion?: { __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }> } | null };
+export type GetPromotionQuery = { promotion?: { __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, description: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, translations: Array<{ __typename?: 'PromotionTranslation', id: string, languageCode: LanguageCode, name: string, description: string }> } | null };
 
 export type GetAdjustmentOperationsQueryVariables = Exact<{ [key: string]: never; }>;
 
@@ -7114,14 +7136,14 @@ export type CreatePromotionMutationVariables = Exact<{
 }>;
 
 
-export type CreatePromotionMutation = { createPromotion: { __typename?: 'MissingConditionsError', errorCode: ErrorCode, message: string } | { __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }> } };
+export type CreatePromotionMutation = { createPromotion: { __typename?: 'MissingConditionsError', errorCode: ErrorCode, message: string } | { __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, description: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, translations: Array<{ __typename?: 'PromotionTranslation', id: string, languageCode: LanguageCode, name: string, description: string }> } };
 
 export type UpdatePromotionMutationVariables = Exact<{
   input: UpdatePromotionInput;
 }>;
 
 
-export type UpdatePromotionMutation = { updatePromotion: { __typename?: 'MissingConditionsError' } | { __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }> } };
+export type UpdatePromotionMutation = { updatePromotion: { __typename?: 'MissingConditionsError' } | { __typename?: 'Promotion', id: string, createdAt: any, updatedAt: any, name: string, description: string, enabled: boolean, couponCode?: string | null, perCustomerUsageLimit?: number | null, startsAt?: any | null, endsAt?: any | null, conditions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, actions: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, translations: Array<{ __typename?: 'PromotionTranslation', id: string, languageCode: LanguageCode, name: string, description: string }> } };
 
 export type DeletePromotionMutationVariables = Exact<{
   id: Scalars['ID'];

+ 7 - 0
packages/admin-ui/src/lib/core/src/data/definitions/promotion-definitions.ts

@@ -12,6 +12,7 @@ export const PROMOTION_FRAGMENT = gql`
         createdAt
         updatedAt
         name
+        description
         enabled
         couponCode
         perCustomerUsageLimit
@@ -23,6 +24,12 @@ export const PROMOTION_FRAGMENT = gql`
         actions {
             ...ConfigurableOperation
         }
+        translations {
+            id
+            languageCode
+            name
+            description
+        }
     }
     ${CONFIGURABLE_OPERATION_FRAGMENT}
 `;

+ 25 - 2
packages/admin-ui/src/lib/core/src/data/providers/promotion-data.service.ts

@@ -1,3 +1,5 @@
+import { pick } from '@vendure/common/lib/pick';
+
 import * as Codegen from '../../common/generated-types';
 import {
     CREATE_PROMOTION,
@@ -44,7 +46,17 @@ export class PromotionDataService {
             Codegen.CreatePromotionMutation,
             Codegen.CreatePromotionMutationVariables
         >(CREATE_PROMOTION, {
-            input,
+            input: pick(input, [
+                'conditions',
+                'actions',
+                'couponCode',
+                'startsAt',
+                'endsAt',
+                'perCustomerUsageLimit',
+                'enabled',
+                'translations',
+                'customFields',
+            ]),
         });
     }
 
@@ -53,7 +65,18 @@ export class PromotionDataService {
             Codegen.UpdatePromotionMutation,
             Codegen.UpdatePromotionMutationVariables
         >(UPDATE_PROMOTION, {
-            input,
+            input: pick(input, [
+                'id',
+                'conditions',
+                'actions',
+                'couponCode',
+                'startsAt',
+                'endsAt',
+                'perCustomerUsageLimit',
+                'enabled',
+                'translations',
+                'customFields',
+            ]),
         });
     }
 

+ 11 - 0
packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.html

@@ -6,6 +6,12 @@
                 <input type="checkbox" clrToggle name="enabled" [formControl]="detailForm.get(['enabled'])" />
                 <label>{{ 'common.enabled' | translate }}</label>
             </clr-toggle-wrapper>
+            <vdr-language-selector
+                [disabled]="isNew$ | async"
+                [availableLanguageCodes]="availableLanguages$ | async"
+                [currentLanguageCode]="languageCode$ | async"
+                (languageCodeChange)="setLanguage($event)"
+            ></vdr-language-selector>
         </div>
     </vdr-ab-left>
 
@@ -41,6 +47,11 @@
             formControlName="name"
         />
     </vdr-form-field>
+    <vdr-rich-text-editor
+        formControlName="description"
+        [readonly]="!('UpdatePromotion' | hasPermission)"
+        [label]="'common.description' | translate"
+    ></vdr-rich-text-editor>
     <vdr-form-field [label]="'marketing.starts-at' | translate" for="startsAt">
         <vdr-datetime-picker formControlName="startsAt"></vdr-datetime-picker>
     </vdr-form-field>

+ 81 - 51
packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.ts

@@ -7,19 +7,25 @@ import {
     ConfigurableOperation,
     ConfigurableOperationDefinition,
     ConfigurableOperationInput,
+    CreatePaymentMethodInput,
     CreatePromotionInput,
+    createUpdatedTranslatable,
     CustomFieldConfig,
     DataService,
     encodeConfigArgValue,
+    findTranslation,
     getConfigArgValue,
     getDefaultConfigArgValue,
     LanguageCode,
     NotificationService,
+    PaymentMethodFragment,
     PromotionFragment,
     ServerConfigService,
+    toConfigurableOperationInput,
+    UpdatePaymentMethodInput,
     UpdatePromotionInput,
 } from '@vendure/admin-ui/core';
-import { Observable } from 'rxjs';
+import { combineLatest, Observable } from 'rxjs';
 import { mergeMap, take } from 'rxjs/operators';
 
 @Component({
@@ -54,6 +60,7 @@ export class PromotionDetailComponent
         this.customFields = this.getCustomFieldConfig('Promotion');
         this.detailForm = this.formBuilder.group({
             name: ['', Validators.required],
+            description: '',
             enabled: true,
             couponCode: null,
             perCustomerUsageLimit: null,
@@ -134,63 +141,55 @@ export class PromotionDetailComponent
         if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.detailForm.value;
-        const input: CreatePromotionInput = {
-            name: formValue.name,
-            enabled: true,
-            couponCode: formValue.couponCode,
-            perCustomerUsageLimit: formValue.perCustomerUsageLimit,
-            startsAt: formValue.startsAt,
-            endsAt: formValue.endsAt,
-            conditions: this.mapOperationsToInputs(this.conditions, formValue.conditions),
-            actions: this.mapOperationsToInputs(this.actions, formValue.actions),
-            customFields: formValue.customFields,
-        };
-        this.dataService.promotion.createPromotion(input).subscribe(
-            ({ createPromotion }) => {
-                switch (createPromotion.__typename) {
-                    case 'Promotion':
-                        this.notificationService.success(_('common.notify-create-success'), {
-                            entity: 'Promotion',
-                        });
-                        this.detailForm.markAsPristine();
-                        this.changeDetector.markForCheck();
-                        this.router.navigate(['../', createPromotion.id], { relativeTo: this.route });
-                        break;
-                    case 'MissingConditionsError':
-                        this.notificationService.error(createPromotion.message);
-                        break;
-                }
-            },
-            err => {
-                this.notificationService.error(_('common.notify-create-error'), {
-                    entity: 'Promotion',
-                });
-            },
-        );
+        combineLatest(this.entity$, this.languageCode$)
+            .pipe(
+                take(1),
+                mergeMap(([promotion, languageCode]) => {
+                    const input = this.getUpdatedPromotion(
+                        promotion,
+                        this.detailForm,
+                        languageCode,
+                    ) as CreatePromotionInput;
+                    return this.dataService.promotion.createPromotion(input);
+                }),
+            )
+            .subscribe(
+                ({ createPromotion }) => {
+                    switch (createPromotion.__typename) {
+                        case 'Promotion':
+                            this.notificationService.success(_('common.notify-create-success'), {
+                                entity: 'Promotion',
+                            });
+                            this.detailForm.markAsPristine();
+                            this.changeDetector.markForCheck();
+                            this.router.navigate(['../', createPromotion.id], { relativeTo: this.route });
+                            break;
+                        case 'MissingConditionsError':
+                            this.notificationService.error(createPromotion.message);
+                            break;
+                    }
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-create-error'), {
+                        entity: 'Promotion',
+                    });
+                },
+            );
     }
 
     save() {
         if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.detailForm.value;
-        this.promotion$
+        combineLatest(this.entity$, this.languageCode$)
             .pipe(
                 take(1),
-                mergeMap(promotion => {
-                    const input: UpdatePromotionInput = {
-                        id: promotion.id,
-                        name: formValue.name,
-                        enabled: formValue.enabled,
-                        couponCode: formValue.couponCode,
-                        perCustomerUsageLimit: formValue.perCustomerUsageLimit,
-                        startsAt: formValue.startsAt,
-                        endsAt: formValue.endsAt,
-                        conditions: this.mapOperationsToInputs(this.conditions, formValue.conditions),
-                        actions: this.mapOperationsToInputs(this.actions, formValue.actions),
-                        customFields: formValue.customFields,
-                    };
+                mergeMap(([paymentMethod, languageCode]) => {
+                    const input = this.getUpdatedPromotion(
+                        paymentMethod,
+                        this.detailForm,
+                        languageCode,
+                    ) as UpdatePromotionInput;
                     return this.dataService.promotion.updatePromotion(input);
                 }),
             )
@@ -210,12 +209,43 @@ export class PromotionDetailComponent
             );
     }
 
+    /**
+     * Given a PaymentMethod and the value of the detailForm, this method creates an updated copy of it which
+     * can then be persisted to the API.
+     */
+    private getUpdatedPromotion(
+        promotion: PromotionFragment,
+        formGroup: FormGroup,
+        languageCode: LanguageCode,
+    ): UpdatePromotionInput | CreatePromotionInput {
+        const formValue = formGroup.value;
+        const input = createUpdatedTranslatable({
+            translatable: promotion,
+            updatedFields: formValue,
+            customFieldConfig: this.customFields,
+            languageCode,
+            defaultTranslation: {
+                languageCode,
+                name: promotion.name || '',
+                description: promotion.description || '',
+            },
+        });
+
+        return {
+            ...input,
+            conditions: this.mapOperationsToInputs(this.conditions, formValue.conditions),
+            actions: this.mapOperationsToInputs(this.actions, formValue.actions),
+        };
+    }
+
     /**
      * Update the form values when the entity changes.
      */
     protected setFormValues(entity: PromotionFragment, languageCode: LanguageCode): void {
+        const currentTranslation = findTranslation(entity, languageCode);
         this.detailForm.patchValue({
-            name: entity.name,
+            name: currentTranslation?.name,
+            description: currentTranslation?.description,
             enabled: entity.enabled,
             couponCode: entity.couponCode,
             perCustomerUsageLimit: entity.perCustomerUsageLimit,

+ 3 - 0
packages/admin-ui/src/lib/marketing/src/providers/routing/promotion-resolver.ts

@@ -18,9 +18,12 @@ export class PromotionResolver extends BaseEntityResolver<PromotionFragment> {
                 createdAt: '',
                 updatedAt: '',
                 name: '',
+                description: '',
+                couponCode: '',
                 enabled: false,
                 conditions: [],
                 actions: [],
+                translations: [],
             },
             id => dataService.promotion.getPromotion(id).mapStream(data => data.promotion),
         );

+ 1 - 1
packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.ts

@@ -219,7 +219,7 @@ export class PaymentMethodDetailComponent
     }
 
     /**
-     * Given a facet and the value of the detailForm, this method creates an updated copy of the facet which
+     * Given a PaymentMethod and the value of the detailForm, this method creates an updated copy of it which
      * can then be persisted to the API.
      */
     private getUpdatedPaymentMethod(