Browse Source

feat(admin-ui): Implement reordering, resize, add, remove of widgets

Relates to #334
Michael Bromley 5 years ago
parent
commit
9a52bdf753

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

@@ -1,46 +1,46 @@
 {
-  "generatedOn": "2020-11-20T08:36:18.659Z",
-  "lastCommit": "73ab736f93c07f8e4580587ddc7aacd97a5769b2",
+  "generatedOn": "2020-11-20T14:39:19.815Z",
+  "lastCommit": "60fd856bbcf3389d1b488e4f3e51a4eeaca85370",
   "translationStatus": {
     "cs": {
-      "tokenCount": 695,
+      "tokenCount": 703,
       "translatedCount": 688,
-      "percentage": 99
+      "percentage": 98
     },
     "de": {
-      "tokenCount": 695,
+      "tokenCount": 703,
       "translatedCount": 597,
-      "percentage": 86
+      "percentage": 85
     },
     "en": {
-      "tokenCount": 695,
-      "translatedCount": 693,
-      "percentage": 100
+      "tokenCount": 703,
+      "translatedCount": 699,
+      "percentage": 99
     },
     "es": {
-      "tokenCount": 695,
+      "tokenCount": 703,
       "translatedCount": 455,
       "percentage": 65
     },
     "pl": {
-      "tokenCount": 695,
+      "tokenCount": 703,
       "translatedCount": 552,
       "percentage": 79
     },
     "pt_BR": {
-      "tokenCount": 695,
+      "tokenCount": 703,
       "translatedCount": 643,
-      "percentage": 93
+      "percentage": 91
     },
     "zh_Hans": {
-      "tokenCount": 695,
+      "tokenCount": 703,
       "translatedCount": 536,
-      "percentage": 77
+      "percentage": 76
     },
     "zh_Hant": {
-      "tokenCount": 695,
+      "tokenCount": 703,
       "translatedCount": 536,
-      "percentage": 77
+      "percentage": 76
     }
   }
 }

+ 3 - 1
packages/admin-ui/src/lib/core/src/providers/dashboard-widget/dashboard-widget-types.ts

@@ -33,4 +33,6 @@ export interface DashboardWidgetConfig {
 }
 
 export type WidgetLayoutDefinition = Array<{ id: string; width: DashboardWidgetWidth }>;
-export type WidgetLayout = Array<Array<{ config: DashboardWidgetConfig; width: DashboardWidgetWidth }>>;
+export type WidgetLayout = Array<
+    Array<{ id: string; config: DashboardWidgetConfig; width: DashboardWidgetWidth }>
+>;

+ 16 - 5
packages/admin-ui/src/lib/core/src/providers/dashboard-widget/dashboard-widget.service.ts

@@ -26,6 +26,17 @@ export class DashboardWidgetService {
         this.registry.set(id, config);
     }
 
+    getAvailableIds(): string[] {
+        return [...this.registry.keys()];
+    }
+
+    getWidgetById(id: string) {
+        if (!this.registry.has(id)) {
+            throw new Error(`No widget was found with the id "${id}"`);
+        }
+        return this.registry.get(id);
+    }
+
     setDefaultLayout(layout: WidgetLayoutDefinition) {
         this.layoutDef = layout;
     }
@@ -34,14 +45,14 @@ export class DashboardWidgetService {
         return this.layoutDef;
     }
 
-    getWidgetLayout(): WidgetLayout {
-        const intermediateLayout = this.layoutDef
+    getWidgetLayout(layoutDef?: WidgetLayoutDefinition): WidgetLayout {
+        const intermediateLayout = (layoutDef || this.layoutDef)
             .map(({ id, width }) => {
                 const config = this.registry.get(id);
                 if (!config) {
                     return this.idNotFound(id);
                 }
-                return { config, width: this.getValidWidth(id, config, width) };
+                return { id, config, width: this.getValidWidth(id, config, width) };
             })
             .filter(notNullOrUndefined);
 
@@ -85,13 +96,13 @@ export class DashboardWidgetService {
     private buildLayout(intermediateLayout: WidgetLayout[number]): WidgetLayout {
         const layout: WidgetLayout = [];
         let row: WidgetLayout[number] = [];
-        for (const { config, width } of intermediateLayout) {
+        for (const { id, config, width } of intermediateLayout) {
             const rowSize = row.reduce((size, c) => size + c.width, 0);
             if (12 < rowSize + width) {
                 layout.push(row);
                 row = [];
             }
-            row.push({ config, width });
+            row.push({ id, config, width });
         }
         layout.push(row);
         return layout;

+ 7 - 2
packages/admin-ui/src/lib/dashboard/src/components/dashboard-widget/dashboard-widget.component.html

@@ -1,6 +1,11 @@
 <div class="card">
-    <div class="card-header" *ngIf="widgetConfig.title as title">
-        {{ title | translate }}
+    <div class="card-header">
+        <div class="title">
+            <ng-container *ngIf="widgetConfig.title as title">{{ title | translate }}</ng-container>
+        </div>
+        <div class="controls">
+            <ng-content></ng-content>
+        </div>
     </div>
     <div class="card-block">
         <ng-template #portal></ng-template>

+ 5 - 0
packages/admin-ui/src/lib/dashboard/src/components/dashboard-widget/dashboard-widget.component.scss

@@ -7,3 +7,8 @@
 .card {
     margin-top: 0;
 }
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+}

+ 67 - 3
packages/admin-ui/src/lib/dashboard/src/components/dashboard/dashboard.component.html

@@ -1,5 +1,69 @@
-<div class="clr-row dashboard-row" *ngFor="let row of widgetLayout">
-    <div *ngFor="let widget of row" [ngClass]="getClassForWidth(widget.width)">
-        <vdr-dashboard-widget *vdrIfPermissions="widget.config.requiresPermissions || null" [widgetConfig]="widget.config"></vdr-dashboard-widget>
+<div class="widget-header">
+    <vdr-dropdown>
+        <button class="btn btn-secondary btn-sm" vdrDropdownTrigger>
+            <clr-icon shape="plus"></clr-icon>
+            {{ 'dashboard.add-widget' | translate }}
+        </button>
+        <vdr-dropdown-menu vdrPosition="bottom-right">
+            <button
+                class="button"
+                vdrDropdownItem
+                *ngFor="let id of availableWidgetIds"
+                (click)="addWidget(id)"
+            >
+                {{ id }}
+            </button>
+        </vdr-dropdown-menu>
+    </vdr-dropdown>
+</div>
+<div cdkDropListGroup>
+    <div
+        class="clr-row dashboard-row"
+        *ngFor="let row of widgetLayout; index as rowIndex"
+        cdkDropList
+        (cdkDropListDropped)="drop($event)"
+        cdkDropListOrientation="horizontal"
+        [cdkDropListData]="{ index: rowIndex }"
+    >
+        <div
+            *ngFor="let widget of row; trackBy: trackRowItem"
+            class="dashboard-item"
+            [ngClass]="getClassForWidth(widget.width)"
+            cdkDrag
+            [cdkDragData]="widget"
+        >
+            <vdr-dashboard-widget
+                *vdrIfPermissions="widget.config.requiresPermissions || null"
+                [widgetConfig]="widget.config"
+            >
+                <div class="flex">
+                    <div class="drag-handle" cdkDragHandle>
+                        <clr-icon shape="drag-handle" size="24"></clr-icon>
+                    </div>
+                    <vdr-dropdown>
+                        <button class="icon-button" vdrDropdownTrigger>
+                            <clr-icon shape="ellipsis-vertical"></clr-icon>
+                        </button>
+                        <vdr-dropdown-menu vdrPosition="bottom-right">
+                            <h4 class="dropdown-header">{{ 'dashboard.widget-resize' | translate }}</h4>
+                            <button
+                                class="button"
+                                vdrDropdownItem
+                                [disabled]="width === widget.width"
+                                *ngFor="let width of getSupportedWidths(widget.config)"
+                                (click)="setWidgetWidth(widget, width)"
+                            >
+                                {{ 'dashboard.widget-width' | translate: { width: width } }}
+                            </button>
+                            <div class="dropdown-divider" role="separator"></div>
+                            <button class="button" vdrDropdownItem (click)="removeWidget(widget)">
+                                <clr-icon shape="trash" class="is-danger"></clr-icon>
+                                {{ 'dashboard.remove-widget' | translate }}
+                            </button>
+                        </vdr-dropdown-menu>
+                    </vdr-dropdown>
+                </div>
+            </vdr-dashboard-widget>
+        </div>
     </div>
 </div>

+ 34 - 1
packages/admin-ui/src/lib/dashboard/src/components/dashboard/dashboard.component.scss

@@ -1,4 +1,9 @@
-@import "variables";
+@import 'variables';
+
+.widget-header {
+    display: flex;
+    justify-content: flex-end;
+}
 
 .placeholder {
     color: $color-grey-300;
@@ -16,3 +21,31 @@
 vdr-dashboard-widget {
     margin-bottom: 24px;
 }
+
+.cdk-drag-preview {
+    box-sizing: border-box;
+    border-radius: 4px;
+}
+
+.cdk-drag-placeholder {
+    opacity: 0;
+}
+
+.cdk-drag-animating {
+    transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
+}
+
+.dashboard-row {
+    padding: 0;
+    border-width: 1;
+    margin-bottom: 6px;
+    transition: padding 0.2s, margin 0.2s;
+}
+
+.dashboard-row.cdk-drop-list-dragging, .dashboard-row.cdk-drop-list-receiving  {
+    border: 1px dashed $color-grey-300;
+    padding: 6px;
+}
+.dashboard-row.cdk-drop-list-dragging .dashboard-item:not(.cdk-drag-placeholder) {
+    transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
+}

+ 70 - 3
packages/admin-ui/src/lib/dashboard/src/components/dashboard/dashboard.component.ts

@@ -1,9 +1,14 @@
+import { CdkDragDrop } from '@angular/cdk/drag-drop';
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
-import { DashboardWidgetConfig, DashboardWidgetService, DashboardWidgetWidth } from '@vendure/admin-ui/core';
+import {
+    DashboardWidgetConfig,
+    DashboardWidgetService,
+    DashboardWidgetWidth,
+    WidgetLayout,
+    WidgetLayoutDefinition,
+} from '@vendure/admin-ui/core';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 
-type WidgetLayout = Array<Array<{ width: DashboardWidgetWidth; config: DashboardWidgetConfig }>>;
-
 @Component({
     selector: 'vdr-dashboard',
     templateUrl: './dashboard.component.html',
@@ -12,11 +17,14 @@ type WidgetLayout = Array<Array<{ width: DashboardWidgetWidth; config: Dashboard
 })
 export class DashboardComponent implements OnInit {
     widgetLayout: WidgetLayout;
+    availableWidgetIds: string[];
+    private readonly deletionMarker = '__delete__';
 
     constructor(private dashboardWidgetService: DashboardWidgetService) {}
 
     ngOnInit() {
         this.widgetLayout = this.dashboardWidgetService.getWidgetLayout();
+        this.availableWidgetIds = this.dashboardWidgetService.getAvailableIds();
     }
 
     getClassForWidth(width: DashboardWidgetWidth): string {
@@ -35,4 +43,63 @@ export class DashboardComponent implements OnInit {
                 assertNever(width);
         }
     }
+
+    getSupportedWidths(config: DashboardWidgetConfig): DashboardWidgetWidth[] {
+        return config.supportedWidths || [3, 4, 6, 8, 12];
+    }
+
+    setWidgetWidth(widget: WidgetLayout[number][number], width: DashboardWidgetWidth) {
+        widget.width = width;
+        this.recalculateLayout();
+    }
+
+    trackRowItem(index: number, item: WidgetLayout[number][number]) {
+        return item.config;
+    }
+
+    addWidget(id: string) {
+        const config = this.dashboardWidgetService.getWidgetById(id);
+        if (config) {
+            const width = this.getSupportedWidths(config)[0];
+            const widget: WidgetLayout[number][number] = {
+                id,
+                config,
+                width,
+            };
+            let targetRow: WidgetLayout[number];
+            if (this.widgetLayout.length) {
+                targetRow = this.widgetLayout[this.widgetLayout.length - 1];
+            } else {
+                targetRow = [];
+                this.widgetLayout.push(targetRow);
+            }
+            targetRow.push(widget);
+            this.recalculateLayout();
+        }
+    }
+
+    removeWidget(widget: WidgetLayout[number][number]) {
+        widget.id = this.deletionMarker;
+        this.recalculateLayout();
+    }
+
+    drop(event: CdkDragDrop<{ index: number }>) {
+        const previousLayoutRow = this.widgetLayout[event.previousContainer.data.index];
+        const newLayoutRow = this.widgetLayout[event.container.data.index];
+
+        previousLayoutRow.splice(event.previousIndex, 1);
+        newLayoutRow.splice(event.currentIndex, 0, event.item.data);
+        this.recalculateLayout();
+    }
+
+    private recalculateLayout() {
+        const flattened = this.widgetLayout
+            .reduce((flat, row) => [...flat, ...row], [])
+            .filter(item => item.id !== this.deletionMarker);
+        const newLayoutDef: WidgetLayoutDefinition = flattened.map(item => ({
+            id: item.id,
+            width: item.width,
+        }));
+        this.widgetLayout = this.dashboardWidgetService.getWidgetLayout(newLayoutDef);
+    }
 }

+ 3 - 0
packages/admin-ui/src/lib/dashboard/src/default-widgets.ts

@@ -16,6 +16,9 @@ export const DEFAULT_DASHBOARD_WIDGET_LAYOUT: WidgetLayoutDefinition = [
     { id: 'welcome', width: 12 },
     { id: 'orderSummary', width: 6 },
     { id: 'latestOrders', width: 6 },
+    { id: 'testWidget', width: 4 },
+    { id: 'testWidget', width: 4 },
+    { id: 'testWidget', width: 4 },
 ];
 
 export const DEFAULT_WIDGETS: { [id: string]: DashboardWidgetConfig } = {

+ 5 - 1
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -288,10 +288,14 @@
     "view-group-members": "Zobrazit členy skupiny"
   },
   "dashboard": {
+    "add-widget": "",
     "latest-orders": "",
     "orders-summary": "",
+    "remove-widget": "",
     "total-order-value": "",
-    "total-orders": ""
+    "total-orders": "",
+    "widget-resize": "",
+    "widget-width": ""
   },
   "datetime": {
     "ago-days": "před {count, plural, one {1 dnem} other {{count} dny}}",

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

@@ -288,10 +288,14 @@
     "view-group-members": ""
   },
   "dashboard": {
+    "add-widget": "",
     "latest-orders": "",
     "orders-summary": "",
+    "remove-widget": "",
     "total-order-value": "",
-    "total-orders": ""
+    "total-orders": "",
+    "widget-resize": "",
+    "widget-width": ""
   },
   "datetime": {
     "ago-days": "{count, plural, one {Vor einem Tag} other {Vor {count} Tagen}}",

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

@@ -288,10 +288,14 @@
     "view-group-members": "View group members"
   },
   "dashboard": {
+    "add-widget": "Add widget",
     "latest-orders": "Latest orders",
     "orders-summary": "Orders summary",
+    "remove-widget": "Remove widget",
     "total-order-value": "Total value",
-    "total-orders": "Total orders"
+    "total-orders": "Total orders",
+    "widget-resize": "Resize",
+    "widget-width": "Width: {width}"
   },
   "datetime": {
     "ago-days": "{count, plural, one {1 day} other {{count} days}} ago",

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

@@ -288,10 +288,14 @@
     "view-group-members": ""
   },
   "dashboard": {
+    "add-widget": "",
     "latest-orders": "",
     "orders-summary": "",
+    "remove-widget": "",
     "total-order-value": "",
-    "total-orders": ""
+    "total-orders": "",
+    "widget-resize": "",
+    "widget-width": ""
   },
   "datetime": {
     "ago-days": "",

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

@@ -288,10 +288,14 @@
     "view-group-members": ""
   },
   "dashboard": {
+    "add-widget": "",
     "latest-orders": "",
     "orders-summary": "",
+    "remove-widget": "",
     "total-order-value": "",
-    "total-orders": ""
+    "total-orders": "",
+    "widget-resize": "",
+    "widget-width": ""
   },
   "datetime": {
     "ago-days": "{count, plural, one {1 dzień} other {{count} dni}} temu",

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

@@ -288,10 +288,14 @@
     "view-group-members": "Visualizar membros do grupo"
   },
   "dashboard": {
+    "add-widget": "",
     "latest-orders": "",
     "orders-summary": "",
+    "remove-widget": "",
     "total-order-value": "",
-    "total-orders": ""
+    "total-orders": "",
+    "widget-resize": "",
+    "widget-width": ""
   },
   "datetime": {
     "ago-days": "{count, plural, one {1 day} other {{count} days}} atrás",

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

@@ -288,10 +288,14 @@
     "view-group-members": ""
   },
   "dashboard": {
+    "add-widget": "",
     "latest-orders": "",
     "orders-summary": "",
+    "remove-widget": "",
     "total-order-value": "",
-    "total-orders": ""
+    "total-orders": "",
+    "widget-resize": "",
+    "widget-width": ""
   },
   "datetime": {
     "ago-days": "",

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

@@ -288,10 +288,14 @@
     "view-group-members": ""
   },
   "dashboard": {
+    "add-widget": "",
     "latest-orders": "",
     "orders-summary": "",
+    "remove-widget": "",
     "total-order-value": "",
-    "total-orders": ""
+    "total-orders": "",
+    "widget-resize": "",
+    "widget-width": ""
   },
   "datetime": {
     "ago-days": "",