Browse Source

feat(admin-ui): Allow the setting of custom Permissions

Relates to #450
Michael Bromley 5 years ago
parent
commit
d525a323e0
22 changed files with 643 additions and 787 deletions
  1. 22 17
      packages/admin-ui/i18n-coverage.json
  2. 417 551
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  3. 5 0
      packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts
  4. 3 0
      packages/admin-ui/src/lib/core/src/shared/components/select-toggle/select-toggle.component.html
  5. 12 3
      packages/admin-ui/src/lib/core/src/shared/components/select-toggle/select-toggle.component.scss
  6. 1 0
      packages/admin-ui/src/lib/core/src/shared/components/select-toggle/select-toggle.component.ts
  7. 4 2
      packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.html
  8. 53 40
      packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.ts
  9. 11 11
      packages/admin-ui/src/lib/settings/src/components/permission-grid/permission-grid.component.html
  10. 5 0
      packages/admin-ui/src/lib/settings/src/components/permission-grid/permission-grid.component.scss
  11. 56 36
      packages/admin-ui/src/lib/settings/src/components/permission-grid/permission-grid.component.ts
  12. 4 2
      packages/admin-ui/src/lib/settings/src/components/role-detail/role-detail.component.html
  13. 25 36
      packages/admin-ui/src/lib/settings/src/components/role-detail/role-detail.component.ts
  14. 11 12
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  15. 1 11
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  16. 1 11
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  17. 1 11
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  18. 1 11
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  19. 1 11
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  20. 1 11
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  21. 1 11
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  22. 7 0
      packages/dev-server/dev-config.ts

+ 22 - 17
packages/admin-ui/i18n-coverage.json

@@ -1,40 +1,45 @@
 {
-  "generatedOn": "2020-10-21T09:57:14.594Z",
-  "lastCommit": "992682eb87470577aa9f600f5bafecbd6b69607b",
+  "generatedOn": "2020-10-26T14:24:59.367Z",
+  "lastCommit": "861a7bde2a8182785c0ab5e723ec642587ef018e",
   "translationStatus": {
+    "cs": {
+      "tokenCount": 672,
+      "translatedCount": 662,
+      "percentage": 99
+    },
     "de": {
-      "tokenCount": 682,
-      "translatedCount": 609,
+      "tokenCount": 672,
+      "translatedCount": 598,
       "percentage": 89
     },
     "en": {
-      "tokenCount": 682,
-      "translatedCount": 680,
+      "tokenCount": 672,
+      "translatedCount": 671,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 682,
-      "translatedCount": 466,
+      "tokenCount": 672,
+      "translatedCount": 455,
       "percentage": 68
     },
     "pl": {
-      "tokenCount": 682,
-      "translatedCount": 564,
-      "percentage": 83
+      "tokenCount": 672,
+      "translatedCount": 553,
+      "percentage": 82
     },
     "pt_BR": {
-      "tokenCount": 682,
-      "translatedCount": 655,
+      "tokenCount": 672,
+      "translatedCount": 644,
       "percentage": 96
     },
     "zh_Hans": {
-      "tokenCount": 682,
-      "translatedCount": 548,
+      "tokenCount": 672,
+      "translatedCount": 537,
       "percentage": 80
     },
     "zh_Hant": {
-      "tokenCount": 682,
-      "translatedCount": 548,
+      "tokenCount": 672,
+      "translatedCount": 537,
       "percentage": 80
     }
   }

File diff suppressed because it is too large
+ 417 - 551
packages/admin-ui/src/lib/core/src/common/generated-types.ts


+ 5 - 0
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -424,6 +424,11 @@ export const GLOBAL_SETTINGS_FRAGMENT = gql`
         trackInventory
         outOfStockThreshold
         serverConfig {
+            permissions {
+                name
+                description
+                assignable
+            }
             orderProcess {
                 name
             }

+ 3 - 0
packages/admin-ui/src/lib/core/src/shared/components/select-toggle/select-toggle.component.html

@@ -10,3 +10,6 @@
 >
     <clr-icon shape="check" [attr.size]="size === 'small' ? 16 : 32"></clr-icon>
 </div>
+<div class="toggle-label" [class.disabled]="disabled" *ngIf="label" (click)="selectedChange.emit(!selected)">
+    {{ label }}
+</div>

+ 12 - 3
packages/admin-ui/src/lib/core/src/shared/components/select-toggle/select-toggle.component.scss

@@ -1,5 +1,5 @@
-@import "variables";
-@import "mixins";
+@import 'variables';
+@import 'mixins';
 
 :host {
     display: flex;
@@ -45,10 +45,19 @@
 
     &:focus {
         outline: none;
-        box-shadow: 0 0 2px 2px #6bc1e3
+        box-shadow: 0 0 2px 2px #6bc1e3;
     }
 
     &.disabled {
         cursor: default;
     }
 }
+.toggle-label {
+    flex: 1;
+    margin-left: 6px;
+    text-align: left;
+    font-size: 12px;
+    &:not(.disabled) {
+        cursor: pointer;
+    }
+}

+ 1 - 0
packages/admin-ui/src/lib/core/src/shared/components/select-toggle/select-toggle.component.ts

@@ -13,5 +13,6 @@ export class SelectToggleComponent {
     @Input() size: 'small' | 'large' = 'large';
     @Input() selected = false;
     @Input() disabled = false;
+    @Input() label: string | undefined;
     @Output() selectedChange = new EventEmitter<boolean>();
 }

+ 4 - 2
packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.html

@@ -17,7 +17,7 @@
                 class="btn btn-primary"
                 (click)="save()"
                 *vdrIfPermissions="'UpdateAdministrator'"
-                [disabled]="(detailForm.invalid || detailForm.pristine)"
+                [disabled]="detailForm.invalid || detailForm.pristine"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -86,7 +86,9 @@
         </li>
     </ul>
     <vdr-permission-grid
-        [permissions]="getPermissionsForSelectedChannel()"
+        *ngIf="permissionDefinitions$ | async as defs"
+        [activePermissions]="getPermissionsForSelectedChannel()"
+        [permissionDefinitions]="defs"
         [readonly]="true"
     ></vdr-permission-grid>
 </form>

+ 53 - 40
packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.ts

@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { BaseDetailComponent } from '@vendure/admin-ui/core';
+import { BaseDetailComponent, PermissionDefinition } from '@vendure/admin-ui/core';
 import {
     Administrator,
     CreateAdministratorInput,
@@ -31,9 +31,11 @@ export interface PermissionsByChannel {
     styleUrls: ['./admin-detail.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class AdminDetailComponent extends BaseDetailComponent<GetAdministrator.Administrator>
+export class AdminDetailComponent
+    extends BaseDetailComponent<GetAdministrator.Administrator>
     implements OnInit, OnDestroy {
     administrator$: Observable<GetAdministrator.Administrator>;
+    permissionDefinitions$: Observable<PermissionDefinition[]>;
     allRoles$: Observable<Role.Fragment[]>;
     selectedRoles: Role.Fragment[] = [];
     detailForm: FormGroup;
@@ -66,7 +68,7 @@ export class AdminDetailComponent extends BaseDetailComponent<GetAdministrator.A
     ngOnInit() {
         this.init();
         this.administrator$ = this.entity$;
-        this.allRoles$ = this.dataService.administrator.getRoles(99999).mapStream((item) => item.roles.items);
+        this.allRoles$ = this.dataService.administrator.getRoles(99999).mapStream(item => item.roles.items);
         this.dataService.client.userStatus().single$.subscribe(({ userStatus }) => {
             if (!userStatus.permissions.includes(Permission.UpdateAdministrator)) {
                 const rolesSelect = this.detailForm.get('roles');
@@ -75,6 +77,9 @@ export class AdminDetailComponent extends BaseDetailComponent<GetAdministrator.A
                 }
             }
         });
+        this.permissionDefinitions$ = this.dataService.settings
+            .getGlobalSettings('cache-and-network')
+            .mapSingle(({ globalSettings }) => globalSettings.serverConfig.permissions);
     }
 
     ngOnDestroy(): void {
@@ -85,17 +90,23 @@ export class AdminDetailComponent extends BaseDetailComponent<GetAdministrator.A
         this.buildPermissionsMap();
     }
 
-    getPermissionsForSelectedChannel() {
+    getPermissionsForSelectedChannel(): string[] {
+        function getActivePermissions(input: PermissionsByChannel['permissions']): string[] {
+            return Object.entries(input)
+                .filter(([permission, active]) => active)
+                .map(([permission, active]) => permission);
+        }
         if (this.selectedChannelId) {
             const selectedChannel = this.selectedRolePermissions[this.selectedChannelId];
             if (selectedChannel) {
-                return this.selectedRolePermissions[this.selectedChannelId].permissions;
+                const permissionMap = this.selectedRolePermissions[this.selectedChannelId].permissions;
+                return getActivePermissions(permissionMap);
             }
         }
         const channels = Object.values(this.selectedRolePermissions);
         if (0 < channels.length) {
             this.selectedChannelId = channels[0].channelId;
-            return channels[0].permissions;
+            return getActivePermissions(channels[0].permissions);
         }
         return [];
     }
@@ -107,10 +118,10 @@ export class AdminDetailComponent extends BaseDetailComponent<GetAdministrator.A
             firstName: formValue.firstName,
             lastName: formValue.lastName,
             password: formValue.password,
-            roleIds: formValue.roles.map((role) => role.id),
+            roleIds: formValue.roles.map(role => role.id),
         };
         this.dataService.administrator.createAdministrator(administrator).subscribe(
-            (data) => {
+            data => {
                 this.notificationService.success(_('common.notify-create-success'), {
                     entity: 'Administrator',
                 });
@@ -118,7 +129,7 @@ export class AdminDetailComponent extends BaseDetailComponent<GetAdministrator.A
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createAdministrator.id], { relativeTo: this.route });
             },
-            (err) => {
+            err => {
                 this.notificationService.error(_('common.notify-create-error'), {
                     entity: 'Administrator',
                 });
@@ -138,20 +149,20 @@ export class AdminDetailComponent extends BaseDetailComponent<GetAdministrator.A
                         firstName: formValue.firstName,
                         lastName: formValue.lastName,
                         password: formValue.password,
-                        roleIds: formValue.roles.map((role) => role.id),
+                        roleIds: formValue.roles.map(role => role.id),
                     };
                     return this.dataService.administrator.updateAdministrator(administrator);
                 }),
             )
             .subscribe(
-                (data) => {
+                data => {
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'Administrator',
                     });
                     this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
                 },
-                (err) => {
+                err => {
                     this.notificationService.error(_('common.notify-update-error'), {
                         entity: 'Administrator',
                     });
@@ -178,39 +189,41 @@ export class AdminDetailComponent extends BaseDetailComponent<GetAdministrator.A
     }
 
     private buildPermissionsMap() {
-        const permissionsControl = this.detailForm.get('roles');
-        if (permissionsControl) {
-            const roles: RoleFragment[] = permissionsControl.value;
-            const channelIdPermissionsMap = new Map<string, Set<Permission>>();
-            const channelIdCodeMap = new Map<string, string>();
+        this.permissionDefinitions$.pipe(take(1)).subscribe(defs => {
+            const permissionsControl = this.detailForm.get('roles');
+            if (permissionsControl) {
+                const roles: RoleFragment[] = permissionsControl.value;
+                const channelIdPermissionsMap = new Map<string, Set<Permission>>();
+                const channelIdCodeMap = new Map<string, string>();
 
-            for (const role of roles) {
-                for (const channel of role.channels) {
-                    const channelPermissions = channelIdPermissionsMap.get(channel.id);
-                    const permissionSet = channelPermissions || new Set<Permission>();
+                for (const role of roles) {
+                    for (const channel of role.channels) {
+                        const channelPermissions = channelIdPermissionsMap.get(channel.id);
+                        const permissionSet = channelPermissions || new Set<Permission>();
 
-                    role.permissions.forEach((p) => permissionSet.add(p));
-                    channelIdPermissionsMap.set(channel.id, permissionSet);
-                    channelIdCodeMap.set(channel.id, channel.code);
+                        role.permissions.forEach(p => permissionSet.add(p));
+                        channelIdPermissionsMap.set(channel.id, permissionSet);
+                        channelIdCodeMap.set(channel.id, channel.code);
+                    }
                 }
-            }
 
-            this.selectedRolePermissions = {} as any;
-            for (const channelId of Array.from(channelIdPermissionsMap.keys())) {
-                // tslint:disable-next-line:no-non-null-assertion
-                const permissionSet = channelIdPermissionsMap.get(channelId)!;
-                const permissionsHash: { [K in Permission]: boolean } = {} as any;
-                for (const permission of Object.keys(Permission)) {
-                    permissionsHash[permission] = permissionSet.has(permission as Permission);
+                this.selectedRolePermissions = {} as any;
+                for (const channelId of Array.from(channelIdPermissionsMap.keys())) {
+                    // tslint:disable-next-line:no-non-null-assertion
+                    const permissionSet = channelIdPermissionsMap.get(channelId)!;
+                    const permissionsHash: { [K in Permission]: boolean } = {} as any;
+                    for (const def of defs) {
+                        permissionsHash[def.name] = permissionSet.has(def.name as Permission);
+                    }
+                    this.selectedRolePermissions[channelId] = {
+                        // tslint:disable:no-non-null-assertion
+                        channelId,
+                        channelCode: channelIdCodeMap.get(channelId)!,
+                        permissions: permissionsHash,
+                        // tslint:enable:no-non-null-assertion
+                    };
                 }
-                this.selectedRolePermissions[channelId] = {
-                    // tslint:disable:no-non-null-assertion
-                    channelId,
-                    channelCode: channelIdCodeMap.get(channelId)!,
-                    permissions: permissionsHash,
-                    // tslint:enable:no-non-null-assertion
-                };
             }
-        }
+        });
     }
 }

+ 11 - 11
packages/admin-ui/src/lib/settings/src/components/permission-grid/permission-grid.component.html

@@ -1,20 +1,20 @@
 <table class="table">
-    <tr>
-        <th>{{ 'settings.section' | translate }}</th>
-        <th>{{ 'settings.create' | translate }}</th>
-        <th>{{ 'settings.read' | translate }}</th>
-        <th>{{ 'settings.update' | translate }}</th>
-        <th>{{ 'settings.delete' | translate }}</th>
-    </tr>
     <tbody>
         <tr *ngFor="let section of gridData">
-            <td>{{ section.label | translate }}</td>
-            <td *ngFor="let permission of section.permissions">
+            <td class="permission-group">
+                <div>{{ section.label | translate }}</div>
+                <button *ngIf="1 < section.permissions.length && !readonly" class="btn btn-sm btn-link" (click)="toggleAll(section.permissions)">
+                    {{ 'common.toggle-all' | translate }}
+                </button>
+            </td>
+            <td *ngFor="let permission of section.permissions" [attr.colspan]="section.permissions.length === 1 ? 4 : 1">
                 <vdr-select-toggle
                     size="small"
+                    [title]="permission.description"
+                    [label]="permission.name"
                     [disabled]="readonly"
-                    [selected]="permissions[permission]"
-                    (selectedChange)="setPermission(permission, $event)"
+                    [selected]="activePermissions?.includes(permission.name)"
+                    (selectedChange)="setPermission(permission.name, $event)"
                 ></vdr-select-toggle>
             </td>
         </tr>

+ 5 - 0
packages/admin-ui/src/lib/settings/src/components/permission-grid/permission-grid.component.scss

@@ -0,0 +1,5 @@
+@import "variables";
+
+td.permission-group {
+    background-color: $color-grey-200;
+}

+ 56 - 36
packages/admin-ui/src/lib/settings/src/components/permission-grid/permission-grid.component.ts

@@ -1,7 +1,11 @@
-import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { PermissionDefinition } from '@vendure/admin-ui/core';
 
-import { Permission } from '@vendure/admin-ui/core';
+export interface PermissionGridRow {
+    label: string;
+    permissions: PermissionDefinition[];
+}
 
 /**
  * A table showing and allowing the setting of all possible CRUD permissions.
@@ -10,47 +14,63 @@ import { Permission } from '@vendure/admin-ui/core';
     selector: 'vdr-permission-grid',
     templateUrl: './permission-grid.component.html',
     styleUrls: ['./permission-grid.component.scss'],
-    changeDetection: ChangeDetectionStrategy.Default,
+    changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class PermissionGridComponent {
-    @Input() permissions: { [K in Permission]: boolean } = {} as any;
+export class PermissionGridComponent implements OnInit {
+    @Input() permissionDefinitions: PermissionDefinition[];
+    @Input() activePermissions: string[];
     @Input() readonly = false;
     @Output() permissionChange = new EventEmitter<{ permission: string; value: boolean }>();
-    readonly gridData = [
-        {
-            label: _('settings.catalog'),
-            permissions: ['CreateCatalog', 'ReadCatalog', 'UpdateCatalog', 'DeleteCatalog'],
-        },
-        {
-            label: _('settings.customer'),
-            permissions: ['CreateCustomer', 'ReadCustomer', 'UpdateCustomer', 'DeleteCustomer'],
-        },
-        {
-            label: _('settings.order'),
-            permissions: ['CreateOrder', 'ReadOrder', 'UpdateOrder', 'DeleteOrder'],
-        },
-        {
-            label: _('settings.promotion'),
-            permissions: ['CreatePromotion', 'ReadPromotion', 'UpdatePromotion', 'DeletePromotion'],
-        },
-        {
-            label: _('settings.administrator'),
-            permissions: [
-                'CreateAdministrator',
-                'ReadAdministrator',
-                'UpdateAdministrator',
-                'DeleteAdministrator',
-            ],
-        },
-        {
-            label: _('settings.settings'),
-            permissions: ['CreateSettings', 'ReadSettings', 'UpdateSettings', 'DeleteSettings'],
-        },
-    ];
+    gridData: PermissionGridRow[];
+
+    ngOnInit() {
+        this.buildGrid();
+    }
 
     setPermission(permission: string, value: boolean) {
         if (!this.readonly) {
             this.permissionChange.emit({ permission, value });
         }
     }
+
+    toggleAll(defs: PermissionDefinition[]) {
+        const value = defs.some(d => !this.activePermissions.includes(d.name));
+        for (const def of defs) {
+            this.permissionChange.emit({ permission: def.name, value });
+        }
+    }
+
+    private buildGrid() {
+        const crudGroups = new Map<string, PermissionDefinition[]>();
+        const nonCrud: PermissionDefinition[] = [];
+        const crudRe = /^(Create|Read|Update|Delete)([a-zA-Z]+)$/;
+        for (const def of this.permissionDefinitions) {
+            const isCrud = crudRe.test(def.name);
+            if (isCrud) {
+                const groupName = def.name.match(crudRe)?.[2];
+                if (groupName) {
+                    const existing = crudGroups.get(groupName);
+                    if (existing) {
+                        existing.push(def);
+                    } else {
+                        crudGroups.set(groupName, [def]);
+                    }
+                }
+            } else if (def.assignable) {
+                nonCrud.push(def);
+            }
+        }
+        this.gridData = [
+            ...nonCrud.map(d => ({
+                label: d.name,
+                permissions: [d],
+            })),
+            ...Array.from(crudGroups.entries()).map(([label, defs]) => {
+                return {
+                    label,
+                    permissions: defs,
+                };
+            }),
+        ];
+    }
 }

+ 4 - 2
packages/admin-ui/src/lib/settings/src/components/role-detail/role-detail.component.html

@@ -17,7 +17,7 @@
                 class="btn btn-primary"
                 (click)="save()"
                 *vdrIfPermissions="'UpdateAdministrator'"
-                [disabled]="(detailForm.invalid || detailForm.pristine) && !permissionsChanged"
+                [disabled]="detailForm.invalid || detailForm.pristine"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -55,7 +55,9 @@
     </vdr-form-field>
     <label>{{ 'settings.permissions' | translate }}</label>
     <vdr-permission-grid
-        [permissions]="permissions"
+        *ngIf="permissionDefinitions$ | async as defs"
+        [permissionDefinitions]="defs"
+        [activePermissions]="detailForm.get('permissions')?.value"
         (permissionChange)="setPermission($event)"
         [readonly]="!('UpdateAdministrator' | hasPermission)"
     ></vdr-permission-grid>

+ 25 - 36
packages/admin-ui/src/lib/settings/src/components/role-detail/role-detail.component.ts

@@ -9,11 +9,13 @@ import {
     LanguageCode,
     NotificationService,
     Permission,
+    PermissionDefinition,
     Role,
     ServerConfigService,
     UpdateRoleInput,
 } from '@vendure/admin-ui/core';
 import { normalizeString } from '@vendure/common/lib/normalize-string';
+import { unique } from '@vendure/common/lib/unique';
 import { Observable } from 'rxjs';
 import { mergeMap, take } from 'rxjs/operators';
 
@@ -26,8 +28,7 @@ import { mergeMap, take } from 'rxjs/operators';
 export class RoleDetailComponent extends BaseDetailComponent<Role> implements OnInit, OnDestroy {
     role$: Observable<Role>;
     detailForm: FormGroup;
-    permissions: { [K in Permission]: boolean };
-    permissionsChanged = false;
+    permissionDefinitions$: Observable<PermissionDefinition[]>;
     constructor(
         router: Router,
         route: ActivatedRoute,
@@ -38,21 +39,20 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService, dataService);
-        this.permissions = Object.keys(Permission).reduce(
-            (result, key) => ({ ...result, [key]: false }),
-            {} as { [K in Permission]: boolean },
-        );
         this.detailForm = this.formBuilder.group({
             code: ['', Validators.required],
             description: ['', Validators.required],
             channelIds: [],
+            permissions: [],
         });
     }
 
     ngOnInit() {
         this.init();
         this.role$ = this.entity$;
-        // setTimeout(() => this.changeDetector.markForCheck(), 2000);
+        this.permissionDefinitions$ = this.dataService.settings
+            .getGlobalSettings('cache-and-network')
+            .mapSingle(({ globalSettings }) => globalSettings.serverConfig.permissions);
     }
 
     ngOnDestroy(): void {
@@ -67,27 +67,29 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
     }
 
     setPermission(change: { permission: string; value: boolean }) {
-        this.permissions = { ...this.permissions, [change.permission]: change.value };
-        this.permissionsChanged = true;
+        const permissionsControl = this.detailForm.get('permissions');
+        if (permissionsControl) {
+            const currentPermissions = permissionsControl.value as string[];
+            const newValue =
+                change.value === true
+                    ? unique([...currentPermissions, change.permission])
+                    : currentPermissions.filter(p => p !== change.permission);
+            permissionsControl.setValue(newValue);
+            permissionsControl.markAsDirty();
+        }
     }
 
     create() {
         const formValue = this.detailForm.value;
-        const role: CreateRoleInput = {
-            code: formValue.code,
-            description: formValue.description,
-            permissions: this.getSelectedPermissions(),
-            channelIds: formValue.channelIds,
-        };
+        const role: CreateRoleInput = formValue;
         this.dataService.administrator.createRole(role).subscribe(
-            (data) => {
+            data => {
                 this.notificationService.success(_('common.notify-create-success'), { entity: 'Role' });
                 this.detailForm.markAsPristine();
                 this.changeDetector.markForCheck();
-                this.permissionsChanged = false;
                 this.router.navigate(['../', data.createRole.id], { relativeTo: this.route });
             },
-            (err) => {
+            err => {
                 this.notificationService.error(_('common.notify-create-error'), {
                     entity: 'Role',
                 });
@@ -101,24 +103,17 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
                 take(1),
                 mergeMap(({ id }) => {
                     const formValue = this.detailForm.value;
-                    const role: UpdateRoleInput = {
-                        id,
-                        code: formValue.code,
-                        description: formValue.description,
-                        permissions: this.getSelectedPermissions(),
-                        channelIds: formValue.channelIds,
-                    };
+                    const role: UpdateRoleInput = { id, ...formValue };
                     return this.dataService.administrator.updateRole(role);
                 }),
             )
             .subscribe(
-                (data) => {
+                data => {
                     this.notificationService.success(_('common.notify-update-success'), { entity: 'Role' });
                     this.detailForm.markAsPristine();
                     this.changeDetector.markForCheck();
-                    this.permissionsChanged = false;
                 },
-                (err) => {
+                err => {
                     this.notificationService.error(_('common.notify-update-error'), {
                         entity: 'Role',
                     });
@@ -130,18 +125,12 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
         this.detailForm.patchValue({
             description: role.description,
             code: role.code,
-            channelIds: role.channels.map((c) => c.id),
+            channelIds: role.channels.map(c => c.id),
+            permissions: role.permissions,
         });
-        for (const permission of Object.keys(this.permissions)) {
-            this.permissions[permission] = role.permissions.includes(permission as Permission);
-        }
         // This was required to get the channel selector component to
         // correctly display its contents. A while spent debugging the root
         // cause did not yield a solution, therefore this next line.
         this.changeDetector.detectChanges();
     }
-
-    private getSelectedPermissions(): Permission[] {
-        return Object.keys(this.permissions).filter((p) => this.permissions[p]) as Permission[];
-    }
 }

+ 11 - 12
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -101,6 +101,8 @@
     "option": "Volba",
     "option-name": "Jméno volby",
     "option-values": "Hodnoty volby",
+    "out-of-stock-threshold": "",
+    "out-of-stock-threshold-tooltip": "",
     "price": "Cena",
     "price-conversion-factor": "Přepočítávací koeficient ceny",
     "price-in-channel": "Cena v { channel }",
@@ -127,7 +129,12 @@
     "tax-category": "Skupina daní",
     "taxes": "Daně",
     "track-inventory": "Dopočítávat sklad",
+    "track-inventory-false": "",
+    "track-inventory-inherit": "",
+    "track-inventory-tooltip": "",
+    "track-inventory-true": "",
     "update-product-option": "Update product option",
+    "use-global-value": "",
     "values": "Hodnoty",
     "variant": "Varianta",
     "view-contents": "Zobrazit obsah",
@@ -200,6 +207,7 @@
     "select-display-language": "Vyberte jazyk",
     "select-today": "Vybrat dnešní datum",
     "there-are-unsaved-changes": "Provedené změny nebyly uloženy. Přechod na jinou stránku způsobí ztrátu těchto změn.",
+    "toggle-all": "",
     "update": "Aktualizovat",
     "updated-at": "Aktualizováno",
     "username": "Uživatelské jméno",
@@ -625,14 +633,11 @@
     "add-countries-to-zone": "Přidat země do { zoneName }",
     "add-countries-to-zone-success": "Přidáno: { countryCount } {countryCount, plural, one {země} other {země}} do zóny \"{ zoneName }\"",
     "add-products-to-test-order": "Přidat produkty do testovací objednávky",
-    "administrator": "Administrátor",
-    "catalog": "Katalog",
     "channel": "Kanál",
     "channel-token": "Token kanálu",
     "confirm-delete-role": "Smazat roli?",
     "confirm-delete-tax-category": "Smazat daňovou kategorii?",
     "confirm-delete-tax-rate": "Smazat daňovou sazbu?",
-    "create": "Vytváření",
     "create-new-channel": "Vytvořit kanál",
     "create-new-country": "Vytvořit zemi",
     "create-new-role": "Vytvořit roli",
@@ -642,32 +647,27 @@
     "create-new-zone": "Vytvořit zónu",
     "create-zone": "Vytvořit zónu",
     "currency": "Měna",
-    "customer": "Zákazník",
     "default-role-label": "Toto je výchozí role a nemůže být změněna.",
     "default-shipping-zone": "Výchozí dodací zóna",
     "default-tax-zone": "Výchozí daňová zóna",
-    "delete": "Mazání",
     "eligible": "Způsobilé",
     "email-address": "E-mailová adresa",
     "filter-by-member-name": "Filtrovat dle země",
     "first-name": "Jméno",
+    "global-out-of-stock-threshold": "",
+    "global-out-of-stock-threshold-tooltip": "",
     "last-name": "Příjmení",
     "no-eligible-shipping-methods": "Nezpůsobilé pro žádnou dodací metodu",
-    "order": "Objednávka",
     "password": "Heslo",
     "payment-method-config-options": "Konfigurace platební metody",
     "permissions": "Oprávnění",
     "prices-include-tax": "Zadávané ceny jsou včetně daně pro výchozí zónu",
-    "promotion": "Propagace",
     "rate": "Sazba",
-    "read": "Čtení",
     "remove-countries-from-zone-success": "Odebráno: { countryCount } {countryCount, plural, one {země} other {země}} ze zóny \"{ zoneName }\"",
     "remove-from-zone": "Odebrat ze zóny",
     "roles": "Role",
     "search-by-product-name-or-sku": "Hledat dle názvu nebo SKU produktu",
     "search-country-by-name": "Vyhledat zemi dle jména",
-    "section": "Sekce",
-    "settings": "Nastavení",
     "shipping-calculator": "Kalkulátor poštovného",
     "shipping-eligibility-checker": "Test způsobilosti k dodací metodě",
     "shipping-method": "Dodací metoda",
@@ -679,7 +679,6 @@
     "test-shipping-method": "Testovat dodací metodu",
     "test-shipping-methods": "Testovat dodací metody",
     "track-inventory-default": "Běžně dopočítavat sklad",
-    "update": "Úpravy",
     "update-zone": "Aktualizovat zónu",
     "view-zone-members": "Zobrazit členy",
     "zone": "Zóna"
@@ -702,4 +701,4 @@
     "job-result": "Výsledek úlohy",
     "job-state": "Stav úlohy"
   }
-}
+}

+ 1 - 11
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -207,6 +207,7 @@
     "select-display-language": "Anzeigesprache wählen",
     "select-today": "Heute auswählen",
     "there-are-unsaved-changes": "Es gibt ungespeicherte Änderungen. Wenn Sie wechseln, gehen diese Änderungen verloren.",
+    "toggle-all": "",
     "update": "Aktualisieren",
     "updated-at": "Aktualisiert am",
     "username": "Benutzername",
@@ -632,14 +633,11 @@
     "add-countries-to-zone": "Länder hinzufügen zu { zoneName }",
     "add-countries-to-zone-success": "{ countryCount } {countryCount, plural, one {Land} other {Länder}} hinzugefügt zu \"{ zoneName }\"",
     "add-products-to-test-order": "Produkte zur Testbestellung hinzufügen",
-    "administrator": "Administrator",
-    "catalog": "Katalog",
     "channel": "Kanal",
     "channel-token": "Kanal-Token",
     "confirm-delete-role": "Rolle löschen?",
     "confirm-delete-tax-category": "Steuerkategorie löschen?",
     "confirm-delete-tax-rate": "Steuersatz löschen?",
-    "create": "Erstellen",
     "create-new-channel": "Neuen Kanal erstellen",
     "create-new-country": "Neues Land erstellen",
     "create-new-role": "Neue Rolle erstellen",
@@ -649,11 +647,9 @@
     "create-new-zone": "Neue Zone erstellen",
     "create-zone": "Zone erstellen",
     "currency": "Währung",
-    "customer": "Kunde",
     "default-role-label": "Dies ist eine Standardrolle und kann nicht geändert werden.",
     "default-shipping-zone": "Standard-Versandzone",
     "default-tax-zone": "Standard-Steuerzone",
-    "delete": "Löschen",
     "eligible": "Verfügbar",
     "email-address": "E-Mail-Adresse",
     "filter-by-member-name": "Nach Land filtern",
@@ -662,21 +658,16 @@
     "global-out-of-stock-threshold-tooltip": "",
     "last-name": "Nachname",
     "no-eligible-shipping-methods": "Keine verfügbaren Versandarten",
-    "order": "Bestellung",
     "password": "Passwort",
     "payment-method-config-options": "Konfiguration der Zahlungsart",
     "permissions": "Berechtigungen",
     "prices-include-tax": "Preise enthalten die Steuer für die Standardzone",
-    "promotion": "Werbeaktion",
     "rate": "Steuersatz",
-    "read": "Lesen",
     "remove-countries-from-zone-success": "{ countryCount } {countryCount, plural, one {Land} other {Länder}} entfernt aus \"{ zoneName }\"",
     "remove-from-zone": "Aus Zone entfernen",
     "roles": "Rollen",
     "search-by-product-name-or-sku": "Suche nach Produktname oder Artikelnummer",
     "search-country-by-name": "Länder nach Namen suchen",
-    "section": "Bereich",
-    "settings": "Einstellungen",
     "shipping-calculator": "Versandkostenrechner",
     "shipping-eligibility-checker": "Verfügbarkeit der Versandart prüfen",
     "shipping-method": "Versandart",
@@ -688,7 +679,6 @@
     "test-shipping-method": "Test-Versandart",
     "test-shipping-methods": "Test-Versandarten",
     "track-inventory-default": "Bestand standardmäßig verfolgen",
-    "update": "Aktualisieren",
     "update-zone": "Zone aktualisieren",
     "view-zone-members": "Mitglieder anzeigen",
     "zone": "Zone"

+ 1 - 11
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -207,6 +207,7 @@
     "select-display-language": "Select display language",
     "select-today": "Select today",
     "there-are-unsaved-changes": "There are unsaved changes. Navigating away will cause these changes to be lost.",
+    "toggle-all": "Toggle all",
     "update": "Update",
     "updated-at": "Updated at",
     "username": "Username",
@@ -632,14 +633,11 @@
     "add-countries-to-zone": "Add countries to { zoneName }",
     "add-countries-to-zone-success": "Added { countryCount } {countryCount, plural, one {country} other {countries}} to zone \"{ zoneName }\"",
     "add-products-to-test-order": "Add products to the test order",
-    "administrator": "Administrator",
-    "catalog": "Catalog",
     "channel": "Channel",
     "channel-token": "Channel token",
     "confirm-delete-role": "Delete role?",
     "confirm-delete-tax-category": "Delete tax category?",
     "confirm-delete-tax-rate": "Delete tax rate?",
-    "create": "Create",
     "create-new-channel": "Create new channel",
     "create-new-country": "Create new country",
     "create-new-role": "Create new role",
@@ -649,11 +647,9 @@
     "create-new-zone": "Create new zone",
     "create-zone": "Create zone",
     "currency": "Currency",
-    "customer": "Customer",
     "default-role-label": "This is a default Role and cannot be modified",
     "default-shipping-zone": "Default shipping zone",
     "default-tax-zone": "Default tax zone",
-    "delete": "Delete",
     "eligible": "Eligible",
     "email-address": "Email address",
     "filter-by-member-name": "Filter by country",
@@ -662,21 +658,16 @@
     "global-out-of-stock-threshold-tooltip": "Sets the stock level at which this a variant is considered to be out of stock. Using a negative value enables backorder support. Can be overridden by product variants.",
     "last-name": "Last name",
     "no-eligible-shipping-methods": "No eligible shipping methods",
-    "order": "Order",
     "password": "Password",
     "payment-method-config-options": "Payment method configuration",
     "permissions": "Permissions",
     "prices-include-tax": "Prices include tax for the default Zone",
-    "promotion": "Promotion",
     "rate": "Rate",
-    "read": "Read",
     "remove-countries-from-zone-success": "Removed { countryCount } {countryCount, plural, one {country} other {countries}} from zone \"{ zoneName }\"",
     "remove-from-zone": "Remove from zone",
     "roles": "Roles",
     "search-by-product-name-or-sku": "Search by product name or SKU",
     "search-country-by-name": "Search countries by name",
-    "section": "Section",
-    "settings": "Settings",
     "shipping-calculator": "Shipping calculator",
     "shipping-eligibility-checker": "Shipping eligibility checker",
     "shipping-method": "Shipping Method",
@@ -688,7 +679,6 @@
     "test-shipping-method": "Test shipping method",
     "test-shipping-methods": "Test shipping methods",
     "track-inventory-default": "Track inventory by default",
-    "update": "Update",
     "update-zone": "Update zone",
     "view-zone-members": "View members",
     "zone": "Zone"

+ 1 - 11
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -207,6 +207,7 @@
     "select-display-language": "Seleccionar idioma de interfaz",
     "select-today": "Hoy",
     "there-are-unsaved-changes": "Hay cambios sin guardar. Si sale de este sitio sus cambios se perderán.",
+    "toggle-all": "",
     "update": "Actualizar",
     "updated-at": "Actualizado el",
     "username": "Nombre de usuario",
@@ -632,14 +633,11 @@
     "add-countries-to-zone": "Añadir países a zona...",
     "add-countries-to-zone-success": "Añadido { countryCount } {countryCount, plural, one {país} other {países}} a la zona \"{ zoneName }\"",
     "add-products-to-test-order": "",
-    "administrator": "Administrador",
-    "catalog": "Catálogo",
     "channel": "Canal de ventas",
     "channel-token": "Token de canal",
     "confirm-delete-role": "¿Eliminar rol?",
     "confirm-delete-tax-category": "¿Eliminar categoría de impuestos?",
     "confirm-delete-tax-rate": "¿Eliminar tasa de impuestos?",
-    "create": "Crear",
     "create-new-channel": "Crear nuevo canal",
     "create-new-country": "Crear nuevo país",
     "create-new-role": "Crear nuevo rol",
@@ -649,11 +647,9 @@
     "create-new-zone": "Crear nueva zona",
     "create-zone": "Crear zona",
     "currency": "Moneda",
-    "customer": "Cliente",
     "default-role-label": "",
     "default-shipping-zone": "Zona de envío por defecto",
     "default-tax-zone": "Zona de impuestos por defecto",
-    "delete": "Eliminar",
     "eligible": "Disponible",
     "email-address": "Dirección de email",
     "filter-by-member-name": "Filtrar por país",
@@ -662,21 +658,16 @@
     "global-out-of-stock-threshold-tooltip": "",
     "last-name": "Apellidos",
     "no-eligible-shipping-methods": "No hay métodos de envío disponibles",
-    "order": "Pedido",
     "password": "Contraseña",
     "payment-method-config-options": "Configuración método de pago",
     "permissions": "Permisos",
     "prices-include-tax": "Los precios incluyen impuestos para la zona por defecto.",
-    "promotion": "Promoción",
     "rate": "Tasa",
-    "read": "Leer",
     "remove-countries-from-zone-success": "Eliminados { countryCount } {countryCount, plural, one {país} other {países}} de la zona \"{ zoneName }\"",
     "remove-from-zone": "Eliminar de la zona",
     "roles": "Roles",
     "search-by-product-name-or-sku": "Buscar producto por nombre o código",
     "search-country-by-name": "Buscar país por nombre",
-    "section": "Sección",
-    "settings": "Ajustes",
     "shipping-calculator": "Calculador de envíos",
     "shipping-eligibility-checker": "Comprueba disponibilidad de envío",
     "shipping-method": "Método de envío",
@@ -688,7 +679,6 @@
     "test-shipping-method": "Comprobar método de envío",
     "test-shipping-methods": "Comprobar métodos de envío",
     "track-inventory-default": "",
-    "update": "Actualizar",
     "update-zone": "Actualizar zona",
     "view-zone-members": "Ver países",
     "zone": "Zona"

+ 1 - 11
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -207,6 +207,7 @@
     "select-display-language": "Wybierz język",
     "select-today": "Wybierz dzisiaj",
     "there-are-unsaved-changes": "Są nie zapisane zmiany. Nawigacja do innej lokalizacji spowoduje utrate zmian.",
+    "toggle-all": "",
     "update": "Aktualizuj",
     "updated-at": "Zaaktualizowano dnia",
     "username": "Nazwa użytkownika",
@@ -632,14 +633,11 @@
     "add-countries-to-zone": "Dodaj kraje do strefy...",
     "add-countries-to-zone-success": "Dodano { countryCount } {countryCount, plural, one {kraj} other {kraje}} do strefy \"{ zoneName }\"",
     "add-products-to-test-order": "Dodaj produkty do testowanego zamówienia",
-    "administrator": "Administrator",
-    "catalog": "Katalog",
     "channel": "Kanał",
     "channel-token": "Token kanału",
     "confirm-delete-role": "Usunąć role?",
     "confirm-delete-tax-category": "Usunąć kategorię podatkową?",
     "confirm-delete-tax-rate": "Usunąć stawke podatkową?",
-    "create": "Utwórz",
     "create-new-channel": "Utwórz nowy kanał",
     "create-new-country": "Utwórz nowy kraj",
     "create-new-role": "Utwórz nową role",
@@ -649,11 +647,9 @@
     "create-new-zone": "",
     "create-zone": "Utwórz strefe",
     "currency": "Waluta",
-    "customer": "Klient",
     "default-role-label": "To jest domyślna rola i nie może być zmieniana",
     "default-shipping-zone": "Domyślna strefa zakupowa",
     "default-tax-zone": "Domyślna strefa podatkowa",
-    "delete": "Usuń",
     "eligible": "Wybieralny",
     "email-address": "Email",
     "filter-by-member-name": "",
@@ -662,21 +658,16 @@
     "global-out-of-stock-threshold-tooltip": "",
     "last-name": "Nazwisko",
     "no-eligible-shipping-methods": "Brak pasujących metod wysyłki",
-    "order": "Zamówienie",
     "password": "Hasło",
     "payment-method-config-options": "Konfiguracja metody płatności",
     "permissions": "Uprawnienia",
     "prices-include-tax": "Ceny zawierają podatek dla domyślnej strefy",
-    "promotion": "Promocja",
     "rate": "Stawka",
-    "read": "Odczyt",
     "remove-countries-from-zone-success": "Usunięto { countryCount } {countryCount, plural, one {kraj} other {kraje}} ze strefy \"{ zoneName }\"",
     "remove-from-zone": "",
     "roles": "Role",
     "search-by-product-name-or-sku": "Szukaj produktu po nazwie lub SKU",
     "search-country-by-name": "Szukaj kraju po nazwie",
-    "section": "Sekcja",
-    "settings": "Ustawienia",
     "shipping-calculator": "Kalkulator wysyłki",
     "shipping-eligibility-checker": "Sprawdź możliwość wysyłki",
     "shipping-method": "Metoda wysyłki",
@@ -688,7 +679,6 @@
     "test-shipping-method": "Testowa metoda wysyłki",
     "test-shipping-methods": "Testowe metody wysyłki",
     "track-inventory-default": "Śledź domyślnie magazyn",
-    "update": "Aktualizuj",
     "update-zone": "",
     "view-zone-members": "",
     "zone": "Strefa"

+ 1 - 11
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -207,6 +207,7 @@
     "select-display-language": "Selecionar idioma de exibição",
     "select-today": "Selecione hoje",
     "there-are-unsaved-changes": "Há alterações não salvas. Navegar para outra página fará com que essas alterações sejam perdidas.",
+    "toggle-all": "",
     "update": "Atualização",
     "updated-at": "Atualizado em",
     "username": "Nome do usuário",
@@ -632,14 +633,11 @@
     "add-countries-to-zone": "Adicionar paises para { zoneName }",
     "add-countries-to-zone-success": "Adicionado { countryCount } {countryCount, plural, one {country} other {countries}} para zona \"{ zoneName }\"",
     "add-products-to-test-order": "Adicionar produdos para o pedido de teste",
-    "administrator": "Administrador",
-    "catalog": "Catálogo",
     "channel": "Canal",
     "channel-token": "Token do canal",
     "confirm-delete-role": "Exluir regra?",
     "confirm-delete-tax-category": "Excluir categoria de imposto?",
     "confirm-delete-tax-rate": "Excluir taxa de imposto?",
-    "create": "Criar",
     "create-new-channel": "Criar novo canal",
     "create-new-country": "Criar novo país",
     "create-new-role": "Criar nova regra",
@@ -649,11 +647,9 @@
     "create-new-zone": "Criar nova zona",
     "create-zone": "Criar zona",
     "currency": "Moeda",
-    "customer": "Cliente",
     "default-role-label": "Esta é uma regra padrão e não pode ser modificada",
     "default-shipping-zone": "Zona de envio padrão",
     "default-tax-zone": "Zona de imposto padrão",
-    "delete": "Excluir",
     "eligible": "Elegível",
     "email-address": "Email",
     "filter-by-member-name": "Filtrar por país",
@@ -662,21 +658,16 @@
     "global-out-of-stock-threshold-tooltip": "",
     "last-name": "Sobrenome",
     "no-eligible-shipping-methods": "Nenhum método de envio qualificado",
-    "order": "Pedido",
     "password": "Senha",
     "payment-method-config-options": "Configuração do método de pagamento",
     "permissions": "Permissões",
     "prices-include-tax": "Os preços incluem impostos para a Zona padrão",
-    "promotion": "Promoção",
     "rate": "Taxa",
-    "read": "Ler",
     "remove-countries-from-zone-success": "Excluído { countryCount } {countryCount, plural, one {country} other {countries}} da zona \"{ zoneName }\"",
     "remove-from-zone": "Excluir da zona",
     "roles": "Regras",
     "search-by-product-name-or-sku": "Pesquisa por nome do produto ou SKU",
     "search-country-by-name": "Pesquisa países por nome",
-    "section": "Seção",
-    "settings": "Configurações",
     "shipping-calculator": "Calculadora de envio",
     "shipping-eligibility-checker": "Verificador de elegibilidade para envio",
     "shipping-method": "Método de envio",
@@ -688,7 +679,6 @@
     "test-shipping-method": "Método de envio de teste",
     "test-shipping-methods": "Métodos de envio de teste",
     "track-inventory-default": "Rastrear inventário por padrão",
-    "update": "Atualização",
     "update-zone": "Zona de atualização",
     "view-zone-members": "Visualizar membros",
     "zone": "Zona"

+ 1 - 11
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -207,6 +207,7 @@
     "select-display-language": "选择显示语言",
     "select-today": "选择今天",
     "there-are-unsaved-changes": "修改尚未被保存,现在离开会导致您的修改会被删除",
+    "toggle-all": "",
     "update": "确认修改",
     "updated-at": "修改时间",
     "username": "用户名",
@@ -632,14 +633,11 @@
     "add-countries-to-zone": "添加国家到销售区域...",
     "add-countries-to-zone-success": "{ countryCount }个国家已到销售区域 \"{ zoneName }\"",
     "add-products-to-test-order": "添加产品到测试订单",
-    "administrator": "用户管理",
-    "catalog": "商品管理",
     "channel": "销售渠道",
     "channel-token": "渠道唯一码",
     "confirm-delete-role": "确认删除角色?",
     "confirm-delete-tax-category": "确认删除税表分类?",
     "confirm-delete-tax-rate": "去人删除税率?",
-    "create": "添加",
     "create-new-channel": "添加销售渠道",
     "create-new-country": "添加国家",
     "create-new-role": "添加角色",
@@ -649,11 +647,9 @@
     "create-new-zone": "",
     "create-zone": "创建销售区域",
     "currency": "币种",
-    "customer": "客户管理",
     "default-role-label": "默认角色不可修改",
     "default-shipping-zone": "默认配送区域",
     "default-tax-zone": "默认销售区域",
-    "delete": "删除",
     "eligible": "符合条件",
     "email-address": "电子邮件",
     "filter-by-member-name": "",
@@ -662,21 +658,16 @@
     "global-out-of-stock-threshold-tooltip": "",
     "last-name": "姓",
     "no-eligible-shipping-methods": "没有符合条件的配送方式",
-    "order": "订单管理",
     "password": "密码",
     "payment-method-config-options": "支付方式选项配置",
     "permissions": "权限",
     "prices-include-tax": "设置默认销售区域价格含税",
-    "promotion": "优惠券管理",
     "rate": "税率",
-    "read": "只读",
     "remove-countries-from-zone-success": "{ countryCount }个国际已从\"{ zoneName }\"中移除",
     "remove-from-zone": "",
     "roles": "角色列表",
     "search-by-product-name-or-sku": "输入要搜索的产品名称或库存编码",
     "search-country-by-name": "输入要搜索的国家名称",
-    "section": "操作项目",
-    "settings": "系统设置",
     "shipping-calculator": "配送费计算",
     "shipping-eligibility-checker": "使用此配送方式的合格条件",
     "shipping-method": "配送方式",
@@ -688,7 +679,6 @@
     "test-shipping-method": "模拟测试配送方式",
     "test-shipping-methods": "模拟测试配送方式",
     "track-inventory-default": "默认跟踪库存",
-    "update": "修改",
     "update-zone": "",
     "view-zone-members": "",
     "zone": "销售区域"

+ 1 - 11
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -207,6 +207,7 @@
     "select-display-language": "選擇顯示語言",
     "select-today": "選擇今天",
     "there-are-unsaved-changes": "變更尚未被儲存,離開會失去所有變更",
+    "toggle-all": "",
     "update": "確認修改",
     "updated-at": "修改時間",
     "username": "用户名",
@@ -632,14 +633,11 @@
     "add-countries-to-zone": "新增國家到銷售區域...",
     "add-countries-to-zone-success": "{ countryCount }個國家已到銷售區域 \"{ zoneName }\"",
     "add-products-to-test-order": "新增產品到測試訂單",
-    "administrator": "用户管理",
-    "catalog": "商品管理",
     "channel": "渠道",
     "channel-token": "渠道唯一碼",
     "confirm-delete-role": "確認移除角色?",
     "confirm-delete-tax-category": "確認移除税表分類?",
     "confirm-delete-tax-rate": "確認移除税率?",
-    "create": "新增",
     "create-new-channel": "新增渠道",
     "create-new-country": "新增國家",
     "create-new-role": "新增角色",
@@ -649,11 +647,9 @@
     "create-new-zone": "",
     "create-zone": "建立銷售區域",
     "currency": "幣種",
-    "customer": "客户管理",
     "default-role-label": "默認角色不可修改",
     "default-shipping-zone": "默認配送區域",
     "default-tax-zone": "默認銷售區域",
-    "delete": "移除",
     "eligible": "符合條件",
     "email-address": "電子郵件",
     "filter-by-member-name": "",
@@ -662,21 +658,16 @@
     "global-out-of-stock-threshold-tooltip": "",
     "last-name": "姓",
     "no-eligible-shipping-methods": "没有符合條件的配送方式",
-    "order": "訂單管理",
     "password": "密碼",
     "payment-method-config-options": "支付方式選項配置",
     "permissions": "權限",
     "prices-include-tax": "設定默認銷售區域價格連税",
-    "promotion": "優惠券管理",
     "rate": "税率",
-    "read": "只讀",
     "remove-countries-from-zone-success": "{ countryCount }個國際已從\"{ zoneName }\"中移除",
     "remove-from-zone": "",
     "roles": "角色列表",
     "search-by-product-name-or-sku": "輸入要搜索的產品名稱或庫存編碼",
     "search-country-by-name": "輸入要搜索的國家名稱",
-    "section": "操作項目",
-    "settings": "系统設定",
     "shipping-calculator": "配送費計算",
     "shipping-eligibility-checker": "使用此配送方式的合格條件",
     "shipping-method": "配送方式",
@@ -688,7 +679,6 @@
     "test-shipping-method": "模擬測試配送方式",
     "test-shipping-methods": "模擬測試配送方式",
     "track-inventory-default": "默認跟踪庫存",
-    "update": "修改",
     "update-zone": "",
     "view-zone-members": "",
     "zone": "銷售區域"

+ 7 - 0
packages/dev-server/dev-config.ts

@@ -8,6 +8,7 @@ import {
     DefaultSearchPlugin,
     examplePaymentHandler,
     LogLevel,
+    PermissionDefinition,
     VendureConfig,
 } from '@vendure/core';
 import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
@@ -40,6 +41,12 @@ export const devConfig: VendureConfig = {
         tokenMethod: 'cookie',
         sessionSecret: 'some-secret',
         requireVerification: true,
+        customPermissions: [
+            new PermissionDefinition({
+                name: 'SyncInventory',
+                description: 'Allows external tools to sync stock levels',
+            }),
+        ],
     },
     dbConnectionOptions: {
         synchronize: false,

Some files were not shown because too many files changed in this diff