Browse Source

feat(admin-ui): Implement CanDeactivate guard for detail components

Closes #56
Michael Bromley 7 years ago
parent
commit
0181b3b984
39 changed files with 276 additions and 186 deletions
  1. 4 0
      admin-ui/src/app/catalog/catalog.routes.ts
  2. 5 5
      admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.html
  3. 20 20
      admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.ts
  4. 4 4
      admin-ui/src/app/catalog/components/product-category-detail/product-category-detail.component.html
  5. 19 19
      admin-ui/src/app/catalog/components/product-category-detail/product-category-detail.component.ts
  6. 5 7
      admin-ui/src/app/catalog/components/product-detail/product-detail.component.html
  7. 14 14
      admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts
  8. 2 0
      admin-ui/src/app/common/base-detail.component.ts
  9. 4 4
      admin-ui/src/app/customer/components/customer-detail/customer-detail.component.html
  10. 9 9
      admin-ui/src/app/customer/components/customer-detail/customer-detail.component.ts
  11. 2 0
      admin-ui/src/app/customer/customer.routes.ts
  12. 1 1
      admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.html
  13. 16 16
      admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.ts
  14. 2 0
      admin-ui/src/app/marketing/marketing.routes.ts
  15. 2 0
      admin-ui/src/app/order/components/order-detail/order-detail.component.ts
  16. 2 0
      admin-ui/src/app/order/order.routes.ts
  17. 3 3
      admin-ui/src/app/settings/components/admin-detail/admin-detail.component.html
  18. 9 9
      admin-ui/src/app/settings/components/admin-detail/admin-detail.component.ts
  19. 1 1
      admin-ui/src/app/settings/components/channel-detail/channel-detail.component.html
  20. 10 10
      admin-ui/src/app/settings/components/channel-detail/channel-detail.component.ts
  21. 3 3
      admin-ui/src/app/settings/components/country-detail/country-detail.component.html
  22. 8 8
      admin-ui/src/app/settings/components/country-detail/country-detail.component.ts
  23. 3 3
      admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.html
  24. 6 6
      admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.ts
  25. 3 3
      admin-ui/src/app/settings/components/role-detail/role-detail.component.html
  26. 8 8
      admin-ui/src/app/settings/components/role-detail/role-detail.component.ts
  27. 3 3
      admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.html
  28. 7 7
      admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.ts
  29. 1 1
      admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.html
  30. 10 10
      admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.ts
  31. 1 1
      admin-ui/src/app/settings/components/tax-rate-detail/tax-rate-detail.component.html
  32. 10 10
      admin-ui/src/app/settings/components/tax-rate-detail/tax-rate-detail.component.ts
  33. 9 0
      admin-ui/src/app/settings/settings.routes.ts
  34. 12 0
      admin-ui/src/app/shared/components/confirm-navigation-dialog/confirm-navigation-dialog.component.html
  35. 0 0
      admin-ui/src/app/shared/components/confirm-navigation-dialog/confirm-navigation-dialog.component.scss
  36. 21 0
      admin-ui/src/app/shared/components/confirm-navigation-dialog/confirm-navigation-dialog.component.ts
  37. 28 0
      admin-ui/src/app/shared/providers/routing/can-deactivate-detail-guard.ts
  38. 5 1
      admin-ui/src/app/shared/shared.module.ts
  39. 4 0
      admin-ui/src/i18n-messages/en.json

+ 4 - 0
admin-ui/src/app/catalog/catalog.routes.ts

@@ -4,6 +4,7 @@ import { FacetWithValues, ProductWithVariants } from 'shared/generated-types';
 import { createResolveData } from '../common/base-entity-resolver';
 import { detailBreadcrumb } from '../common/detail-breadcrumb';
 import { _ } from '../core/providers/i18n/mark-for-extraction';
+import { CanDeactivateDetailGuard } from '../shared/providers/routing/can-deactivate-detail-guard';
 
 import { AssetListComponent } from './components/asset-list/asset-list.component';
 import { FacetDetailComponent } from './components/facet-detail/facet-detail.component';
@@ -28,6 +29,7 @@ export const catalogRoutes: Route[] = [
         path: 'products/:id',
         component: ProductDetailComponent,
         resolve: createResolveData(ProductResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: productBreadcrumb,
         },
@@ -43,6 +45,7 @@ export const catalogRoutes: Route[] = [
         path: 'facets/:id',
         component: FacetDetailComponent,
         resolve: createResolveData(FacetResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: facetBreadcrumb,
         },
@@ -58,6 +61,7 @@ export const catalogRoutes: Route[] = [
         path: 'categories/:id',
         component: ProductCategoryDetailComponent,
         resolve: createResolveData(ProductCategoryResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: productCategoryBreadcrumb,
         },

+ 5 - 5
admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.html

@@ -12,7 +12,7 @@
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
             (click)="create()"
-            [disabled]="facetForm.invalid || facetForm.pristine"
+            [disabled]="detailForm.invalid || detailForm.pristine"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -20,7 +20,7 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="facetForm.invalid || facetForm.pristine"
+                [disabled]="detailForm.invalid || detailForm.pristine"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -28,7 +28,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="facetForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block" formGroupName="facet">
         <label>{{ 'catalog.facet' | translate }}</label>
         <vdr-form-field [label]="'common.name' | translate" for="name">
@@ -43,7 +43,7 @@
             <ng-container *ngFor="let customField of customFields">
                 <vdr-custom-field-control
                     *ngIf="customFieldIsSet(customField.name)"
-                    [customFieldsFormGroup]="facetForm.get(['facet', 'customFields'])"
+                    [customFieldsFormGroup]="detailForm.get(['facet', 'customFields'])"
                     [customField]="customField"
                 ></vdr-custom-field-control>
             </ng-container>
@@ -82,7 +82,7 @@
                             <vdr-custom-field-control
                                 *ngIf="customValueFieldIsSet(i, customField.name)"
                                 [showLabel]="false"
-                                [customFieldsFormGroup]="facetForm.get(['values', i, 'customFields'])"
+                                [customFieldsFormGroup]="detailForm.get(['values', i, 'customFields'])"
                                 [customField]="customField"
                             ></vdr-custom-field-control>
                         </td>

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

@@ -32,7 +32,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
     values$: Observable<FacetWithValues.Values[]>;
     customFields: CustomFieldConfig[];
     customValueFields: CustomFieldConfig[];
-    facetForm: FormGroup;
+    detailForm: FormGroup;
 
     constructor(
         router: Router,
@@ -46,7 +46,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
         super(route, router, serverConfigService);
         this.customFields = this.getCustomFieldConfig('Facet');
         this.customValueFields = this.getCustomFieldConfig('FacetValue');
-        this.facetForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             facet: this.formBuilder.group({
                 code: ['', Validators.required],
                 name: '',
@@ -69,40 +69,40 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
     }
 
     updateCode(nameValue: string) {
-        const codeControl = this.facetForm.get(['facet', 'code']);
+        const codeControl = this.detailForm.get(['facet', 'code']);
         if (codeControl && codeControl.pristine) {
             codeControl.setValue(normalizeString(nameValue, '-'));
         }
     }
 
     updateValueCode(nameValue: string, index: number) {
-        const codeControl = this.facetForm.get(['values', index, 'code']);
+        const codeControl = this.detailForm.get(['values', index, 'code']);
         if (codeControl && codeControl.pristine) {
             codeControl.setValue(normalizeString(nameValue, '-'));
         }
     }
 
     customFieldIsSet(name: string): boolean {
-        return !!this.facetForm.get(['facet', 'customFields', name]);
+        return !!this.detailForm.get(['facet', 'customFields', name]);
     }
 
     customValueFieldIsSet(index: number, name: string): boolean {
-        return !!this.facetForm.get(['values', index, 'customFields', name]);
+        return !!this.detailForm.get(['values', index, 'customFields', name]);
     }
 
     getValuesFormArray(): FormArray {
-        return this.facetForm.get('values') as FormArray;
+        return this.detailForm.get('values') as FormArray;
     }
 
     addFacetValue() {
-        const valuesFormArray = this.facetForm.get('values') as FormArray | null;
+        const valuesFormArray = this.detailForm.get('values') as FormArray | null;
         if (valuesFormArray) {
             valuesFormArray.insert(valuesFormArray.length, this.formBuilder.group({ name: '', code: '' }));
         }
     }
 
     create() {
-        const facetForm = this.facetForm.get('facet');
+        const facetForm = this.detailForm.get('facet');
         if (!facetForm || !facetForm.dirty) {
             return;
         }
@@ -117,7 +117,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
             .subscribe(
                 data => {
                     this.notificationService.success(_('common.notify-create-success'), { entity: 'Facet' });
-                    this.facetForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                     this.router.navigate(['../', data.createFacet.id], { relativeTo: this.route });
                 },
@@ -134,7 +134,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
             .pipe(
                 take(1),
                 mergeMap(([facet, languageCode]) => {
-                    const facetGroup = this.facetForm.get('facet');
+                    const facetGroup = this.detailForm.get('facet');
                     const updateOperations: Array<Observable<any>> = [];
 
                     if (facetGroup && facetGroup.dirty) {
@@ -143,7 +143,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
                             updateOperations.push(this.dataService.facet.updateFacet(newFacet));
                         }
                     }
-                    const valuesArray = this.facetForm.get('values');
+                    const valuesArray = this.detailForm.get('values');
                     if (valuesArray && valuesArray.dirty) {
                         const newValues: CreateFacetValueInput[] = (valuesArray as FormArray).controls
                             .filter(c => !c.value.id)
@@ -170,7 +170,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
             )
             .subscribe(
                 () => {
-                    this.facetForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                     this.notificationService.success(_('common.notify-update-success'), { entity: 'Facet' });
                 },
@@ -188,7 +188,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
     protected setFormValues(facet: FacetWithValues.Fragment, languageCode: LanguageCode) {
         const currentTranslation = facet.translations.find(t => t.languageCode === languageCode);
         if (currentTranslation) {
-            this.facetForm.patchValue({
+            this.detailForm.patchValue({
                 facet: {
                     code: facet.code,
                     name: currentTranslation.name,
@@ -196,7 +196,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
             });
 
             if (this.customFields.length) {
-                const customFieldsGroup = this.facetForm.get(['facet', 'customFields']) as FormGroup;
+                const customFieldsGroup = this.detailForm.get(['facet', 'customFields']) as FormGroup;
 
                 for (const fieldDef of this.customFields) {
                     const key = fieldDef.name;
@@ -211,7 +211,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
                 }
             }
 
-            const valuesFormArray = this.facetForm.get('values') as FormArray;
+            const valuesFormArray = this.detailForm.get('values') as FormArray;
             facet.values.forEach((value, i) => {
                 const valueTranslation = value.translations.find(t => t.languageCode === languageCode);
                 const group = {
@@ -226,14 +226,14 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
                     valuesFormArray.insert(i, this.formBuilder.group(group));
                 }
                 if (this.customValueFields.length) {
-                    let customValueFieldsGroup = this.facetForm.get([
+                    let customValueFieldsGroup = this.detailForm.get([
                         'values',
                         i,
                         'customFields',
                     ]) as FormGroup;
                     if (!customValueFieldsGroup) {
                         customValueFieldsGroup = new FormGroup({});
-                        (this.facetForm.get(['values', i]) as FormGroup).addControl(
+                        (this.detailForm.get(['values', i]) as FormGroup).addControl(
                             'customFields',
                             customValueFieldsGroup,
                         );
@@ -260,7 +260,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
     }
 
     /**
-     * Given a facet and the value of the facetForm, this method creates an updated copy of the facet which
+     * Given a facet and the value of the detailForm, this method creates an updated copy of the facet which
      * can then be persisted to the API.
      */
     private getUpdatedFacet(
@@ -281,7 +281,7 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues.Fr
     }
 
     /**
-     * Given an array of facet values and the values from the facetForm, this method creates an new array
+     * Given an array of facet values and the values from the detailForm, this method creates an new array
      * which can be persisted to the API.
      */
     private getUpdatedFacetValues(

+ 4 - 4
admin-ui/src/app/catalog/components/product-category-detail/product-category-detail.component.html

@@ -12,7 +12,7 @@
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
             (click)="create()"
-            [disabled]="categoryForm.invalid || categoryForm.pristine"
+            [disabled]="detailForm.invalid || detailForm.pristine"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -20,7 +20,7 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="(categoryForm.invalid || categoryForm.pristine) && !assetsChanged()"
+                [disabled]="(detailForm.invalid || detailForm.pristine) && !assetsChanged()"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -28,7 +28,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="categoryForm" *ngIf="(entity$ | async) as category">
+<form class="form" [formGroup]="detailForm" *ngIf="(entity$ | async) as category">
     <div class="clr-row">
         <div class="clr-col">
             <section class="form-block">
@@ -45,7 +45,7 @@
                     <ng-container *ngFor="let customField of customFields">
                         <vdr-custom-field-control
                             *ngIf="customFieldIsSet(customField.name)"
-                            [customFieldsFormGroup]="categoryForm.get(['customFields'])"
+                            [customFieldsFormGroup]="detailForm.get(['customFields'])"
                             [customField]="customField"
                         ></vdr-custom-field-control>
                     </ng-container>

+ 19 - 19
admin-ui/src/app/catalog/components/product-category-detail/product-category-detail.component.ts

@@ -42,7 +42,7 @@ import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dia
 export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductCategory.Fragment>
     implements OnInit, OnDestroy {
     customFields: CustomFieldConfig[];
-    categoryForm: FormGroup;
+    detailForm: FormGroup;
     assetChanges: { assetIds?: string[]; featuredAssetId?: string } = {};
     facetValues$: Observable<FacetValue.Fragment[]>;
     private facets$: Observable<FacetWithValues.Fragment[]>;
@@ -59,7 +59,7 @@ export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductC
     ) {
         super(route, router, serverConfigService);
         this.customFields = this.getCustomFieldConfig('ProductCategory');
-        this.categoryForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             name: ['', Validators.required],
             description: '',
             facetValueIds: [[]],
@@ -80,8 +80,8 @@ export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductC
         const facetValueIds$ = this.entity$.pipe(
             filter(category => !!(category && category.facetValues)),
             take(1),
-            switchMap(category => this.categoryForm.valueChanges),
-            startWith(this.categoryForm.value),
+            switchMap(category => this.detailForm.valueChanges),
+            startWith(this.detailForm.value),
             map(formValue => formValue.facetValueIds),
         );
 
@@ -95,7 +95,7 @@ export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductC
     }
 
     customFieldIsSet(name: string): boolean {
-        return !!this.categoryForm.get(['customFields', name]);
+        return !!this.detailForm.get(['customFields', name]);
     }
 
     assetsChanged(): boolean {
@@ -117,33 +117,33 @@ export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductC
             )
             .subscribe(([facetValueIds, category]) => {
                 if (facetValueIds) {
-                    const existingFacetValueIds = this.categoryForm.value.facetValueIds;
-                    this.categoryForm.patchValue({
+                    const existingFacetValueIds = this.detailForm.value.facetValueIds;
+                    this.detailForm.patchValue({
                         facetValueIds: unique([...existingFacetValueIds, ...facetValueIds]),
                     });
-                    this.categoryForm.markAsDirty();
+                    this.detailForm.markAsDirty();
                     this.changeDetector.markForCheck();
                 }
             });
     }
 
     removeValueFacet(id: string) {
-        const facetValueIds = this.categoryForm.value.facetValueIds.filter(fvid => fvid !== id);
-        this.categoryForm.patchValue({
+        const facetValueIds = this.detailForm.value.facetValueIds.filter(fvid => fvid !== id);
+        this.detailForm.patchValue({
             facetValueIds,
         });
-        this.categoryForm.markAsDirty();
+        this.detailForm.markAsDirty();
     }
 
     create() {
-        if (!this.categoryForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
         combineLatest(this.entity$, this.languageCode$)
             .pipe(
                 take(1),
                 mergeMap(([category, languageCode]) => {
-                    const input = this.getUpdatedCategory(category, this.categoryForm, languageCode);
+                    const input = this.getUpdatedCategory(category, this.detailForm, languageCode);
                     return this.dataService.product.createProductCategory(input);
                 }),
             )
@@ -152,7 +152,7 @@ export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductC
                     this.notificationService.success(_('common.notify-create-success'), {
                         entity: 'ProductCategory',
                     });
-                    this.categoryForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                     this.router.navigate(['../', data.createProductCategory.id], { relativeTo: this.route });
                 },
@@ -172,7 +172,7 @@ export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductC
                     const updateOperations: Array<Observable<any>> = [];
                     const input = this.getUpdatedCategory(
                         category,
-                        this.categoryForm,
+                        this.detailForm,
                         languageCode,
                     ) as UpdateProductCategoryInput;
                     return this.dataService.product.updateProductCategory(input);
@@ -180,7 +180,7 @@ export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductC
             )
             .subscribe(
                 () => {
-                    this.categoryForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'ProductCategory',
@@ -200,14 +200,14 @@ export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductC
     protected setFormValues(category: ProductCategory.Fragment, languageCode: LanguageCode) {
         const currentTranslation = category.translations.find(t => t.languageCode === languageCode);
         if (currentTranslation) {
-            this.categoryForm.patchValue({
+            this.detailForm.patchValue({
                 name: currentTranslation.name,
                 description: currentTranslation.description,
                 facetValueIds: category.facetValues.map(fv => fv.id),
             });
 
             if (this.customFields.length) {
-                const customFieldsGroup = this.categoryForm.get(['customFields']) as FormGroup;
+                const customFieldsGroup = this.detailForm.get(['customFields']) as FormGroup;
 
                 for (const fieldDef of this.customFields) {
                     const key = fieldDef.name;
@@ -247,7 +247,7 @@ export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductC
         return {
             ...updatedCategory,
             ...this.assetChanges,
-            facetValueIds: this.categoryForm.value.facetValueIds,
+            facetValueIds: this.detailForm.value.facetValueIds,
         };
     }
 }

+ 5 - 7
admin-ui/src/app/catalog/components/product-detail/product-detail.component.html

@@ -12,7 +12,7 @@
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
             (click)="create()"
-            [disabled]="productForm.invalid || productForm.pristine"
+            [disabled]="detailForm.invalid || detailForm.pristine"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -21,9 +21,7 @@
                 class="btn btn-primary"
                 (click)="save()"
                 [disabled]="
-                    (productForm.invalid || productForm.pristine) &&
-                    !assetsChanged() &&
-                    !variantAssetsChanged()
+                    (detailForm.invalid || detailForm.pristine) && !assetsChanged() && !variantAssetsChanged()
                 "
             >
                 {{ 'common.update' | translate }}
@@ -32,7 +30,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="productForm" *ngIf="(product$ | async) as product">
+<form class="form" [formGroup]="detailForm" *ngIf="(product$ | async) as product">
     <clr-tabs>
         <clr-tab>
             <button clrTabLink (click)="navigateToTab('details')">
@@ -63,7 +61,7 @@
                                 <ng-container *ngFor="let customField of customFields">
                                     <vdr-custom-field-control
                                         *ngIf="customFieldIsSet(customField.name)"
-                                        [customFieldsFormGroup]="productForm.get(['product', 'customFields'])"
+                                        [customFieldsFormGroup]="detailForm.get(['product', 'customFields'])"
                                         [customField]="customField"
                                     ></vdr-custom-field-control>
                                 </ng-container>
@@ -107,7 +105,7 @@
                         <vdr-product-variants-list
                             [variants]="variants$ | async"
                             [facets]="facets$ | async"
-                            [productVariantsFormArray]="productForm.get('variants')"
+                            [productVariantsFormArray]="detailForm.get('variants')"
                             [taxCategories]="taxCategories$ | async"
                             (assetChange)="variantAssetChange($event)"
                             (selectionChange)="selectedVariantIds = $event"

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

@@ -59,7 +59,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     taxCategories$: Observable<TaxCategory.Fragment[]>;
     customFields: CustomFieldConfig[];
     customVariantFields: CustomFieldConfig[];
-    productForm: FormGroup;
+    detailForm: FormGroup;
     assetChanges: SelectedAssets = {};
     variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
     facetValues$: Observable<ProductWithVariants.FacetValues[]>;
@@ -80,7 +80,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         super(route, router, serverConfigService);
         this.customFields = this.getCustomFieldConfig('Product');
         this.customVariantFields = this.getCustomFieldConfig('ProductVariant');
-        this.productForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             product: this.formBuilder.group({
                 name: ['', Validators.required],
                 slug: '',
@@ -136,7 +136,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     }
 
     customFieldIsSet(name: string): boolean {
-        return !!this.productForm.get(['product', 'customFields', name]);
+        return !!this.detailForm.get(['product', 'customFields', name]);
     }
 
     assetsChanged(): boolean {
@@ -157,7 +157,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     updateSlug(nameValue: string) {
         this.isNew$.pipe(take(1)).subscribe(isNew => {
             if (isNew) {
-                const slugControl = this.productForm.get(['product', 'slug']);
+                const slugControl = this.detailForm.get(['product', 'slug']);
                 if (slugControl && slugControl.pristine) {
                     slugControl.setValue(normalizeString(`${nameValue}`, '-'));
                 }
@@ -198,7 +198,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                         const index = variants.findIndex(v => v.id === variantId);
                         const variant = variants[index];
                         const existingFacetValueIds = variant ? variant.facetValues.map(fv => fv.id) : [];
-                        const variantFormGroup = this.productForm.get(['variants', index]);
+                        const variantFormGroup = this.detailForm.get(['variants', index]);
                         if (variantFormGroup) {
                             variantFormGroup.patchValue({
                                 facetValueIds: unique([...existingFacetValueIds, ...facetValueIds]),
@@ -258,7 +258,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                     });
                     this.assetChanges = {};
                     this.variantAssetChanges = {};
-                    this.productForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.router.navigate(['../', data.createProduct.id], { relativeTo: this.route });
                 },
                 err => {
@@ -287,7 +287,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                             updateOperations.push(this.dataService.product.updateProduct(newProduct));
                         }
                     }
-                    const variantsArray = this.productForm.get('variants');
+                    const variantsArray = this.detailForm.get('variants');
                     if ((variantsArray && variantsArray.dirty) || this.variantAssetsChanged()) {
                         const newVariants = this.getUpdatedProductVariants(
                             product,
@@ -302,7 +302,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
             )
             .subscribe(
                 () => {
-                    this.productForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.assetChanges = {};
                     this.variantAssetChanges = {};
                     this.notificationService.success(_('common.notify-update-success'), {
@@ -323,7 +323,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     protected setFormValues(product: ProductWithVariants.Fragment, languageCode: LanguageCode) {
         const currentTranslation = product.translations.find(t => t.languageCode === languageCode);
         if (currentTranslation) {
-            this.productForm.patchValue({
+            this.detailForm.patchValue({
                 product: {
                     name: currentTranslation.name,
                     slug: currentTranslation.slug,
@@ -333,7 +333,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
             });
 
             if (this.customFields.length) {
-                const customFieldsGroup = this.productForm.get(['product', 'customFields']) as FormGroup;
+                const customFieldsGroup = this.detailForm.get(['product', 'customFields']) as FormGroup;
 
                 for (const fieldDef of this.customFields) {
                     const key = fieldDef.name;
@@ -348,7 +348,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                 }
             }
 
-            const variantsFormArray = this.productForm.get('variants') as FormArray;
+            const variantsFormArray = this.detailForm.get('variants') as FormArray;
             product.variants.forEach((variant, i) => {
                 const variantTranslation = variant.translations.find(t => t.languageCode === languageCode);
                 const facetValueIds = variant.facetValues.map(fv => fv.id);
@@ -379,7 +379,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     }
 
     /**
-     * Given a product and the value of the productForm, this method creates an updated copy of the product which
+     * Given a product and the value of the detailForm, this method creates an updated copy of the product which
      * can then be persisted to the API.
      */
     private getUpdatedProduct(
@@ -407,7 +407,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     }
 
     /**
-     * Given an array of product variants and the values from the productForm, this method creates an new array
+     * Given an array of product variants and the values from the detailForm, this method creates an new array
      * which can be persisted to the API.
      */
     private getUpdatedProductVariants(
@@ -446,6 +446,6 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     }
 
     private getProductFormGroup(): FormGroup {
-        return this.productForm.get('product') as FormGroup;
+        return this.detailForm.get('product') as FormGroup;
     }
 }

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

@@ -1,3 +1,4 @@
+import { FormGroup } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { combineLatest, Observable, of, Subject } from 'rxjs';
 import { map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
@@ -13,6 +14,7 @@ export abstract class BaseDetailComponent<Entity extends { id: string }> {
     availableLanguages$: Observable<LanguageCode[]>;
     languageCode$: Observable<LanguageCode>;
     isNew$: Observable<boolean>;
+    abstract detailForm: FormGroup;
     protected destroy$ = new Subject<void>();
 
     protected constructor(

+ 4 - 4
admin-ui/src/app/customer/components/customer-detail/customer-detail.component.html

@@ -6,7 +6,7 @@
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
             (click)="create()"
-            [disabled]="customerForm.invalid || customerForm.pristine"
+            [disabled]="detailForm.invalid || detailForm.pristine"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -14,7 +14,7 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="customerForm.invalid || customerForm.pristine"
+                [disabled]="detailForm.invalid || detailForm.pristine"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -22,7 +22,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="customerForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <vdr-form-field
             [label]="'customer.title' | translate"
@@ -61,7 +61,7 @@
             <ng-container *ngFor="let customField of customFields">
                 <vdr-custom-field-control
                     *ngIf="customFieldIsSet(customField.name)"
-                    [customFieldsFormGroup]="customerForm.get(['facet', 'customFields'])"
+                    [customFieldsFormGroup]="detailForm.get(['facet', 'customFields'])"
                     [customField]="customField"
                 ></vdr-custom-field-control>
             </ng-container>

+ 9 - 9
admin-ui/src/app/customer/components/customer-detail/customer-detail.component.ts

@@ -25,7 +25,7 @@ import { ServerConfigService } from '../../../data/server-config';
 })
 export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragment>
     implements OnInit, OnDestroy {
-    customerForm: FormGroup;
+    detailForm: FormGroup;
     customFields: CustomFieldConfig[];
 
     constructor(
@@ -40,7 +40,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
         super(route, router, serverConfigService);
 
         this.customFields = this.getCustomFieldConfig('Customer');
-        this.customerForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             title: '',
             firstName: ['', Validators.required],
             lastName: ['', Validators.required],
@@ -62,11 +62,11 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
     }
 
     customFieldIsSet(name: string): boolean {
-        return !!this.customerForm.get(['customFields', name]);
+        return !!this.detailForm.get(['customFields', name]);
     }
 
     create() {
-        const formValue = this.customerForm.value;
+        const formValue = this.detailForm.value;
         const customer: CreateCustomerInput = {
             title: formValue.title,
             emailAddress: formValue.emailAddress,
@@ -78,7 +78,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
                 this.notificationService.success(_('common.notify-create-success'), {
                     entity: 'Customer',
                 });
-                this.customerForm.markAsPristine();
+                this.detailForm.markAsPristine();
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createCustomer.id], { relativeTo: this.route });
             },
@@ -95,7 +95,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
             .pipe(
                 take(1),
                 mergeMap(({ id }) => {
-                    const formValue = this.customerForm.value;
+                    const formValue = this.detailForm.value;
                     const customer: UpdateCustomerInput = {
                         id,
                         title: formValue.title,
@@ -111,7 +111,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'Customer',
                     });
-                    this.customerForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -123,7 +123,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
     }
 
     protected setFormValues(entity: Customer.Fragment): void {
-        this.customerForm.patchValue({
+        this.detailForm.patchValue({
             title: entity.title,
             firstName: entity.firstName,
             lastName: entity.lastName,
@@ -132,7 +132,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
         });
 
         if (this.customFields.length) {
-            const customFieldsGroup = this.customerForm.get(['customFields']) as FormGroup;
+            const customFieldsGroup = this.detailForm.get(['customFields']) as FormGroup;
 
             for (const fieldDef of this.customFields) {
                 const key = fieldDef.name;

+ 2 - 0
admin-ui/src/app/customer/customer.routes.ts

@@ -4,6 +4,7 @@ import { Customer } from 'shared/generated-types';
 import { createResolveData } from '../common/base-entity-resolver';
 import { detailBreadcrumb } from '../common/detail-breadcrumb';
 import { _ } from '../core/providers/i18n/mark-for-extraction';
+import { CanDeactivateDetailGuard } from '../shared/providers/routing/can-deactivate-detail-guard';
 
 import { CustomerDetailComponent } from './components/customer-detail/customer-detail.component';
 import { CustomerListComponent } from './components/customer-list/customer-list.component';
@@ -22,6 +23,7 @@ export const customerRoutes: Route[] = [
         path: 'customers/:id',
         component: CustomerDetailComponent,
         resolve: createResolveData(CustomerResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: customerBreadcrumb,
         },

+ 1 - 1
admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.html

@@ -18,7 +18,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="promotionForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <vdr-form-field [label]="'common.name' | translate" for="name">
             <input id="name" type="text" formControlName="name" />

+ 16 - 16
admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.ts

@@ -27,7 +27,7 @@ import { ServerConfigService } from '../../../data/server-config';
 export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Fragment>
     implements OnInit, OnDestroy {
     promotion$: Observable<Promotion.Fragment>;
-    promotionForm: FormGroup;
+    detailForm: FormGroup;
     conditions: AdjustmentOperation[] = [];
     actions: AdjustmentOperation[] = [];
 
@@ -44,7 +44,7 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService);
-        this.promotionForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             name: ['', Validators.required],
             conditions: this.formBuilder.array([]),
             actions: this.formBuilder.array([]),
@@ -74,8 +74,8 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
 
     saveButtonEnabled(): boolean {
         return (
-            this.promotionForm.dirty &&
-            this.promotionForm.valid &&
+            this.detailForm.dirty &&
+            this.detailForm.valid &&
             this.conditions.length !== 0 &&
             this.actions.length !== 0
         );
@@ -83,33 +83,33 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
 
     addCondition(condition: AdjustmentOperation) {
         this.addOperation('conditions', condition);
-        this.promotionForm.markAsDirty();
+        this.detailForm.markAsDirty();
     }
 
     addAction(action: AdjustmentOperation) {
         this.addOperation('actions', action);
-        this.promotionForm.markAsDirty();
+        this.detailForm.markAsDirty();
     }
 
     removeCondition(condition: AdjustmentOperation) {
         this.removeOperation('conditions', condition);
-        this.promotionForm.markAsDirty();
+        this.detailForm.markAsDirty();
     }
 
     removeAction(action: AdjustmentOperation) {
         this.removeOperation('actions', action);
-        this.promotionForm.markAsDirty();
+        this.detailForm.markAsDirty();
     }
 
     formArrayOf(key: 'conditions' | 'actions'): FormArray {
-        return this.promotionForm.get(key) as FormArray;
+        return this.detailForm.get(key) as FormArray;
     }
 
     create() {
-        if (!this.promotionForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.promotionForm.value;
+        const formValue = this.detailForm.value;
         const input: CreatePromotionInput = {
             name: formValue.name,
             enabled: true,
@@ -119,7 +119,7 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
         this.dataService.promotion.createPromotion(input).subscribe(
             data => {
                 this.notificationService.success(_('common.notify-create-success'), { entity: 'Promotion' });
-                this.promotionForm.markAsPristine();
+                this.detailForm.markAsPristine();
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createPromotion.id], { relativeTo: this.route });
             },
@@ -132,10 +132,10 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
     }
 
     save() {
-        if (!this.promotionForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.promotionForm.value;
+        const formValue = this.detailForm.value;
         this.promotion$
             .pipe(
                 take(1),
@@ -154,7 +154,7 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'Promotion',
                     });
-                    this.promotionForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -169,7 +169,7 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
      * Update the form values when the entity changes.
      */
     protected setFormValues(entity: Promotion.Fragment, languageCode: LanguageCode): void {
-        this.promotionForm.patchValue({ name: entity.name });
+        this.detailForm.patchValue({ name: entity.name });
         entity.conditions.forEach(o => {
             this.addOperation('conditions', o);
         });

+ 2 - 0
admin-ui/src/app/marketing/marketing.routes.ts

@@ -4,6 +4,7 @@ import { Promotion } from 'shared/generated-types';
 import { createResolveData } from '../common/base-entity-resolver';
 import { detailBreadcrumb } from '../common/detail-breadcrumb';
 import { _ } from '../core/providers/i18n/mark-for-extraction';
+import { CanDeactivateDetailGuard } from '../shared/providers/routing/can-deactivate-detail-guard';
 
 import { PromotionDetailComponent } from './components/promotion-detail/promotion-detail.component';
 import { PromotionListComponent } from './components/promotion-list/promotion-list.component';
@@ -21,6 +22,7 @@ export const marketingRoutes: Route[] = [
         path: 'promotions/:id',
         component: PromotionDetailComponent,
         resolve: createResolveData(PromotionResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: promotionBreadcrumb,
         },

+ 2 - 0
admin-ui/src/app/order/components/order-detail/order-detail.component.ts

@@ -1,4 +1,5 @@
 import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormGroup } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { Order, OrderWithLines } from 'shared/generated-types';
 
@@ -13,6 +14,7 @@ import { ServerConfigService } from '../../../data/server-config';
 })
 export class OrderDetailComponent extends BaseDetailComponent<OrderWithLines.Fragment>
     implements OnInit, OnDestroy {
+    detailForm: FormGroup;
     constructor(router: Router, route: ActivatedRoute, serverConfigService: ServerConfigService) {
         super(route, router, serverConfigService);
     }

+ 2 - 0
admin-ui/src/app/order/order.routes.ts

@@ -4,6 +4,7 @@ import { OrderWithLines } from 'shared/generated-types';
 import { createResolveData } from '../common/base-entity-resolver';
 import { detailBreadcrumb } from '../common/detail-breadcrumb';
 import { _ } from '../core/providers/i18n/mark-for-extraction';
+import { CanDeactivateDetailGuard } from '../shared/providers/routing/can-deactivate-detail-guard';
 
 import { OrderDetailComponent } from './components/order-detail/order-detail.component';
 import { OrderListComponent } from './components/order-list/order-list.component';
@@ -21,6 +22,7 @@ export const orderRoutes: Route[] = [
         path: ':id',
         component: OrderDetailComponent,
         resolve: createResolveData(OrderResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: orderBreadcrumb,
         },

+ 3 - 3
admin-ui/src/app/settings/components/admin-detail/admin-detail.component.html

@@ -5,7 +5,7 @@
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
             (click)="create()"
-            [disabled]="administratorForm.invalid || administratorForm.pristine"
+            [disabled]="detailForm.invalid || detailForm.pristine"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -13,7 +13,7 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="(administratorForm.invalid || administratorForm.pristine) && !permissionsChanged"
+                [disabled]="(detailForm.invalid || detailForm.pristine) && !permissionsChanged"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -21,7 +21,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="administratorForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <label>{{ 'settings.administrator' | translate }}</label>
         <vdr-form-field [label]="'settings.email-address' | translate" for="emailAddress">

+ 9 - 9
admin-ui/src/app/settings/components/admin-detail/admin-detail.component.ts

@@ -28,7 +28,7 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
     administrator$: Observable<Administrator>;
     allRoles$: Observable<Role.Fragment[]>;
     selectedRoles: Role.Fragment[] = [];
-    administratorForm: FormGroup;
+    detailForm: FormGroup;
     selectedRolePermissions: { [K in Permission]: boolean } = {} as any;
 
     constructor(
@@ -41,7 +41,7 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService);
-        this.administratorForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             emailAddress: ['', Validators.required],
             firstName: ['', Validators.required],
             lastName: ['', Validators.required],
@@ -65,7 +65,7 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
     }
 
     create() {
-        const formValue = this.administratorForm.value;
+        const formValue = this.detailForm.value;
         const administrator: CreateAdministratorInput = {
             emailAddress: formValue.emailAddress,
             firstName: formValue.firstName,
@@ -78,7 +78,7 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
                 this.notificationService.success(_('common.notify-create-success'), {
                     entity: 'Administrator',
                 });
-                this.administratorForm.markAsPristine();
+                this.detailForm.markAsPristine();
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createAdministrator.id], { relativeTo: this.route });
             },
@@ -95,7 +95,7 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
             .pipe(
                 take(1),
                 mergeMap(({ id }) => {
-                    const formValue = this.administratorForm.value;
+                    const formValue = this.detailForm.value;
                     const administrator: UpdateAdministratorInput = {
                         id,
                         emailAddress: formValue.emailAddress,
@@ -112,7 +112,7 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'Administrator',
                     });
-                    this.administratorForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -124,13 +124,13 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
     }
 
     protected setFormValues(administrator: Administrator, languageCode: LanguageCode): void {
-        this.administratorForm.patchValue({
+        this.detailForm.patchValue({
             emailAddress: administrator.emailAddress,
             firstName: administrator.firstName,
             lastName: administrator.lastName,
             roles: administrator.user.roles,
         });
-        const passwordControl = this.administratorForm.get('password');
+        const passwordControl = this.detailForm.get('password');
         if (passwordControl) {
             if (!administrator.id) {
                 passwordControl.setValidators([Validators.required]);
@@ -142,7 +142,7 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
     }
 
     private buildPermissionsMap() {
-        const permissionsControl = this.administratorForm.get('roles');
+        const permissionsControl = this.detailForm.get('roles');
         if (permissionsControl) {
             const permissions = permissionsControl.value.reduce(
                 (output, role: Role) => [...output, ...role.permissions],

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

@@ -18,7 +18,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="channelForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <vdr-form-field [label]="'common.code' | translate" for="code">
             <input id="code" type="text" formControlName="code" />

+ 10 - 10
admin-ui/src/app/settings/components/channel-detail/channel-detail.component.ts

@@ -26,7 +26,7 @@ import { ServerConfigService } from '../../../data/server-config';
 export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment>
     implements OnInit, OnDestroy {
     zones$: Observable<GetZones.Zones[]>;
-    channelForm: FormGroup;
+    detailForm: FormGroup;
 
     constructor(
         router: Router,
@@ -38,7 +38,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService);
-        this.channelForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             code: ['', Validators.required],
             token: ['', Validators.required],
             pricesIncludeTax: [false],
@@ -57,14 +57,14 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
     }
 
     saveButtonEnabled(): boolean {
-        return this.channelForm.dirty && this.channelForm.valid;
+        return this.detailForm.dirty && this.detailForm.valid;
     }
 
     create() {
-        if (!this.channelForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.channelForm.value;
+        const formValue = this.detailForm.value;
         const input = {
             code: formValue.code,
             pricesIncludeTax: formValue.pricesIncludeTax,
@@ -76,7 +76,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
                 this.notificationService.success(_('common.notify-create-success'), {
                     entity: 'Channel',
                 });
-                this.channelForm.markAsPristine();
+                this.detailForm.markAsPristine();
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createChannel.id], { relativeTo: this.route });
             },
@@ -89,10 +89,10 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
     }
 
     save() {
-        if (!this.channelForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.channelForm.value;
+        const formValue = this.detailForm.value;
         this.entity$
             .pipe(
                 take(1),
@@ -112,7 +112,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'Channel',
                     });
-                    this.channelForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -127,7 +127,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
      * Update the form values when the entity changes.
      */
     protected setFormValues(entity: Channel.Fragment, languageCode: LanguageCode): void {
-        this.channelForm.patchValue({
+        this.detailForm.patchValue({
             code: entity.code,
             token: entity.token,
             pricesIncludeTax: entity.pricesIncludeTax,

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

@@ -11,7 +11,7 @@
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
             (click)="create()"
-            [disabled]="countryForm.invalid || countryForm.pristine"
+            [disabled]="detailForm.invalid || detailForm.pristine"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -19,7 +19,7 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="countryForm.invalid || countryForm.pristine"
+                [disabled]="detailForm.invalid || detailForm.pristine"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -27,7 +27,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="countryForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <vdr-form-field [label]="'common.code' | translate" for="code">
             <input id="code" type="text" formControlName="code" />

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

@@ -20,7 +20,7 @@ import { ServerConfigService } from '../../../data/server-config';
 export class CountryDetailComponent extends BaseDetailComponent<Country.Fragment>
     implements OnInit, OnDestroy {
     country$: Observable<Country.Fragment>;
-    countryForm: FormGroup;
+    detailForm: FormGroup;
 
     constructor(
         router: Router,
@@ -32,7 +32,7 @@ export class CountryDetailComponent extends BaseDetailComponent<Country.Fragment
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService);
-        this.countryForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             code: ['', Validators.required],
             name: ['', Validators.required],
             enabled: [true],
@@ -49,14 +49,14 @@ export class CountryDetailComponent extends BaseDetailComponent<Country.Fragment
     }
 
     create() {
-        if (!this.countryForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
         combineLatest(this.country$, this.languageCode$)
             .pipe(
                 take(1),
                 mergeMap(([country, languageCode]) => {
-                    const formValue = this.countryForm.value;
+                    const formValue = this.detailForm.value;
                     const input: CreateCountryInput = createUpdatedTranslatable({
                         translatable: country,
                         updatedFields: formValue,
@@ -74,7 +74,7 @@ export class CountryDetailComponent extends BaseDetailComponent<Country.Fragment
                     this.notificationService.success(_('common.notify-create-success'), {
                         entity: 'Country',
                     });
-                    this.countryForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                     this.router.navigate(['../', data.createCountry.id], { relativeTo: this.route });
                 },
@@ -91,7 +91,7 @@ export class CountryDetailComponent extends BaseDetailComponent<Country.Fragment
             .pipe(
                 take(1),
                 mergeMap(([country, languageCode]) => {
-                    const formValue = this.countryForm.value;
+                    const formValue = this.detailForm.value;
                     const input: UpdateCountryInput = createUpdatedTranslatable({
                         translatable: country,
                         updatedFields: formValue,
@@ -105,7 +105,7 @@ export class CountryDetailComponent extends BaseDetailComponent<Country.Fragment
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'Country',
                     });
-                    this.countryForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -119,7 +119,7 @@ export class CountryDetailComponent extends BaseDetailComponent<Country.Fragment
     protected setFormValues(country: Country, languageCode: LanguageCode): void {
         const currentTranslation = country.translations.find(t => t.languageCode === languageCode);
         if (currentTranslation) {
-            this.countryForm.patchValue({
+            this.detailForm.patchValue({
                 code: country.code,
                 name: currentTranslation.name,
                 enabled: country.enabled,

+ 3 - 3
admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.html

@@ -5,7 +5,7 @@
         <button
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
-            [disabled]="paymentMethodForm.pristine || paymentMethodForm.invalid"
+            [disabled]="detailForm.pristine || detailForm.invalid"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -13,7 +13,7 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="paymentMethodForm.pristine || paymentMethodForm.invalid"
+                [disabled]="detailForm.pristine || detailForm.invalid"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -21,7 +21,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="paymentMethodForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <vdr-form-field [label]="'common.code' | translate" for="code">
             <input id="code" type="text" formControlName="code" />

+ 6 - 6
admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.ts

@@ -24,7 +24,7 @@ import { ServerConfigService } from '../../../data/server-config';
 })
 export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMethod.Fragment>
     implements OnInit, OnDestroy {
-    paymentMethodForm: FormGroup;
+    detailForm: FormGroup;
 
     constructor(
         router: Router,
@@ -36,7 +36,7 @@ export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMet
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService);
-        this.paymentMethodForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             code: ['', Validators.required],
             enabled: [true, Validators.required],
             configArgs: this.formBuilder.group({}),
@@ -56,7 +56,7 @@ export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMet
             .pipe(
                 take(1),
                 mergeMap(({ id }) => {
-                    const formValue = this.paymentMethodForm.value;
+                    const formValue = this.detailForm.value;
                     const input: UpdatePaymentMethodInput = {
                         id,
                         code: formValue.code,
@@ -74,7 +74,7 @@ export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMet
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'PaymentMethod',
                     });
-                    this.paymentMethodForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -86,11 +86,11 @@ export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMet
     }
 
     protected setFormValues(paymentMethod: PaymentMethod.Fragment): void {
-        this.paymentMethodForm.patchValue({
+        this.detailForm.patchValue({
             code: paymentMethod.code,
             enabled: paymentMethod.enabled,
         });
-        const configArgsGroup = this.paymentMethodForm.get('configArgs') as FormGroup;
+        const configArgsGroup = this.detailForm.get('configArgs') as FormGroup;
         if (configArgsGroup) {
             for (const arg of paymentMethod.configArgs) {
                 const control = configArgsGroup.get(arg.name);

+ 3 - 3
admin-ui/src/app/settings/components/role-detail/role-detail.component.html

@@ -5,7 +5,7 @@
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
             (click)="create()"
-            [disabled]="roleForm.invalid || roleForm.pristine"
+            [disabled]="detailForm.invalid || detailForm.pristine"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -13,7 +13,7 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="(roleForm.invalid || roleForm.pristine) && !permissionsChanged"
+                [disabled]="(detailForm.invalid || detailForm.pristine) && !permissionsChanged"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -21,7 +21,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="roleForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <label>{{ 'settings.role' | translate }}</label>
         <vdr-form-field [label]="'common.description' | translate" for="description">

+ 8 - 8
admin-ui/src/app/settings/components/role-detail/role-detail.component.ts

@@ -20,7 +20,7 @@ import { ServerConfigService } from '../../../data/server-config';
 })
 export class RoleDetailComponent extends BaseDetailComponent<Role> implements OnInit, OnDestroy {
     role$: Observable<Role>;
-    roleForm: FormGroup;
+    detailForm: FormGroup;
     permissions: { [K in Permission]: boolean };
     permissionsChanged = false;
     constructor(
@@ -37,7 +37,7 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
             (result, key) => ({ ...result, [key]: false }),
             {} as { [K in Permission]: boolean },
         );
-        this.roleForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             code: ['', Validators.required],
             description: ['', Validators.required],
         });
@@ -53,7 +53,7 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
     }
 
     updateCode(nameValue: string) {
-        const codeControl = this.roleForm.get(['code']);
+        const codeControl = this.detailForm.get(['code']);
         if (codeControl && codeControl.pristine) {
             codeControl.setValue(normalizeString(nameValue, '-'));
         }
@@ -65,7 +65,7 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
     }
 
     create() {
-        const formValue = this.roleForm.value;
+        const formValue = this.detailForm.value;
         const role: CreateRoleInput = {
             code: formValue.code,
             description: formValue.description,
@@ -74,7 +74,7 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
         this.dataService.administrator.createRole(role).subscribe(
             data => {
                 this.notificationService.success(_('common.notify-create-success'), { entity: 'Role' });
-                this.roleForm.markAsPristine();
+                this.detailForm.markAsPristine();
                 this.changeDetector.markForCheck();
                 this.permissionsChanged = false;
                 this.router.navigate(['../', data.createRole.id], { relativeTo: this.route });
@@ -92,7 +92,7 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
             .pipe(
                 take(1),
                 mergeMap(({ id }) => {
-                    const formValue = this.roleForm.value;
+                    const formValue = this.detailForm.value;
                     const role: UpdateRoleInput = {
                         id,
                         code: formValue.code,
@@ -105,7 +105,7 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
             .subscribe(
                 data => {
                     this.notificationService.success(_('common.notify-update-success'), { entity: 'Role' });
-                    this.roleForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                     this.permissionsChanged = false;
                 },
@@ -118,7 +118,7 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
     }
 
     protected setFormValues(role: Role, languageCode: LanguageCode): void {
-        this.roleForm.patchValue({
+        this.detailForm.patchValue({
             description: role.description,
             code: role.code,
         });

+ 3 - 3
admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.html

@@ -6,7 +6,7 @@
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
             (click)="create()"
-            [disabled]="shippingMethodForm.pristine || shippingMethodForm.invalid"
+            [disabled]="detailForm.pristine || detailForm.invalid"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -14,7 +14,7 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="shippingMethodForm.pristine || shippingMethodForm.invalid"
+                [disabled]="detailForm.pristine || detailForm.invalid"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -22,7 +22,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="shippingMethodForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <vdr-form-field [label]="'common.code' | translate" for="code">
             <input id="code" type="text" formControlName="code" />

+ 7 - 7
admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.ts

@@ -25,7 +25,7 @@ import { ServerConfigService } from '../../../data/server-config';
 })
 export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingMethod.Fragment>
     implements OnInit, OnDestroy {
-    shippingMethodForm: FormGroup;
+    detailForm: FormGroup;
     checkers: AdjustmentOperation[] = [];
     calculators: AdjustmentOperation[] = [];
     selectedChecker?: AdjustmentOperation;
@@ -41,7 +41,7 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService);
-        this.shippingMethodForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             code: ['', Validators.required],
             description: ['', Validators.required],
             checker: {},
@@ -74,7 +74,7 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
         if (!this.selectedChecker || !this.selectedCalculator) {
             return;
         }
-        const formValue = this.shippingMethodForm.value;
+        const formValue = this.detailForm.value;
         const input: CreateShippingMethodInput = {
             code: formValue.code,
             description: formValue.description,
@@ -86,7 +86,7 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
                 this.notificationService.success(_('common.notify-create-success'), {
                     entity: 'ShippingMethod',
                 });
-                this.shippingMethodForm.markAsPristine();
+                this.detailForm.markAsPristine();
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createShippingMethod.id], { relativeTo: this.route });
             },
@@ -108,7 +108,7 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
             .pipe(
                 take(1),
                 mergeMap(({ id }) => {
-                    const formValue = this.shippingMethodForm.value;
+                    const formValue = this.detailForm.value;
                     const input: UpdateShippingMethodInput = {
                         id,
                         code: formValue.code,
@@ -124,7 +124,7 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'ShippingMethod',
                     });
-                    this.shippingMethodForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -152,7 +152,7 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
     }
 
     protected setFormValues(shippingMethod: ShippingMethod.Fragment): void {
-        this.shippingMethodForm.patchValue({
+        this.detailForm.patchValue({
             description: shippingMethod.description,
             code: shippingMethod.code,
             checker: shippingMethod.checker || {},

+ 1 - 1
admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.html

@@ -18,7 +18,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="taxCategoryForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <vdr-form-field [label]="'common.name' | translate" for="name">
             <input id="name" type="text" formControlName="name" />

+ 10 - 10
admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.ts

@@ -26,7 +26,7 @@ import { ServerConfigService } from '../../../data/server-config';
 export class TaxCategoryDetailComponent extends BaseDetailComponent<TaxCategory.Fragment>
     implements OnInit, OnDestroy {
     taxCategory$: Observable<TaxCategory.Fragment>;
-    taxCategoryForm: FormGroup;
+    detailForm: FormGroup;
 
     private taxCondition: AdjustmentOperation;
     private taxAction: AdjustmentOperation;
@@ -41,7 +41,7 @@ export class TaxCategoryDetailComponent extends BaseDetailComponent<TaxCategory.
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService);
-        this.taxCategoryForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             name: ['', Validators.required],
             taxRate: [0, Validators.required],
         });
@@ -57,21 +57,21 @@ export class TaxCategoryDetailComponent extends BaseDetailComponent<TaxCategory.
     }
 
     saveButtonEnabled(): boolean {
-        return this.taxCategoryForm.dirty && this.taxCategoryForm.valid;
+        return this.detailForm.dirty && this.detailForm.valid;
     }
 
     create() {
-        if (!this.taxCategoryForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.taxCategoryForm.value;
+        const formValue = this.detailForm.value;
         const input = { name: formValue.name } as CreateTaxCategoryInput;
         this.dataService.settings.createTaxCategory(input).subscribe(
             data => {
                 this.notificationService.success(_('common.notify-create-success'), {
                     entity: 'TaxCategory',
                 });
-                this.taxCategoryForm.markAsPristine();
+                this.detailForm.markAsPristine();
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createTaxCategory.id], { relativeTo: this.route });
             },
@@ -84,10 +84,10 @@ export class TaxCategoryDetailComponent extends BaseDetailComponent<TaxCategory.
     }
 
     save() {
-        if (!this.taxCategoryForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.taxCategoryForm.value;
+        const formValue = this.detailForm.value;
         this.taxCategory$
             .pipe(
                 take(1),
@@ -104,7 +104,7 @@ export class TaxCategoryDetailComponent extends BaseDetailComponent<TaxCategory.
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'TaxCategory',
                     });
-                    this.taxCategoryForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -119,7 +119,7 @@ export class TaxCategoryDetailComponent extends BaseDetailComponent<TaxCategory.
      * Update the form values when the entity changes.
      */
     protected setFormValues(entity: TaxCategory.Fragment, languageCode: LanguageCode): void {
-        this.taxCategoryForm.patchValue({
+        this.detailForm.patchValue({
             name: entity.name,
         });
     }

+ 1 - 1
admin-ui/src/app/settings/components/tax-rate-detail/tax-rate-detail.component.html

@@ -18,7 +18,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="taxRateForm">
+<form class="form" [formGroup]="detailForm">
     <section class="form-block">
         <vdr-form-field [label]="'common.name' | translate" for="name">
             <input id="name" type="text" formControlName="name" />

+ 10 - 10
admin-ui/src/app/settings/components/tax-rate-detail/tax-rate-detail.component.ts

@@ -30,7 +30,7 @@ export class TaxRateDetailComponent extends BaseDetailComponent<TaxRate.Fragment
     taxCategories$: Observable<TaxCategory.Fragment[]>;
     zones$: Observable<GetZones.Zones[]>;
     groups$: Observable<CustomerGroup[]>;
-    taxRateForm: FormGroup;
+    detailForm: FormGroup;
 
     constructor(
         router: Router,
@@ -42,7 +42,7 @@ export class TaxRateDetailComponent extends BaseDetailComponent<TaxRate.Fragment
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService);
-        this.taxRateForm = this.formBuilder.group({
+        this.detailForm = this.formBuilder.group({
             name: ['', Validators.required],
             enabled: [true],
             value: [0, Validators.required],
@@ -65,14 +65,14 @@ export class TaxRateDetailComponent extends BaseDetailComponent<TaxRate.Fragment
     }
 
     saveButtonEnabled(): boolean {
-        return this.taxRateForm.dirty && this.taxRateForm.valid;
+        return this.detailForm.dirty && this.detailForm.valid;
     }
 
     create() {
-        if (!this.taxRateForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.taxRateForm.value;
+        const formValue = this.detailForm.value;
         const input = {
             name: formValue.name,
             enabled: formValue.enabled,
@@ -86,7 +86,7 @@ export class TaxRateDetailComponent extends BaseDetailComponent<TaxRate.Fragment
                 this.notificationService.success(_('common.notify-create-success'), {
                     entity: 'TaxRate',
                 });
-                this.taxRateForm.markAsPristine();
+                this.detailForm.markAsPristine();
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createTaxRate.id], { relativeTo: this.route });
             },
@@ -99,10 +99,10 @@ export class TaxRateDetailComponent extends BaseDetailComponent<TaxRate.Fragment
     }
 
     save() {
-        if (!this.taxRateForm.dirty) {
+        if (!this.detailForm.dirty) {
             return;
         }
-        const formValue = this.taxRateForm.value;
+        const formValue = this.detailForm.value;
         this.entity$
             .pipe(
                 take(1),
@@ -124,7 +124,7 @@ export class TaxRateDetailComponent extends BaseDetailComponent<TaxRate.Fragment
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'TaxRate',
                     });
-                    this.taxRateForm.markAsPristine();
+                    this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -139,7 +139,7 @@ export class TaxRateDetailComponent extends BaseDetailComponent<TaxRate.Fragment
      * Update the form values when the entity changes.
      */
     protected setFormValues(entity: TaxRate.Fragment, languageCode: LanguageCode): void {
-        this.taxRateForm.patchValue({
+        this.detailForm.patchValue({
             name: entity.name,
             enabled: entity.enabled,
             value: entity.value,

+ 9 - 0
admin-ui/src/app/settings/settings.routes.ts

@@ -12,6 +12,7 @@ import {
 import { createResolveData } from '../common/base-entity-resolver';
 import { detailBreadcrumb } from '../common/detail-breadcrumb';
 import { _ } from '../core/providers/i18n/mark-for-extraction';
+import { CanDeactivateDetailGuard } from '../shared/providers/routing/can-deactivate-detail-guard';
 
 import { AdminDetailComponent } from './components/admin-detail/admin-detail.component';
 import { AdministratorListComponent } from './components/administrator-list/administrator-list.component';
@@ -50,6 +51,7 @@ export const settingsRoutes: Route[] = [
         path: 'administrators/:id',
         component: AdminDetailComponent,
         resolve: createResolveData(AdministratorResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: { breadcrumb: administratorBreadcrumb },
     },
     {
@@ -63,6 +65,7 @@ export const settingsRoutes: Route[] = [
         path: 'channels/:id',
         component: ChannelDetailComponent,
         resolve: createResolveData(ChannelResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: { breadcrumb: channelBreadcrumb },
     },
     {
@@ -76,6 +79,7 @@ export const settingsRoutes: Route[] = [
         path: 'roles/:id',
         component: RoleDetailComponent,
         resolve: createResolveData(RoleResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: { breadcrumb: roleBreadcrumb },
     },
     {
@@ -89,6 +93,7 @@ export const settingsRoutes: Route[] = [
         path: 'tax-categories/:id',
         component: TaxCategoryDetailComponent,
         resolve: createResolveData(TaxCategoryResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: taxCategoryBreadcrumb,
         },
@@ -104,6 +109,7 @@ export const settingsRoutes: Route[] = [
         path: 'tax-rates/:id',
         component: TaxRateDetailComponent,
         resolve: createResolveData(TaxRateResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: taxRateBreadcrumb,
         },
@@ -119,6 +125,7 @@ export const settingsRoutes: Route[] = [
         path: 'countries/:id',
         component: CountryDetailComponent,
         resolve: createResolveData(CountryResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: countryBreadcrumb,
         },
@@ -134,6 +141,7 @@ export const settingsRoutes: Route[] = [
         path: 'shipping-methods/:id',
         component: ShippingMethodDetailComponent,
         resolve: createResolveData(ShippingMethodResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: shippingMethodBreadcrumb,
         },
@@ -149,6 +157,7 @@ export const settingsRoutes: Route[] = [
         path: 'payment-methods/:id',
         component: PaymentMethodDetailComponent,
         resolve: createResolveData(PaymentMethodResolver),
+        canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: paymentMethodBreadcrumb,
         },

+ 12 - 0
admin-ui/src/app/shared/components/confirm-navigation-dialog/confirm-navigation-dialog.component.html

@@ -0,0 +1,12 @@
+<ng-template vdrDialogTitle>{{ 'common.confirm-navigation' | translate }}</ng-template>
+
+{{ 'common.there-are-unsaved-changes' | translate }}
+
+<ng-template vdrDialogButtons>
+    <button type="button" (click)="confirm()" class="btn btn-warning">
+        {{ 'common.discard-changes' | translate }}
+    </button>
+    <button type="button" class="btn btn-primary" (click)="cancel()">
+        {{ 'common.cancel-navigation' | translate }}
+    </button>
+</ng-template>

+ 0 - 0
admin-ui/src/app/shared/components/confirm-navigation-dialog/confirm-navigation-dialog.component.scss


+ 21 - 0
admin-ui/src/app/shared/components/confirm-navigation-dialog/confirm-navigation-dialog.component.ts

@@ -0,0 +1,21 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+import { Dialog } from '../../providers/modal/modal.service';
+
+@Component({
+    selector: 'vdr-confirm-navigation-dialog',
+    templateUrl: './confirm-navigation-dialog.component.html',
+    styleUrls: ['./confirm-navigation-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ConfirmNavigationDialogComponent implements Dialog<boolean> {
+    resolveWith: (result?: boolean) => void;
+
+    confirm() {
+        this.resolveWith(true);
+    }
+
+    cancel() {
+        this.resolveWith(false);
+    }
+}

+ 28 - 0
admin-ui/src/app/shared/providers/routing/can-deactivate-detail-guard.ts

@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot } from '@angular/router';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { BaseDetailComponent } from '../../../common/base-detail.component';
+import { ConfirmNavigationDialogComponent } from '../../components/confirm-navigation-dialog/confirm-navigation-dialog.component';
+import { ModalService } from '../modal/modal.service';
+
+@Injectable()
+export class CanDeactivateDetailGuard implements CanDeactivate<BaseDetailComponent<any>> {
+    constructor(private modalService: ModalService) {}
+
+    canDeactivate(
+        component: BaseDetailComponent<any>,
+        currentRoute: ActivatedRouteSnapshot,
+        currentState: RouterStateSnapshot,
+        nextState?: RouterStateSnapshot,
+    ): boolean | Observable<boolean> {
+        if (component.detailForm && component.detailForm.dirty) {
+            return this.modalService
+                .fromComponent(ConfirmNavigationDialogComponent)
+                .pipe(map(result => !!result));
+        } else {
+            return true;
+        }
+    }
+}

+ 5 - 1
admin-ui/src/app/shared/shared.module.ts

@@ -16,6 +16,7 @@ import { AdjustmentOperationInputComponent } from './components/adjustment-opera
 import { AffixedInputComponent } from './components/affixed-input/affixed-input.component';
 import { PercentageSuffixInputComponent } from './components/affixed-input/percentage-suffix-input.component';
 import { ChipComponent } from './components/chip/chip.component';
+import { ConfirmNavigationDialogComponent } from './components/confirm-navigation-dialog/confirm-navigation-dialog.component';
 import { CurrencyInputComponent } from './components/currency-input/currency-input.component';
 import { CustomFieldControlComponent } from './components/custom-field-control/custom-field-control.component';
 import { CustomerLabelComponent } from './components/customer-label/customer-label.component';
@@ -38,6 +39,7 @@ import { TableRowActionComponent } from './components/table-row-action/table-row
 import { BackgroundColorFromDirective } from './directives/background-color-from.directive';
 import { FileSizePipe } from './pipes/file-size.pipe';
 import { ModalService } from './providers/modal/modal.service';
+import { CanDeactivateDetailGuard } from './providers/routing/can-deactivate-detail-guard';
 
 const IMPORTS = [
     ClarityModule,
@@ -58,6 +60,7 @@ const DECLARATIONS = [
     AffixedInputComponent,
     BackgroundColorFromDirective,
     ChipComponent,
+    ConfirmNavigationDialogComponent,
     CurrencyInputComponent,
     CustomerLabelComponent,
     CustomFieldControlComponent,
@@ -91,8 +94,9 @@ const DECLARATIONS = [
         // are unknown to the CoreModule instance of ModalService.
         // See https://github.com/angular/angular/issues/14324#issuecomment-305650763
         ModalService,
+        CanDeactivateDetailGuard,
     ],
-    entryComponents: [ModalDialogComponent],
+    entryComponents: [ModalDialogComponent, ConfirmNavigationDialogComponent],
     schemas: [CUSTOM_ELEMENTS_SCHEMA],
 })
 export class SharedModule {}

+ 4 - 0
admin-ui/src/i18n-messages/en.json

@@ -79,13 +79,16 @@
     "ID": "ID",
     "back": "Back",
     "cancel": "Cancel",
+    "cancel-navigation": "Cancel navigation",
     "code": "Code",
     "confirm": "Confirm",
+    "confirm-navigation": "Confirm navigation",
     "create": "Create",
     "created": "Created",
     "created-at": "Created at",
     "custom-fields": "Custom fields",
     "description": "Description",
+    "discard-changes": "Discard changes",
     "edit": "Edit",
     "edit-field": "Edit field",
     "enabled": "Enabled",
@@ -107,6 +110,7 @@
     "remember-me": "Remember me",
     "remove": "Remove",
     "select": "Select...",
+    "there-are-unsaved-changes": "There are unsaved changes. Navigating away will cause these changes to be lost.",
     "update": "Update",
     "updated": "Updated",
     "updated-at": "Updated at",