Browse Source

feat(admin-ui): Add permissions checks for Product list/detail views

Relates to #94
Michael Bromley 6 years ago
parent
commit
75dc385774

+ 12 - 3
packages/admin-ui/src/app/catalog/components/product-assets/product-assets.component.html

@@ -13,7 +13,7 @@
         </div>
     </div>
     <div class="card-block"><ng-container *ngTemplateOutlet="assetList"></ng-container></div>
-    <div class="card-footer">
+    <div class="card-footer" *vdrIfPermissions="'UpdateCatalog'">
         <button class="btn" (click)="selectAssets()">
             <clr-icon shape="attachment"></clr-icon>
             {{ 'catalog.add-asset' | translate }}
@@ -33,6 +33,7 @@
     </div>
     <ng-container *ngTemplateOutlet="assetList"></ng-container>
     <button
+        *vdrIfPermissions="'UpdateCatalog'"
         class="compact-select btn btn-icon btn-sm btn-block"
         [title]="'catalog.add-asset' | translate"
         (click)="selectAssets()"
@@ -46,12 +47,14 @@
     <div class="all-assets" [class.compact]="compact" cdkDropListGroup>
         <div
             cdkDropList
+            [cdkDropListDisabled]="!('UpdateCatalog' | hasPermission)"
             [cdkDropListEnterPredicate]="dropListEnterPredicate"
             (cdkDropListDropped)="dropListDropped($event)"
         ></div>
         <div
             *ngFor="let asset of assets"
             cdkDropList
+            [cdkDropListDisabled]="!('UpdateCatalog' | hasPermission)"
             [cdkDropListEnterPredicate]="dropListEnterPredicate"
             (cdkDropListDropped)="dropListDropped($event)"
         >
@@ -71,14 +74,20 @@
                     </button>
                     <button
                         type="button"
-                        [disabled]="isFeatured(asset)"
+                        [disabled]="isFeatured(asset) || !('UpdateCatalog' | hasPermission)"
                         vdrDropdownItem
                         (click)="setAsFeatured(asset)"
                     >
                         {{ 'catalog.set-as-featured-asset' | translate }}
                     </button>
                     <div class="dropdown-divider"></div>
-                    <button type="button" class="remove-asset" vdrDropdownItem (click)="removeAsset(asset)">
+                    <button
+                        type="button"
+                        class="remove-asset"
+                        vdrDropdownItem
+                        [disabled]="!('UpdateCatalog' | hasPermission)"
+                        (click)="removeAsset(asset)"
+                    >
                         {{ 'catalog.remove-asset' | translate }}
                     </button>
                 </vdr-dropdown-menu>

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

@@ -1,6 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-left>
-        <clr-toggle-wrapper>
+        <clr-toggle-wrapper *vdrIfPermissions="'UpdateCatalog'">
             <input
                 type="checkbox"
                 clrToggle
@@ -28,6 +28,7 @@
         </button>
         <ng-template #updateButton>
             <button
+                *vdrIfPermissions="'UpdateCatalog'"
                 class="btn btn-primary"
                 (click)="save()"
                 [disabled]="
@@ -55,6 +56,7 @@
                                     id="name"
                                     type="text"
                                     formControlName="name"
+                                    [readonly]="!('UpdateCatalog' | hasPermission)"
                                     (input)="updateSlug($event.target.value)"
                                 />
                             </vdr-form-field>
@@ -63,10 +65,17 @@
                                 for="slug"
                                 [errors]="{ pattern: 'catalog.slug-pattern-error' | translate }"
                             >
-                                <input id="slug" type="text" formControlName="slug" pattern="[a-z0-9_-]+" />
+                                <input
+                                    id="slug"
+                                    type="text"
+                                    formControlName="slug"
+                                    [readonly]="!('UpdateCatalog' | hasPermission)"
+                                    pattern="[a-z0-9_-]+"
+                                />
                             </vdr-form-field>
                             <vdr-rich-text-editor
                                 formControlName="description"
+                                [readonly]="!('UpdateCatalog' | hasPermission)"
                                 [label]="'common.description' | translate"
                             ></vdr-rich-text-editor>
 
@@ -77,6 +86,7 @@
                                         *ngIf="customFieldIsSet(customField.name)"
                                         [customFieldsFormGroup]="detailForm.get(['product', 'customFields'])"
                                         [customField]="customField"
+                                        [readonly]="!('UpdateCatalog' | hasPermission)"
                                     ></vdr-custom-field-control>
                                 </ng-container>
                             </section>
@@ -85,9 +95,12 @@
                                 <vdr-facet-value-chip
                                     *ngFor="let facetValue of facetValues$ | async"
                                     [facetValue]="facetValue"
+                                    [removable]="'UpdateCatalog' | hasPermission"
                                     (remove)="removeProductFacetValue(facetValue.id)"
                                 ></vdr-facet-value-chip>
-                                <button class="btn btn-sm btn-secondary" (click)="selectProductFacetValue()">
+                                <button class="btn btn-sm btn-secondary"
+                                        *vdrIfPermissions="'UpdateCatalog'"
+                                        (click)="selectProductFacetValue()">
                                     <clr-icon shape="plus"></clr-icon>
                                     {{ 'catalog.add-facets' | translate }}
                                 </button>
@@ -137,8 +150,11 @@
                             </button>
                         </div>
                         <div class="flex-spacer"></div>
-                        <a [routerLink]="['./', 'manage-variants']"
-                           class="btn btn-secondary btn-sm edit-variants-btn">
+                        <a
+                            *vdrIfPermissions="'UpdateCatalog'"
+                            [routerLink]="['./', 'manage-variants']"
+                            class="btn btn-secondary btn-sm edit-variants-btn"
+                        >
                             <clr-icon shape="add-text"></clr-icon>
                             {{ 'catalog.manage-variants' | translate }}
                         </a>

+ 8 - 2
packages/admin-ui/src/app/catalog/components/product-list/product-list.component.html

@@ -12,7 +12,12 @@
                     <clr-icon shape="cog"></clr-icon>
                 </button>
                 <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button type="button" vdrDropdownItem (click)="rebuildSearchIndex()">
+                    <button
+                        type="button"
+                        vdrDropdownItem
+                        (click)="rebuildSearchIndex()"
+                        [disabled]="!('UpdateCatalog' | hasPermission)"
+                    >
                         {{ 'catalog.rebuild-search-index' | translate }}
                     </button>
                 </vdr-dropdown-menu>
@@ -24,7 +29,7 @@
         </clr-checkbox-wrapper>
     </vdr-ab-left>
     <vdr-ab-right>
-        <a class="btn btn-primary" [routerLink]="['./create']">
+        <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateCatalog'">
             <clr-icon shape="plus"></clr-icon>
             <span class="full-label">{{ 'catalog.create-new-product' | translate }}</span>
         </a>
@@ -80,6 +85,7 @@
                         type="button"
                         class="delete-button"
                         (click)="deleteProduct(result.productId)"
+                        [disabled]="!('DeleteCatalog' | hasPermission)"
                         vdrDropdownItem
                     >
                         <clr-icon shape="trash" class="is-danger"></clr-icon>

+ 21 - 5
packages/admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.html

@@ -7,29 +7,31 @@
         <ng-container [formGroup]="formArray.at(i)">
             <div class="card-block header-row">
                 <div class="details">
-                    <vdr-title-input class="sku">
+                    <vdr-title-input class="sku" [readonly]="!('UpdateCatalog' | hasPermission)">
                         <clr-input-container>
                             <input
                                 clrInput
                                 type="text"
                                 formControlName="sku"
+                                [readonly]="!('UpdateCatalog' | hasPermission)"
                                 [placeholder]="'catalog.sku' | translate"
                             />
                         </clr-input-container>
                     </vdr-title-input>
-                    <vdr-title-input class="name">
+                    <vdr-title-input class="name" [readonly]="!('UpdateCatalog' | hasPermission)">
                         <clr-input-container>
                             <input
                                 clrInput
                                 type="text"
                                 formControlName="name"
+                                [readonly]="!('UpdateCatalog' | hasPermission)"
                                 [placeholder]="'common.name' | translate"
                             />
                         </clr-input-container>
                     </vdr-title-input>
                 </div>
                 <div class="right-controls">
-                    <clr-toggle-wrapper>
+                    <clr-toggle-wrapper *vdrIfPermissions="'UpdateCatalog'">
                         <input type="checkbox" clrToggle name="enabled" formControlName="enabled" />
                         <label>{{ 'common.enabled' | translate }}</label>
                     </clr-toggle-wrapper>
@@ -49,7 +51,7 @@
                         <div class="standard-fields">
                             <div class="variant-form-input-row">
                                 <div class="tax-category">
-                                    <clr-select-container>
+                                    <clr-select-container *vdrIfPermissions="'UpdateCatalog'; else taxCategoryLabel">
                                         <label>{{ 'catalog.tax-category' | translate }}</label>
                                         <select clrSelect name="options" formControlName="taxCategoryId">
                                             <option
@@ -60,6 +62,12 @@
                                             </option>
                                         </select>
                                     </clr-select-container>
+                                    <ng-template #taxCategoryLabel>
+                                        <label class="clr-control-label">{{ 'catalog.tax-category' | translate }}</label>
+                                        <div class="tax-category-label">
+                                            {{ getTaxCategoryName(i) }}
+                                        </div>
+                                    </ng-template>
                                 </div>
                                 <div class="price">
                                     <clr-input-container>
@@ -67,6 +75,7 @@
                                         <vdr-currency-input
                                             clrInput
                                             [currencyCode]="variant.currencyCode"
+                                            [readonly]="!('UpdateCatalog' | hasPermission)"
                                             formControlName="price"
                                         ></vdr-currency-input>
                                     </clr-input-container>
@@ -87,6 +96,7 @@
                                         min="0"
                                         step="1"
                                         formControlName="stockOnHand"
+                                        [readonly]="!('UpdateCatalog' | hasPermission)"
                                     />
                                 </clr-input-container>
                                 <clr-checkbox-wrapper class="track-inventory-toggle">
@@ -94,7 +104,9 @@
                                         type="checkbox"
                                         clrCheckbox
                                         name="trackInventory"
+
                                         formControlName="trackInventory"
+                                        [attr.disabled]="!('UpdateCatalog' | hasPermission)"
                                     />
                                     <label>{{ 'catalog.track-inventory' | translate }}</label>
                                 </clr-checkbox-wrapper>
@@ -109,6 +121,7 @@
                                             *ngIf="customFieldIsSet(i, customField.name)"
                                             [compact]="true"
                                             [customFieldsFormGroup]="formArray.at(i).get(['customFields'])"
+                                            [readonly]="!('UpdateCatalog' | hasPermission)"
                                             [customField]="customField"
                                         ></vdr-custom-field-control>
                                     </ng-container>
@@ -127,7 +140,7 @@
                                 [colorFrom]="optionGroupName(option.groupId)"
                                 [invert]="true"
                                 (iconClick)="editOption(option)"
-                                icon="pencil"
+                                [icon]="('UpdateCatalog' | hasPermission) && 'pencil'"
                             >
                                 <span class="option-group-name">{{ optionGroupName(option.groupId) }}</span>
                                 {{ option.name }}
@@ -139,14 +152,17 @@
                         <vdr-facet-value-chip
                             *ngFor="let facetValue of existingFacetValues(i)"
                             [facetValue]="facetValue"
+                            [removable]="'UpdateCatalog' | hasPermission"
                             (remove)="removeFacetValue(i, facetValue.id)"
                         ></vdr-facet-value-chip>
                         <vdr-facet-value-chip
                             *ngFor="let facetValue of pendingFacetValues(i)"
                             [facetValue]="facetValue"
+                            [removable]="'UpdateCatalog' | hasPermission"
                             (remove)="removeFacetValue(i, facetValue.id)"
                         ></vdr-facet-value-chip>
                         <button
+                            *vdrIfPermissions="'UpdateCatalog'"
                             class="btn btn-sm btn-secondary"
                             (click)="selectFacetValueClick.emit([variant.id])"
                         >

+ 4 - 0
packages/admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.scss

@@ -70,6 +70,10 @@
         display: flex;
     }
 
+    .tax-category-label {
+        margin-top: 3px;
+    }
+
     .variant-form-inputs {
         flex: 1;
         display: flex;

+ 9 - 0
packages/admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.ts

@@ -74,6 +74,15 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
         }
     }
 
+    getTaxCategoryName(index: number): string {
+        const control = this.formArray.at(index).get(['taxCategoryId']);
+        if (control && this.taxCategories) {
+            const match = this.taxCategories.find(t => t.id === control.value);
+            return match ? match.name : '';
+        }
+        return '';
+    }
+
     areAllSelected(): boolean {
         return !!this.variants && this.selectedVariantIds.length === this.variants.length;
     }

+ 18 - 2
packages/admin-ui/src/app/catalog/components/product-variants-table/product-variants-table.component.html

@@ -29,6 +29,7 @@
                         clrInput
                         type="text"
                         formControlName="name"
+                        [readonly]="!('UpdateCatalog' | hasPermission)"
                         [placeholder]="'common.name' | translate"
                     />
                 </clr-input-container>
@@ -39,6 +40,7 @@
                         clrInput
                         type="text"
                         formControlName="sku"
+                        [readonly]="!('UpdateCatalog' | hasPermission)"
                         [placeholder]="'catalog.sku' | translate"
                     />
                 </clr-input-container>
@@ -57,18 +59,32 @@
                     <vdr-currency-input
                         clrInput
                         [currencyCode]="variant.currencyCode"
+                        [readonly]="!('UpdateCatalog' | hasPermission)"
                         formControlName="price"
                     ></vdr-currency-input>
                 </clr-input-container>
             </td>
             <td class="left align-middle stock" [class.disabled]="!formArray.get([i, 'enabled']).value">
                 <clr-input-container>
-                    <input clrInput type="number" min="0" step="1" formControlName="stockOnHand" />
+                    <input
+                        clrInput
+                        type="number"
+                        min="0"
+                        step="1"
+                        formControlName="stockOnHand"
+                        [readonly]="!('UpdateCatalog' | hasPermission)"
+                    />
                 </clr-input-container>
             </td>
             <td class="left align-middle stock" [class.disabled]="!formArray.get([i, 'enabled']).value">
                 <clr-toggle-wrapper>
-                    <input type="checkbox" clrToggle name="enabled" formControlName="enabled" />
+                    <input
+                        type="checkbox"
+                        clrToggle
+                        name="enabled"
+                        formControlName="enabled"
+                        [attr.disabled]="!('UpdateCatalog' | hasPermission)"
+                    />
                 </clr-toggle-wrapper>
             </td>
         </ng-container>

+ 1 - 1
packages/admin-ui/src/app/shared/components/form-field/form-field.component.scss

@@ -33,7 +33,7 @@
         display: flex;
         ::ng-deep input {
             flex: 1;
-            &[readonly] {
+            &[disabled] {
                 background-color: $color-grey-200;
             }
         }

+ 1 - 1
packages/admin-ui/src/app/shared/components/title-input/title-input.component.html

@@ -1,4 +1,4 @@
 <ng-content></ng-content>
-<div class="edit-icon">
+<div class="edit-icon" *ngIf="!readonly">
     <clr-icon shape="edit"></clr-icon>
 </div>

+ 10 - 0
packages/admin-ui/src/app/shared/components/title-input/title-input.component.scss

@@ -36,4 +36,14 @@
         }
     }
 
+    &.readonly {
+        .edit-icon {
+            display: none;
+        }
+        &:hover {
+            ::ng-deep input:not(:focus) {
+                background-color: $color-grey-200 !important;
+            }
+        }
+    }
 }

+ 6 - 2
packages/admin-ui/src/app/shared/components/title-input/title-input.component.ts

@@ -1,4 +1,4 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core';
 
 @Component({
     selector: 'vdr-title-input',
@@ -6,4 +6,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
     styleUrls: ['./title-input.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class TitleInputComponent {}
+export class TitleInputComponent {
+    @HostBinding('class.readonly')
+    @Input()
+    readonly = false;
+}

+ 52 - 0
packages/admin-ui/src/app/shared/pipes/has-permission.pipe.ts

@@ -0,0 +1,52 @@
+import { OnDestroy, Pipe, PipeTransform } from '@angular/core';
+import { DataService } from '@vendure/admin-ui/src/app/data/providers/data.service';
+import { Observable, Subscription } from 'rxjs';
+
+/**
+ * A pipe which checks the provided permission against all the permissions of the current user.
+ * Returns `true` if the current user has that permission.
+ *
+ * @example
+ * ```
+ * <button [disabled]="!('UpdateCatalog' | hasPermission)">Save Changes</button>
+ * ```
+ */
+@Pipe({
+    name: 'hasPermission',
+    pure: false,
+})
+export class HasPermissionPipe implements PipeTransform, OnDestroy {
+    private hasPermission = false;
+    private currentPermissions$: Observable<string[]>;
+    private permission: string | null = null;
+    private subscription: Subscription;
+
+    constructor(private dataService: DataService) {
+        this.currentPermissions$ = this.dataService.client
+            .userStatus()
+            .mapStream(data => data.userStatus.permissions);
+    }
+
+    transform(permission: string): any {
+        if (this.permission !== permission) {
+            this.permission = permission;
+            this.hasPermission = false;
+            this.dispose();
+            this.subscription = this.currentPermissions$.subscribe(permissions => {
+                this.hasPermission = permissions.includes(permission);
+            });
+        }
+
+        return this.hasPermission;
+    }
+
+    ngOnDestroy() {
+        this.dispose();
+    }
+
+    private dispose() {
+        if (this.subscription) {
+            this.subscription.unsubscribe();
+        }
+    }
+}

+ 2 - 0
packages/admin-ui/src/app/shared/shared.module.ts

@@ -14,6 +14,7 @@ import {
     ActionBarRightComponent,
 } from './components/action-bar/action-bar.component';
 import { IfPermissionsDirective } from './directives/if-permissions.directive';
+import { HasPermissionPipe } from './pipes/has-permission.pipe';
 import { ModalService } from './providers/modal/modal.service';
 import { CanDeactivateDetailGuard } from './providers/routing/can-deactivate-detail-guard';
 import { AffixedInputComponent } from './shared-declarations';
@@ -112,6 +113,7 @@ const DECLARATIONS = [
     StringToColorPipe,
     ObjectTreeComponent,
     IfPermissionsDirective,
+    HasPermissionPipe,
 ];
 
 @NgModule({