Просмотр исходного кода

feat(admin-ui): Implement new Zone & CustomerGroup list views

Michael Bromley 2 лет назад
Родитель
Сommit
39e2204618
34 измененных файлов с 858 добавлено и 419 удалено
  1. 71 3
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 1 0
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  3. 9 0
      packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts
  4. 23 11
      packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts
  5. 8 0
      packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts
  6. 18 4
      packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts
  7. 5 0
      packages/admin-ui/src/lib/core/src/providers/bulk-action-registry/bulk-action-types.ts
  8. 3 2
      packages/admin-ui/src/lib/core/src/shared/components/bulk-action-menu/bulk-action-menu.component.html
  9. 2 2
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.html
  10. 5 1
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.scss
  11. 1 0
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.ts
  12. 27 0
      packages/admin-ui/src/lib/core/src/shared/components/split-view/split-view.component.html
  13. 104 0
      packages/admin-ui/src/lib/core/src/shared/components/split-view/split-view.component.scss
  14. 72 0
      packages/admin-ui/src/lib/core/src/shared/components/split-view/split-view.component.ts
  15. 14 0
      packages/admin-ui/src/lib/core/src/shared/components/split-view/split-view.directive.ts
  16. 16 8
      packages/admin-ui/src/lib/core/src/shared/shared.module.ts
  17. 2 3
      packages/admin-ui/src/lib/customer/src/components/add-customer-to-group-dialog/add-customer-to-group-dialog.component.html
  18. 12 0
      packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list-bulk-actions.ts
  19. 0 56
      packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.scss
  20. 39 0
      packages/admin-ui/src/lib/customer/src/components/customer-group-member-list/customer-group-member-list-bulk-actions.ts
  21. 4 0
      packages/admin-ui/src/lib/customer/src/customer.module.ts
  22. 2 2
      packages/admin-ui/src/lib/settings/src/components/add-country-to-zone-dialog/add-country-to-zone-dialog.component.ts
  23. 4 3
      packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts
  24. 3 2
      packages/admin-ui/src/lib/settings/src/components/seller-detail/seller-detail.component.ts
  25. 4 3
      packages/admin-ui/src/lib/settings/src/components/tax-rate-detail/tax-rate-detail.component.ts
  26. 11 0
      packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list-bulk-actions.ts
  27. 102 135
      packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.html
  28. 74 71
      packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.ts
  29. 28 0
      packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list-bulk-actions.ts
  30. 52 38
      packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.html
  31. 61 33
      packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.ts
  32. 4 0
      packages/admin-ui/src/lib/settings/src/settings.module.ts
  33. 31 0
      packages/admin-ui/src/lib/static/styles/theme/dark.scss
  34. 46 42
      packages/admin-ui/src/lib/static/styles/theme/default.scss

+ 71 - 3
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -2616,6 +2616,8 @@ export type Mutation = {
   deleteCustomerAddress: Success;
   /** Delete a CustomerGroup */
   deleteCustomerGroup: DeletionResponse;
+  /** Delete multiple CustomerGroups */
+  deleteCustomerGroups: Array<DeletionResponse>;
   deleteCustomerNote: DeletionResponse;
   /** Deletes Customers */
   deleteCustomers: Array<DeletionResponse>;
@@ -2671,6 +2673,8 @@ export type Mutation = {
   deleteTaxRates: Array<DeletionResponse>;
   /** Delete a Zone */
   deleteZone: DeletionResponse;
+  /** Delete a Zone */
+  deleteZones: Array<DeletionResponse>;
   flushBufferedJobs: Success;
   importProducts?: Maybe<ImportInfo>;
   /** Authenticates the user using the native authentication strategy. This mutation is an alias for `authenticate({ native: { ... }})` */
@@ -3097,6 +3101,11 @@ export type MutationDeleteCustomerGroupArgs = {
 };
 
 
+export type MutationDeleteCustomerGroupsArgs = {
+  ids: Array<Scalars['ID']>;
+};
+
+
 export type MutationDeleteCustomerNoteArgs = {
   id: Scalars['ID'];
 };
@@ -3252,6 +3261,11 @@ export type MutationDeleteZoneArgs = {
 };
 
 
+export type MutationDeleteZonesArgs = {
+  ids: Array<Scalars['ID']>;
+};
+
+
 export type MutationFlushBufferedJobsArgs = {
   bufferIds?: InputMaybe<Array<Scalars['String']>>;
 };
@@ -4867,7 +4881,7 @@ export type Query = {
   uiState: UiState;
   userStatus: UserStatus;
   zone?: Maybe<Zone>;
-  zones: Array<Zone>;
+  zones: ZoneList;
 };
 
 
@@ -5149,6 +5163,11 @@ export type QueryZoneArgs = {
   id: Scalars['ID'];
 };
 
+
+export type QueryZonesArgs = {
+  options?: InputMaybe<ZoneListOptions>;
+};
+
 export type Refund = Node & {
   __typename?: 'Refund';
   adjustment: Scalars['Money'];
@@ -6315,6 +6334,39 @@ export type Zone = Node & {
   updatedAt: Scalars['DateTime'];
 };
 
+export type ZoneFilterParameter = {
+  createdAt?: InputMaybe<DateOperators>;
+  id?: InputMaybe<IdOperators>;
+  name?: InputMaybe<StringOperators>;
+  updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type ZoneList = PaginatedList & {
+  __typename?: 'ZoneList';
+  items: Array<Zone>;
+  totalItems: Scalars['Int'];
+};
+
+export type ZoneListOptions = {
+  /** Allows the results to be filtered */
+  filter?: InputMaybe<ZoneFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: InputMaybe<LogicalOperator>;
+  /** Skips the first n results, for use in pagination */
+  skip?: InputMaybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: InputMaybe<ZoneSortParameter>;
+  /** Takes n results, for use in pagination */
+  take?: InputMaybe<Scalars['Int']>;
+};
+
+export type ZoneSortParameter = {
+  createdAt?: InputMaybe<SortOrder>;
+  id?: InputMaybe<SortOrder>;
+  name?: InputMaybe<SortOrder>;
+  updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type GetProductsWithFacetValuesByIdsQueryVariables = Exact<{
   ids: Array<Scalars['String']> | Scalars['String'];
 }>;
@@ -6745,6 +6797,13 @@ export type DeleteCustomerGroupMutationVariables = Exact<{
 
 export type DeleteCustomerGroupMutation = { deleteCustomerGroup: { __typename?: 'DeletionResponse', result: DeletionResult, message?: string | null } };
 
+export type DeleteCustomerGroupsMutationVariables = Exact<{
+  ids: Array<Scalars['ID']> | Scalars['ID'];
+}>;
+
+
+export type DeleteCustomerGroupsMutation = { deleteCustomerGroups: Array<{ __typename?: 'DeletionResponse', result: DeletionResult, message?: string | null }> };
+
 export type GetCustomerGroupsQueryVariables = Exact<{
   options?: InputMaybe<CustomerGroupListOptions>;
 }>;
@@ -7539,10 +7598,12 @@ export type DeleteCountriesMutation = { deleteCountries: Array<{ __typename?: 'D
 
 export type ZoneFragment = { __typename?: 'Zone', id: string, createdAt: any, updatedAt: any, name: string, members: Array<{ __typename?: 'Country', id: string, createdAt: any, updatedAt: any, code: string, name: string, enabled: boolean, translations: Array<{ __typename?: 'RegionTranslation', id: string, languageCode: LanguageCode, name: string }> } | { __typename?: 'Province' }> };
 
-export type GetZonesQueryVariables = Exact<{ [key: string]: never; }>;
+export type GetZoneListQueryVariables = Exact<{
+  options?: InputMaybe<ZoneListOptions>;
+}>;
 
 
-export type GetZonesQuery = { zones: Array<{ __typename?: 'Zone', id: string, createdAt: any, updatedAt: any, name: string, members: Array<{ __typename?: 'Country', createdAt: any, updatedAt: any, id: string, name: string, code: string, enabled: boolean, translations: Array<{ __typename?: 'RegionTranslation', id: string, languageCode: LanguageCode, name: string }> } | { __typename?: 'Province', createdAt: any, updatedAt: any, id: string, name: string, code: string, enabled: boolean }> }> };
+export type GetZoneListQuery = { zones: { __typename?: 'ZoneList', totalItems: number, items: Array<{ __typename?: 'Zone', id: string, createdAt: any, updatedAt: any, name: string, members: Array<{ __typename?: 'Country', createdAt: any, updatedAt: any, id: string, name: string, code: string, enabled: boolean, translations: Array<{ __typename?: 'RegionTranslation', id: string, languageCode: LanguageCode, name: string }> } | { __typename?: 'Province', createdAt: any, updatedAt: any, id: string, name: string, code: string, enabled: boolean }> }> } };
 
 export type GetZoneQueryVariables = Exact<{
   id: Scalars['ID'];
@@ -7572,6 +7633,13 @@ export type DeleteZoneMutationVariables = Exact<{
 
 export type DeleteZoneMutation = { deleteZone: { __typename?: 'DeletionResponse', message?: string | null, result: DeletionResult } };
 
+export type DeleteZonesMutationVariables = Exact<{
+  ids: Array<Scalars['ID']> | Scalars['ID'];
+}>;
+
+
+export type DeleteZonesMutation = { deleteZones: Array<{ __typename?: 'DeletionResponse', message?: string | null, result: DeletionResult }> };
+
 export type AddMembersToZoneMutationVariables = Exact<{
   zoneId: Scalars['ID'];
   memberIds: Array<Scalars['ID']> | Scalars['ID'];

+ 1 - 0
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -188,6 +188,7 @@ const result: PossibleTypesResultData = {
             'TagList',
             'TaxCategoryList',
             'TaxRateList',
+            'ZoneList',
         ],
         RefundOrderResult: [
             'AlreadyRefundedError',

+ 9 - 0
packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts

@@ -195,6 +195,15 @@ export const DELETE_CUSTOMER_GROUP = gql`
     }
 `;
 
+export const DELETE_CUSTOMER_GROUPS = gql`
+    mutation DeleteCustomerGroups($ids: [ID!]!) {
+        deleteCustomerGroups(ids: $ids) {
+            result
+            message
+        }
+    }
+`;
+
 export const GET_CUSTOMER_GROUPS = gql`
     query GetCustomerGroups($options: CustomerGroupListOptions) {
         customerGroups(options: $options) {

+ 23 - 11
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -107,18 +107,21 @@ export const ZONE_FRAGMENT = gql`
     ${COUNTRY_FRAGMENT}
 `;
 
-export const GET_ZONES = gql`
-    query GetZones {
-        zones {
-            ...Zone
-            members {
-                createdAt
-                updatedAt
-                id
-                name
-                code
-                enabled
+export const GET_ZONE_LIST = gql`
+    query GetZoneList($options: ZoneListOptions) {
+        zones(options: $options) {
+            items {
+                ...Zone
+                members {
+                    createdAt
+                    updatedAt
+                    id
+                    name
+                    code
+                    enabled
+                }
             }
+            totalItems
         }
     }
     ${ZONE_FRAGMENT}
@@ -160,6 +163,15 @@ export const DELETE_ZONE = gql`
     }
 `;
 
+export const DELETE_ZONES = gql`
+    mutation DeleteZones($ids: [ID!]!) {
+        deleteZones(ids: $ids) {
+            message
+            result
+        }
+    }
+`;
+
 export const ADD_MEMBERS_TO_ZONE = gql`
     mutation AddMembersToZone($zoneId: ID!, $memberIds: [ID!]!) {
         addMembersToZone(zoneId: $zoneId, memberIds: $memberIds) {

+ 8 - 0
packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts

@@ -21,6 +21,7 @@ import {
     UPDATE_CUSTOMER_GROUP,
     UPDATE_CUSTOMER_NOTE,
     DELETE_CUSTOMERS,
+    DELETE_CUSTOMER_GROUPS,
 } from '../definitions/customer-definitions';
 
 import { BaseDataService } from './base-data.service';
@@ -148,6 +149,13 @@ export class CustomerDataService {
         >(DELETE_CUSTOMER_GROUP, { id });
     }
 
+    deleteCustomerGroups(ids: string[]) {
+        return this.baseDataService.mutate<
+            Codegen.DeleteCustomerGroupsMutation,
+            Codegen.DeleteCustomerGroupsMutationVariables
+        >(DELETE_CUSTOMER_GROUPS, { ids });
+    }
+
     getCustomerGroupList(options?: Codegen.CustomerGroupListOptions) {
         return this.baseDataService.query<
             Codegen.GetCustomerGroupsQuery,

+ 18 - 4
packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts

@@ -47,7 +47,7 @@ import {
     GET_TAX_RATE,
     GET_TAX_RATE_LIST,
     GET_TAX_RATE_LIST_SIMPLE,
-    GET_ZONES,
+    GET_ZONE_LIST,
     REMOVE_MEMBERS_FROM_ZONE,
     UPDATE_CHANNEL,
     UPDATE_COUNTRY,
@@ -63,6 +63,8 @@ import {
     DELETE_TAX_CATEGORIES,
     DELETE_TAX_RATES,
     DELETE_COUNTRIES,
+    GET_ZONE,
+    DELETE_ZONES,
 } from '../definitions/settings-definitions';
 
 import { BaseDataService } from './base-data.service';
@@ -132,12 +134,15 @@ export class SettingsDataService {
         });
     }
 
-    getZones() {
-        return this.baseDataService.query<Codegen.GetZonesQuery>(GET_ZONES);
+    getZones(options?: Codegen.ZoneListOptions) {
+        return this.baseDataService.query<Codegen.GetZoneListQuery, Codegen.GetZoneListQueryVariables>(
+            GET_ZONE_LIST,
+            { options },
+        );
     }
 
     getZone(id: string) {
-        return this.baseDataService.query<Codegen.GetZoneQuery, Codegen.GetZoneQueryVariables>(GET_ZONES, {
+        return this.baseDataService.query<Codegen.GetZoneQuery, Codegen.GetZoneQueryVariables>(GET_ZONE, {
             id,
         });
     }
@@ -169,6 +174,15 @@ export class SettingsDataService {
         );
     }
 
+    deleteZones(ids: string[]) {
+        return this.baseDataService.mutate<Codegen.DeleteZonesMutation, Codegen.DeleteZonesMutationVariables>(
+            DELETE_ZONES,
+            {
+                ids,
+            },
+        );
+    }
+
     addMembersToZone(zoneId: string, memberIds: string[]) {
         return this.baseDataService.mutate<
             Codegen.AddMembersToZoneMutation,

+ 5 - 0
packages/admin-ui/src/lib/core/src/providers/bulk-action-registry/bulk-action-types.ts

@@ -14,6 +14,9 @@ export type BulkActionLocationId =
     | 'facet-list'
     | 'collection-list'
     | 'customer-list'
+    | 'customer-group-list'
+    | 'customer-group-members-list'
+    | 'customer-group-members-picker-list'
     | 'promotion-list'
     | 'seller-list'
     | 'channel-list'
@@ -23,6 +26,8 @@ export type BulkActionLocationId =
     | 'payment-method-list'
     | 'tax-category-list'
     | 'tax-rate-list'
+    | 'zone-list'
+    | 'zone-members-list'
     | string;
 
 /**

+ 3 - 2
packages/admin-ui/src/lib/core/src/shared/components/bulk-action-menu/bulk-action-menu.component.html

@@ -1,6 +1,7 @@
 <vdr-dropdown *ngIf="actions$ | async as actions">
     <button
-        class="btn btn-sm btn-outline mr1"
+        *ngIf="actions.length"
+        class="btn btn-sm btn-outline mr-2"
         vdrDropdownTrigger
         [disabled]="!selectionManager.selection?.length"
         [class.hidden]="!selectionManager.selection?.length"
@@ -34,7 +35,7 @@
     </vdr-dropdown-menu>
 </vdr-dropdown>
 <button
-    class="button-small ml-2"
+    class="button-small"
     (click)="clearSelection()"
     [class.hidden]="!selectionManager.selection?.length"
 >

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.html

@@ -70,7 +70,7 @@
                 trackBy: trackByFn
             "
         >
-            <td *ngIf="selectionManager" class="selection-col">
+            <td *ngIf="selectionManager" class="selection-col" [class.active]="activeIndex === i">
                 <input
                     type="checkbox"
                     clrCheckbox
@@ -78,7 +78,7 @@
                     (click)="onRowClick(item, $event)"
                 />
             </td>
-            <td *ngFor="let column of visibleColumns">
+            <td *ngFor="let column of visibleColumns" [class.active]="activeIndex === i">
                 <div class="cell-content" [ngClass]="column.align">
                     <ng-container
                         *ngTemplateOutlet="column.template; context: { item: item, index: i }"

+ 5 - 1
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.scss

@@ -96,7 +96,7 @@ tbody tr:nth-child(even) {
 }
 
 tbody tr:hover {
-    background-color: var(--color-weight-100);
+    background-color: var(--color-table-row-hover-bg);
 }
 
 .cell-link {
@@ -105,6 +105,10 @@ tbody tr:hover {
     height: 100%;
 }
 
+td.active {
+    background-color: var(--color-table-row-active-bg);
+}
+
 .cell-content {
     display: flex;
     align-items: center;

+ 1 - 0
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.ts

@@ -99,6 +99,7 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnIn
     @Input() totalItems: number;
     @Input() emptyStateLabel: string;
     @Input() filters: DataTableFilterCollection;
+    @Input() activeIndex = -1;
     @Output() pageChange = new EventEmitter<number>();
     @Output() itemsPerPageChange = new EventEmitter<number>();
 

+ 27 - 0
packages/admin-ui/src/lib/core/src/shared/components/split-view/split-view.component.html

@@ -0,0 +1,27 @@
+<div class="split-view-wrapper" [class.expanded]="rightPanelOpen" [class.resizing]="resizing$ | async">
+    <div class="left-panel">
+        <ng-container *ngTemplateOutlet="leftTemplate"></ng-container>
+    </div>
+    <div class="separator" [class.hidden]="!rightPanelOpen">
+        <div class="top"></div>
+        <div class="resize-handle" #resizeHandle>
+            <clr-icon shape="drag-handle"></clr-icon>
+        </div>
+        <div class="bottom"></div>
+    </div>
+    <div
+        class="right-panel"
+        [class.expanded]="rightPanelOpen"
+        [class.resizing]="resizing$ | async"
+        [style.width.px]="rightPanelOpen ? (rightPanelWidth$ | async) : 0"
+    >
+        <div class="close-row">
+            <div class="title" *ngIf="rightTemplate.splitViewTitle as title">{{ title }}</div>
+            <button type="button" class="button-small" (click)="close()">
+                <clr-icon shape="close"></clr-icon>
+            </button>
+        </div>
+
+        <ng-container *ngTemplateOutlet="rightTemplate.template"></ng-container>
+    </div>
+</div>

+ 104 - 0
packages/admin-ui/src/lib/core/src/shared/components/split-view/split-view.component.scss

@@ -0,0 +1,104 @@
+:host {
+    --separator-border: var(--color-split-view-separator-border);
+}
+
+.split-view-wrapper {
+    display: flex;
+    height: calc(100% - 50px);
+
+    &.resizing {
+        user-select: none;
+    }
+
+    .left-panel {
+        flex: 1;
+        overflow: auto;
+        margin-top: 0;
+
+        .active {
+            background-color: var(--clr-global-selection-color);
+        }
+        &.expanded {
+            // Fix for Firefox layout https://github.com/vendure-ecommerce/vendure/issues/531
+            width: calc(100% - 40vw);
+        }
+    }
+}
+.separator {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+
+    &.hidden {
+        display: none;
+    }
+    .resize-handle {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        flex: 1;
+        border-left: 1px solid var(--separator-border);
+        border-right: 1px solid var(--separator-border);
+        cursor: ew-resize;
+        transition: background-color 0.2s;
+    }
+    .top,
+    .bottom {
+        height: 48px;
+        width: 100%;
+        border-style: solid;
+        border-width: 0 1px;
+    }
+    .top {
+        border-image: linear-gradient(0deg, var(--separator-border), transparent) 1;
+    }
+    .bottom {
+        border-image: linear-gradient(180deg, var(--separator-border), transparent) 1;
+    }
+}
+.resizing {
+    .resize-handle {
+        color: var(--color-primary-700);
+    }
+    --separator-border: var(--color-split-view-separator-resize-border);
+}
+.close-row {
+    display: flex;
+    justify-content: space-between;
+    padding: calc(var(--space-unit) * 1);
+    padding-left: calc(var(--space-unit) * 4);
+}
+.right-panel {
+    height: 100%;
+    opacity: 0;
+    visibility: hidden;
+    overflow: auto;
+    &:not(.resizing) {
+        transition: width 0.3s, opacity 0.2s 0.3s, visibility 0s 0.3s;
+    }
+
+    &.expanded {
+        visibility: visible;
+        opacity: 1;
+    }
+
+    .close-button {
+        margin: 0;
+        background: none;
+        border: none;
+        cursor: pointer;
+    }
+
+    ::ng-deep table.table {
+        margin-top: 0;
+        th {
+            top: 0;
+        }
+    }
+
+    .controls {
+        display: flex;
+        justify-content: space-between;
+    }
+}

+ 72 - 0
packages/admin-ui/src/lib/core/src/shared/components/split-view/split-view.component.ts

@@ -0,0 +1,72 @@
+import {
+    AfterContentInit,
+    AfterViewInit,
+    ChangeDetectionStrategy,
+    Component,
+    ContentChild,
+    ElementRef,
+    EventEmitter,
+    Input,
+    Output,
+    TemplateRef,
+    ViewChild,
+    ViewContainerRef,
+} from '@angular/core';
+import { fromEvent, merge, Observable, switchMap } from 'rxjs';
+import { map, mapTo, startWith, takeUntil } from 'rxjs/operators';
+import { SplitViewLeftDirective, SplitViewRightDirective } from './split-view.directive';
+
+@Component({
+    selector: 'vdr-split-view',
+    templateUrl: './split-view.component.html',
+    styleUrls: ['./split-view.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SplitViewComponent implements AfterContentInit, AfterViewInit {
+    @Input() rightPanelOpen = false;
+    @Output() closeClicked = new EventEmitter<void>();
+
+    @ContentChild(SplitViewLeftDirective, { static: true, read: TemplateRef })
+    leftTemplate: TemplateRef<any>;
+    @ContentChild(SplitViewRightDirective, { static: true, read: SplitViewRightDirective })
+    rightTemplate: SplitViewRightDirective;
+    @ViewChild('resizeHandle', { static: true, read: ElementRef }) resizeHandle: ElementRef<HTMLDivElement>;
+    protected rightPanelWidth$: Observable<number>;
+    protected resizing$: Observable<boolean>;
+
+    constructor(private viewContainerRef: ViewContainerRef) {}
+
+    ngAfterContentInit(): void {
+        if (!this.leftTemplate) {
+            throw new Error('A <vdr-split-view-left> must be provided');
+        }
+        if (!this.rightTemplate) {
+            throw new Error('A <vdr-split-view-right> must be provided');
+        }
+    }
+
+    ngAfterViewInit() {
+        const hostElement = this.viewContainerRef.element.nativeElement;
+        const hostElementWidth = hostElement.getBoundingClientRect()?.width;
+
+        const mouseDown$ = fromEvent<MouseEvent>(this.resizeHandle.nativeElement, 'mousedown');
+        const mouseMove$ = fromEvent<MouseEvent>(document, 'mousemove');
+        const mouseUp$ = fromEvent<MouseEvent>(document, 'mouseup');
+
+        // update right panel width when resize handle is dragged
+        this.rightPanelWidth$ = mouseDown$.pipe(
+            switchMap(() => mouseMove$.pipe(takeUntil(mouseUp$))),
+            map(event => {
+                const width = hostElement.getBoundingClientRect().right - event.clientX;
+                return Math.max(100, Math.min(width, hostElementWidth - 100));
+            }),
+            startWith(hostElementWidth / 2),
+        );
+
+        this.resizing$ = merge(mouseDown$.pipe(mapTo(true)), mouseUp$.pipe(mapTo(false)));
+    }
+
+    close() {
+        this.closeClicked.emit();
+    }
+}

+ 14 - 0
packages/admin-ui/src/lib/core/src/shared/components/split-view/split-view.directive.ts

@@ -0,0 +1,14 @@
+import { Directive, Input, TemplateRef } from '@angular/core';
+
+@Directive({
+    selector: '[vdrSplitViewLeft]',
+})
+export class SplitViewLeftDirective {}
+
+@Directive({
+    selector: '[vdrSplitViewRight]',
+})
+export class SplitViewRightDirective {
+    constructor(public template: TemplateRef<any>) {}
+    @Input() splitViewTitle?: string;
+}

+ 16 - 8
packages/admin-ui/src/lib/core/src/shared/shared.module.ts

@@ -42,6 +42,9 @@ import { CustomerLabelComponent } from './components/customer-label/customer-lab
 import { DataTable2ColumnComponent } from './components/data-table-2/data-table-column.component';
 import { DataTable2SearchComponent } from './components/data-table-2/data-table-search.component';
 import { DataTable2Component } from './components/data-table-2/data-table2.component';
+import { DataTableColumnPickerComponent } from './components/data-table-column-picker/data-table-column-picker.component';
+import { DataTableFilterLabelComponent } from './components/data-table-filter-label/data-table-filter-label.component';
+import { DataTableFiltersComponent } from './components/data-table-filters/data-table-filters.component';
 import { DataTableColumnComponent } from './components/data-table/data-table-column.component';
 import { DataTableComponent } from './components/data-table/data-table.component';
 import { DatetimePickerComponent } from './components/datetime-picker/datetime-picker.component';
@@ -72,13 +75,17 @@ import { DialogTitleDirective } from './components/modal-dialog/dialog-title.dir
 import { ModalDialogComponent } from './components/modal-dialog/modal-dialog.component';
 import { ObjectTreeComponent } from './components/object-tree/object-tree.component';
 import { OrderStateLabelComponent } from './components/order-state-label/order-state-label.component';
+import { PageBodyComponent } from './components/page-body/page-body.component';
+import { PageHeaderDescriptionComponent } from './components/page-header-description/page-header-description.component';
+import { PageHeaderTabsComponent } from './components/page-header-tabs/page-header-tabs.component';
+import { PageHeaderComponent } from './components/page-header/page-header.component';
 import { PageTitleComponent } from './components/page-title/page-title.component';
 import { PaginationControlsComponent } from './components/pagination-controls/pagination-controls.component';
 import { ProductMultiSelectorDialogComponent } from './components/product-multi-selector-dialog/product-multi-selector-dialog.component';
 import { ProductSearchInputComponent } from './components/product-search-input/product-search-input.component';
+import { ProductVariantSelectorComponent } from './components/product-variant-selector/product-variant-selector.component';
 import { RadioCardFieldsetComponent } from './components/radio-card/radio-card-fieldset.component';
 import { RadioCardComponent } from './components/radio-card/radio-card.component';
-import { ProductVariantSelectorComponent } from './components/product-variant-selector/product-variant-selector.component';
 import { ExternalImageDialogComponent } from './components/rich-text-editor/external-image-dialog/external-image-dialog.component';
 import { LinkDialogComponent } from './components/rich-text-editor/link-dialog/link-dialog.component';
 import { ContextMenuComponent } from './components/rich-text-editor/prosemirror/context-menu/context-menu.component';
@@ -86,6 +93,11 @@ import { RawHtmlDialogComponent } from './components/rich-text-editor/raw-html-d
 import { RichTextEditorComponent } from './components/rich-text-editor/rich-text-editor.component';
 import { SelectToggleComponent } from './components/select-toggle/select-toggle.component';
 import { SimpleDialogComponent } from './components/simple-dialog/simple-dialog.component';
+import {
+    SplitViewLeftDirective,
+    SplitViewRightDirective,
+} from './components/split-view/split-view.directive';
+import { SplitViewComponent } from './components/split-view/split-view.component';
 import { StatusBadgeComponent } from './components/status-badge/status-badge.component';
 import { TabbedCustomFieldsComponent } from './components/tabbed-custom-fields/tabbed-custom-fields.component';
 import { TableRowActionComponent } from './components/table-row-action/table-row-action.component';
@@ -144,13 +156,6 @@ import { StateI18nTokenPipe } from './pipes/state-i18n-token.pipe';
 import { StringToColorPipe } from './pipes/string-to-color.pipe';
 import { TimeAgoPipe } from './pipes/time-ago.pipe';
 import { CanDeactivateDetailGuard } from './providers/routing/can-deactivate-detail-guard';
-import { PageHeaderComponent } from './components/page-header/page-header.component';
-import { PageHeaderDescriptionComponent } from './components/page-header-description/page-header-description.component';
-import { PageHeaderTabsComponent } from './components/page-header-tabs/page-header-tabs.component';
-import { PageBodyComponent } from './components/page-body/page-body.component';
-import { DataTableFiltersComponent } from './components/data-table-filters/data-table-filters.component';
-import { DataTableFilterLabelComponent } from './components/data-table-filter-label/data-table-filter-label.component';
-import { DataTableColumnPickerComponent } from './components/data-table-column-picker/data-table-column-picker.component';
 
 const IMPORTS = [
     ClarityModule,
@@ -270,6 +275,9 @@ const DECLARATIONS = [
     DataTableFilterLabelComponent,
     DataTableColumnPickerComponent,
     DataTable2SearchComponent,
+    SplitViewComponent,
+    SplitViewLeftDirective,
+    SplitViewRightDirective,
 ];
 
 const DYNAMIC_FORM_INPUTS = [

+ 2 - 3
packages/admin-ui/src/lib/customer/src/components/add-customer-to-group-dialog/add-customer-to-group-dialog.component.html

@@ -3,15 +3,14 @@
 </ng-template>
 
 <vdr-customer-group-member-list
+    locationId="customer-group-members-picker-list"
     [members]="customers$ | async"
     [totalItems]="customersTotal$ | async"
     [route]="route"
     [selectedMemberIds]="selectedCustomerIds"
     (fetchParamsChange)="fetchGroupMembers$.next($event)"
     (selectionChange)="selectedCustomerIds = $event"
->
-
-</vdr-customer-group-member-list>
+/>
 
 <ng-template vdrDialogButtons>
     <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>

+ 12 - 0
packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list-bulk-actions.ts

@@ -0,0 +1,12 @@
+import { createBulkDeleteAction, GetCustomerGroupsQuery, ItemOf, Permission } from '@vendure/admin-ui/core';
+import { map } from 'rxjs/operators';
+
+export const deleteCustomerGroupsBulkAction = createBulkDeleteAction<
+    ItemOf<GetCustomerGroupsQuery, 'customerGroups'>
+>({
+    location: 'customer-group-list',
+    requiresPermission: userPermissions => userPermissions.includes(Permission.DeleteCustomerGroup),
+    getItemName: item => item.name,
+    bulkDelete: (dataService, ids) =>
+        dataService.customer.deleteCustomerGroups(ids).pipe(map(res => res.deleteCustomerGroups)),
+});

+ 0 - 56
packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.scss

@@ -1,61 +1,5 @@
 @import 'variables';
 
-.group-wrapper {
-    display: flex;
-    height: calc(100% - 50px);
-
-    .group-list {
-        flex: 1;
-        overflow: auto;
-        margin-top: 0;
-
-        .active {
-            background-color: var(--clr-global-selection-color);
-        }
-        &.expanded {
-            // Fix for Firefox layout https://github.com/vendure-ecommerce/vendure/issues/531
-            width: calc(100% - 40vw);
-        }
-    }
-}
-vdr-data-table ::ng-deep table {
-    margin-top: 0;
-}
-.group-members {
-    height: 100%;
-    width: 0;
-    opacity: 0;
-    visibility: hidden;
-    overflow: auto;
-    transition: width 0.3s, opacity 0.2s 0.3s, visibility 0s 0.3s;
-
-    &.expanded {
-        width: 40vw;
-        visibility: visible;
-        opacity: 1;
-        padding-left: 12px;
-    }
-
-    .close-button {
-        margin: 0;
-        background: none;
-        border: none;
-        cursor: pointer;
-    }
-
-    ::ng-deep table.table {
-        margin-top: 0;
-        th {
-            top: 0;
-        }
-    }
-
-    .controls {
-        display: flex;
-        justify-content: space-between;
-    }
-}
-
 vdr-empty-placeholder {
     flex: 1;
 }

+ 39 - 0
packages/admin-ui/src/lib/customer/src/components/customer-group-member-list/customer-group-member-list-bulk-actions.ts

@@ -0,0 +1,39 @@
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import {
+    BulkAction,
+    DataService,
+    ModalService,
+    NotificationService,
+    Permission,
+} from '@vendure/admin-ui/core';
+import { CustomerGroupMember, CustomerGroupMemberListComponent } from '@vendure/admin-ui/customer';
+
+export const removeCustomerGroupMembersBulkAction: BulkAction<
+    CustomerGroupMember,
+    CustomerGroupMemberListComponent
+> = {
+    location: 'customer-group-members-list',
+    label: _('customer.remove-from-group'),
+    icon: 'trash',
+    iconClass: 'is-danger',
+    requiresPermission: Permission.UpdateCustomerGroup,
+    onClick: ({ injector, selection, hostComponent, clearSelection }) => {
+        const modalService = injector.get(ModalService);
+        const dataService = injector.get(DataService);
+        const notificationService = injector.get(NotificationService);
+
+        const group = hostComponent.activeGroup;
+        const customerIds = selection.map(s => s.id);
+
+        dataService.customer.removeCustomersFromGroup(group.id, customerIds).subscribe({
+            complete: () => {
+                notificationService.success(_(`customer.remove-customers-from-group-success`), {
+                    customerCount: customerIds.length,
+                    groupName: group.name,
+                });
+                clearSelection();
+                hostComponent.refresh();
+            },
+        });
+    },
+};

+ 4 - 0
packages/admin-ui/src/lib/customer/src/customer.module.ts

@@ -7,7 +7,9 @@ import { AddressCardComponent } from './components/address-card/address-card.com
 import { AddressDetailDialogComponent } from './components/address-detail-dialog/address-detail-dialog.component';
 import { CustomerDetailComponent } from './components/customer-detail/customer-detail.component';
 import { CustomerGroupDetailDialogComponent } from './components/customer-group-detail-dialog/customer-group-detail-dialog.component';
+import { deleteCustomerGroupsBulkAction } from './components/customer-group-list/customer-group-list-bulk-actions';
 import { CustomerGroupListComponent } from './components/customer-group-list/customer-group-list.component';
+import { removeCustomerGroupMembersBulkAction } from './components/customer-group-member-list/customer-group-member-list-bulk-actions';
 import { CustomerGroupMemberListComponent } from './components/customer-group-member-list/customer-group-member-list.component';
 import { CustomerHistoryEntryHostComponent } from './components/customer-history/customer-history-entry-host.component';
 import { CustomerHistoryComponent } from './components/customer-history/customer-history.component';
@@ -38,5 +40,7 @@ import { customerRoutes } from './customer.routes';
 export class CustomerModule {
     constructor(private bulkActionRegistryService: BulkActionRegistryService) {
         bulkActionRegistryService.registerBulkAction(deleteCustomersBulkAction);
+        bulkActionRegistryService.registerBulkAction(deleteCustomerGroupsBulkAction);
+        bulkActionRegistryService.registerBulkAction(removeCustomerGroupMembersBulkAction);
     }
 }

+ 2 - 2
packages/admin-ui/src/lib/settings/src/components/add-country-to-zone-dialog/add-country-to-zone-dialog.component.ts

@@ -1,5 +1,5 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
-import { DataService, Dialog, GetCountryListQuery, GetZonesQuery, ItemOf } from '@vendure/admin-ui/core';
+import { DataService, Dialog, GetCountryListQuery, GetZoneListQuery, ItemOf } from '@vendure/admin-ui/core';
 import { Observable } from 'rxjs';
 import { filter, map } from 'rxjs/operators';
 
@@ -12,7 +12,7 @@ import { filter, map } from 'rxjs/operators';
 export class AddCountryToZoneDialogComponent implements Dialog<string[]>, OnInit {
     resolveWith: (result?: string[]) => void;
     zoneName: string;
-    currentMembers: GetZonesQuery['zones'][number]['members'] = [];
+    currentMembers: ItemOf<GetZoneListQuery, 'zones'>['members'] = [];
     availableCountries$: Observable<Array<ItemOf<GetCountryListQuery, 'countries'>>>;
     selectedMemberIds: string[] = [];
 

+ 4 - 3
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts

@@ -10,7 +10,8 @@ import {
     CustomFieldConfig,
     DataService,
     GetSellersQuery,
-    GetZonesQuery,
+    GetZoneListQuery,
+    ItemOf,
     LanguageCode,
     NotificationService,
     Permission,
@@ -32,7 +33,7 @@ export class ChannelDetailComponent
     implements OnInit, OnDestroy
 {
     customFields: CustomFieldConfig[];
-    zones$: Observable<GetZonesQuery['zones']>;
+    zones$: Observable<Array<ItemOf<GetZoneListQuery, 'zones'>>>;
     sellers$: Observable<GetSellersQuery['sellers']['items']>;
     detailForm: UntypedFormGroup;
     currencyCodes = Object.values(CurrencyCode);
@@ -67,7 +68,7 @@ export class ChannelDetailComponent
 
     ngOnInit() {
         this.init();
-        this.zones$ = this.dataService.settings.getZones().mapSingle(data => data.zones);
+        this.zones$ = this.dataService.settings.getZones({ take: 999 }).mapSingle(data => data.zones.items);
         // TODO: make this lazy-loaded autocomplete
         this.sellers$ = this.dataService.settings.getSellerList().mapSingle(data => data.sellers.items);
         this.availableLanguageCodes$ = this.serverConfigService.getAvailableLanguages();

+ 3 - 2
packages/admin-ui/src/lib/settings/src/components/seller-detail/seller-detail.component.ts

@@ -8,7 +8,8 @@ import {
     CurrencyCode,
     CustomFieldConfig,
     DataService,
-    GetZonesQuery,
+    GetZoneListQuery,
+    ItemOf,
     LanguageCode,
     NotificationService,
     Permission,
@@ -27,7 +28,7 @@ import { mergeMap, take } from 'rxjs/operators';
 })
 export class SellerDetailComponent extends BaseDetailComponent<SellerFragment> implements OnInit, OnDestroy {
     customFields: CustomFieldConfig[];
-    zones$: Observable<GetZonesQuery['zones']>;
+    zones$: Observable<Array<ItemOf<GetZoneListQuery, 'zones'>>>;
     detailForm: UntypedFormGroup;
     currencyCodes = Object.values(CurrencyCode);
     availableLanguageCodes$: Observable<LanguageCode[]>;

+ 4 - 3
packages/admin-ui/src/lib/settings/src/components/tax-rate-detail/tax-rate-detail.component.ts

@@ -8,7 +8,8 @@ import {
     CustomerGroup,
     CustomFieldConfig,
     DataService,
-    GetZonesQuery,
+    GetZoneListQuery,
+    ItemOf,
     LanguageCode,
     NotificationService,
     Permission,
@@ -32,7 +33,7 @@ export class TaxRateDetailComponent
     implements OnInit, OnDestroy
 {
     taxCategories$: Observable<TaxCategoryFragment[]>;
-    zones$: Observable<GetZonesQuery['zones']>;
+    zones$: Observable<Array<ItemOf<GetZoneListQuery, 'zones'>>>;
     groups$: Observable<CustomerGroup[]>;
     detailForm: UntypedFormGroup;
     customFields: CustomFieldConfig[];
@@ -67,7 +68,7 @@ export class TaxRateDetailComponent
         this.taxCategories$ = this.dataService.settings
             .getTaxCategories()
             .mapSingle(data => data.taxCategories.items);
-        this.zones$ = this.dataService.settings.getZones().mapSingle(data => data.zones);
+        this.zones$ = this.dataService.settings.getZones({ take: 999 }).mapSingle(data => data.zones.items);
     }
 
     ngOnDestroy() {

+ 11 - 0
packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list-bulk-actions.ts

@@ -0,0 +1,11 @@
+import { createBulkDeleteAction, GetZoneListQuery, ItemOf, Permission } from '@vendure/admin-ui/core';
+import { map } from 'rxjs/operators';
+
+export const deleteZonesBulkAction = createBulkDeleteAction<ItemOf<GetZoneListQuery, 'zones'>>({
+    location: 'zone-list',
+    requiresPermission: userPermissions =>
+        userPermissions.includes(Permission.DeleteSettings) ||
+        userPermissions.includes(Permission.DeleteZone),
+    getItemName: item => item.name,
+    bulkDelete: (dataService, ids) => dataService.settings.deleteZones(ids).pipe(map(res => res.deleteZones)),
+});

+ 102 - 135
packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.html

@@ -1,12 +1,5 @@
-<vdr-action-bar>
-    <vdr-ab-left>
-            <vdr-language-selector
-                [availableLanguageCodes]="availableLanguages$ | async"
-                [currentLanguageCode]="contentLanguage$ | async"
-                (languageCodeChange)="setLanguage($event)"
-            ></vdr-language-selector>
-        </vdr-ab-left>
-    <vdr-ab-right>
+<vdr-page-header>
+    <vdr-page-title>
         <vdr-action-bar-items locationId="zone-list"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
@@ -16,134 +9,108 @@
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-zone' | translate }}
         </button>
-    </vdr-ab-right>
-</vdr-action-bar>
-<div class="zone-wrapper">
-    <div class="zone-list">
-        <table class="table mt0">
-            <tbody>
-                <tr
-                    *ngFor="let zone of zones$ | async"
-                    [class.active]="zone.id === (activeZone$ | async)?.id"
+    </vdr-page-title>
+</vdr-page-header>
+<vdr-page-body>
+    <vdr-language-selector
+        class="ml-4 mt-2"
+        [availableLanguageCodes]="availableLanguages$ | async"
+        [currentLanguageCode]="contentLanguage$ | async"
+        (languageCodeChange)="setLanguage($event)"
+    />
+    <vdr-split-view [rightPanelOpen]="activeZone$ | async" (closeClicked)="closeMembers()">
+        <ng-template vdrSplitViewLeft>
+            <vdr-data-table-2
+                class="mt-2"
+                id="zone-list"
+                [items]="items$ | async"
+                [itemsPerPage]="itemsPerPage$ | async"
+                [totalItems]="totalItems$ | async"
+                [currentPage]="currentPage$ | async"
+                [filters]="filters"
+                [activeIndex]="activeIndex$ | async"
+                (pageChange)="setPageNumber($event)"
+                (itemsPerPageChange)="setItemsPerPage($event)"
+            >
+                <vdr-bulk-action-menu
+                    locationId="zone-list"
+                    [hostComponent]="this"
+                    [selectionManager]="selectionManager"
+                />
+                <vdr-dt2-search
+                    [searchTermControl]="searchTermControl"
+                    [searchTermPlaceholder]="'common.search-by-name' | translate"
+                />
+                <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+                    <ng-template let-customerGroup="item">
+                        {{ customerGroup.id }}
+                    </ng-template>
+                </vdr-dt2-column>
+                <vdr-dt2-column
+                    [heading]="'common.created-at' | translate"
+                    [hiddenByDefault]="true"
+                    [sort]="sorts.get('createdAt')"
+                >
+                    <ng-template let-customerGroup="item">
+                        {{ customerGroup.createdAt | localeDate : 'short' }}
+                    </ng-template>
+                </vdr-dt2-column>
+                <vdr-dt2-column
+                    [heading]="'common.updated-at' | translate"
+                    [hiddenByDefault]="true"
+                    [sort]="sorts.get('updatedAt')"
+                >
+                    <ng-template let-customerGroup="item">
+                        {{ customerGroup.updatedAt | localeDate : 'short' }}
+                    </ng-template>
+                </vdr-dt2-column>
+                <vdr-dt2-column
+                    [heading]="'common.name' | translate"
+                    [optional]="false"
+                    [sort]="sorts.get('name')"
                 >
-                    <td class="left align-middle"><vdr-entity-info [entity]="zone"></vdr-entity-info></td>
-                    <td class="left align-middle">
-                        <vdr-chip [colorFrom]="zone.name">{{ zone.name }}</vdr-chip>
-                    </td>
-                    <td class="text-right align-middle">
+                    <ng-template let-customerGroup="item">
+                        <a class="button-ghost" [routerLink]="['./', customerGroup.id]"
+                            ><span>{{ customerGroup.name }}</span>
+                            <clr-icon shape="arrow right"></clr-icon>
+                        </a>
+                    </ng-template>
+                </vdr-dt2-column>
+                <vdr-dt2-column
+                    [heading]="'common.view-contents' | translate"
+                    [optional]="false"
+                    [sort]="sorts.get('name')"
+                >
+                    <ng-template let-customerGroup="item">
                         <a
-                            class="btn btn-link btn-sm"
-                            [routerLink]="['./', { contents: zone.id }]"
+                            class="button-small bg-weight-150"
+                            [routerLink]="['./', { contents: customerGroup.id }]"
                             queryParamsHandling="preserve"
                         >
-                            <clr-icon shape="view-list"></clr-icon>
-                            {{ 'settings.view-zone-members' | translate }}
+                            <span>{{ 'settings.view-zone-members' | translate }}</span>
+                            <clr-icon shape="file-group"></clr-icon>
                         </a>
-                    </td>
-                    <td class="align-middle">
-                        <button class="btn btn-link btn-sm" (click)="update(zone)">
-                            <clr-icon shape="edit"></clr-icon>
-                            {{ 'common.edit' | translate }}
-                        </button>
-                    </td>
-                    <td class="align-middle">
-                        <vdr-dropdown>
-                            <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                                {{ 'common.actions' | translate }}
-                                <clr-icon shape="caret down"></clr-icon>
-                            </button>
-                            <vdr-dropdown-menu vdrPosition="bottom-right">
-                                <button
-                                    class="button"
-                                    vdrDropdownItem
-                                    (click)="delete(zone.id)"
-                                    [disabled]="!(['DeleteSettings', 'DeleteZone'] | hasPermission)"
-                                >
-                                    <clr-icon shape="trash" class="is-danger"></clr-icon>
-                                    {{ 'common.delete' | translate }}
-                                </button>
-                            </vdr-dropdown-menu>
-                        </vdr-dropdown>
-                    </td>
-                </tr>
-            </tbody>
-        </table>
-    </div>
-    <div class="zone-members" [class.expanded]="activeZone$ | async">
-        <ng-container *ngIf="activeZone$ | async as activeZone">
-            <vdr-zone-member-list
-                [members]="activeZone.members"
-                [selectedMemberIds]="selectedMemberIds"
-                (selectionChange)="selectedMemberIds = $event"
-            >
-                <div *vdrZoneMemberListHeader>
-                    <div class="flex">
-                        <div class="header-title-row">
-                            {{ activeZone.name }} ({{ activeZone.members.length }})
-                        </div>
-                        <div class="flex-spacer"></div>
-                        <button type="button" class="close-button" (click)="closeMembers()">
-                            <clr-icon shape="close"></clr-icon>
-                        </button>
-                    </div>
-                    <div class="controls">
-                        <vdr-dropdown>
-                            <button
-                                type="button"
-                                class="btn btn-secondary btn-sm"
-                                vdrDropdownTrigger
-                                [disabled]="selectedMemberIds.length === 0"
-                            >
-                                {{ 'common.with-selected' | translate: { count: selectedMemberIds.length } }}
-                                <clr-icon shape="caret down"></clr-icon>
-                            </button>
-                            <vdr-dropdown-menu vdrPosition="bottom-right">
-                                <button
-                                    type="button"
-                                    class="delete-button"
-                                    (click)="removeFromZone(activeZone, selectedMemberIds)"
-                                    vdrDropdownItem
-                                    [disabled]="!(['UpdateSettings', 'UpdateZone'] | hasPermission)"
-                                >
-                                    <clr-icon shape="trash" class="is-danger"></clr-icon>
-                                    {{ 'settings.remove-from-zone' | translate }}
-                                </button>
-                            </vdr-dropdown-menu>
-                        </vdr-dropdown>
-                        <button class="btn btn-secondary btn-sm" (click)="addToZone(activeZone)">
-                            {{ 'settings.add-countries-to-zone' | translate: { zoneName: activeZone.name } }}
-                        </button>
-                    </div>
-                </div>
-                <div *vdrZoneMemberControls="let member = member">
-                    <vdr-dropdown>
-                        <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                            {{ 'common.actions' | translate }}
-                            <clr-icon shape="caret down"></clr-icon>
-                        </button>
-                        <vdr-dropdown-menu vdrPosition="bottom-right">
-                            <a
-                                type="button"
-                                [routerLink]="['/settings', 'countries', member.id]"
-                                vdrDropdownItem
-                            >
-                                <clr-icon shape="edit"></clr-icon>
-                                {{ 'common.edit' | translate }}
-                            </a>
-                            <button
-                                type="button"
-                                class="delete-button"
-                                (click)="removeFromZone(activeZone, [member.id])"
-                                vdrDropdownItem
-                                [disabled]="!(['UpdateSettings', 'UpdateZone'] | hasPermission)"
-                            >
-                                <clr-icon shape="trash" class="is-danger"></clr-icon>
-                                {{ 'settings.remove-from-zone' | translate }}
-                            </button>
-                        </vdr-dropdown-menu>
-                    </vdr-dropdown>
-                </div>
-            </vdr-zone-member-list>
-        </ng-container>
-    </div>
-</div>
+                    </ng-template>
+                </vdr-dt2-column>
+            </vdr-data-table-2>
+        </ng-template>
+        <ng-template vdrSplitViewRight [splitViewTitle]="(activeZone$ | async)?.name">
+            <ng-container *ngIf="activeZone$ | async as activeZone">
+                <button class="button-ghost ml-4" (click)="addToZone(activeZone)">
+                    <clr-icon shape="plus"></clr-icon>
+                    <span>{{
+                        'settings.add-countries-to-zone' | translate : { zoneName: activeZone.name }
+                    }}</span>
+                </button>
+                <vdr-zone-member-list
+                    *ngIf="activeZone$ | async as activeZone"
+                    locationId="zone-members-list"
+                    [members]="activeZone.members"
+                    [selectedMemberIds]="selectedMemberIds"
+                    [activeZone]="activeZone"
+                    (selectionChange)="selectedMemberIds = $event"
+                />
+            </ng-container>
+        </ng-template>
+    </vdr-split-view>
+</vdr-page-body>

+ 74 - 71
packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.ts

@@ -2,15 +2,20 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
+    BaseListComponent,
     DataService,
-    DeletionResult,
-    GetZonesQuery,
+    DataTableService,
+    GetZoneListQuery,
+    ItemOf,
     LanguageCode,
+    LogicalOperator,
     ModalService,
     NotificationService,
     ServerConfigService,
+    ZoneFilterParameter,
+    ZoneSortParameter,
 } from '@vendure/admin-ui/core';
-import { combineLatest, EMPTY, Observable, of } from 'rxjs';
+import { combineLatest, EMPTY, Observable } from 'rxjs';
 import { distinctUntilChanged, map, mapTo, switchMap, tap } from 'rxjs/operators';
 
 import { AddCountryToZoneDialogComponent } from '../add-country-to-zone-dialog/add-country-to-zone-dialog.component';
@@ -22,43 +27,94 @@ import { ZoneDetailDialogComponent } from '../zone-detail-dialog/zone-detail-dia
     styleUrls: ['./zone-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ZoneListComponent implements OnInit {
-    activeZone$: Observable<GetZonesQuery['zones'][number] | undefined>;
-    zones$: Observable<GetZonesQuery['zones']>;
-    members$: Observable<GetZonesQuery['zones'][number]['members']>;
+export class ZoneListComponent
+    extends BaseListComponent<GetZoneListQuery, ItemOf<GetZoneListQuery, 'zones'>>
+    implements OnInit
+{
+    activeZone$: Observable<ItemOf<GetZoneListQuery, 'zones'> | undefined>;
+    activeIndex$: Observable<number>;
+    members$: Observable<ItemOf<GetZoneListQuery, 'zones'>['members']>;
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
     selectedMemberIds: string[] = [];
 
+    readonly filters = this.dataTableService
+        .createFilterCollection<ZoneFilterParameter>()
+        .addDateFilters()
+        .addFilter({
+            name: 'name',
+            type: { kind: 'text' },
+            label: _('common.name'),
+            filterField: 'name',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<ZoneSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'name' })
+        .connectToRoute(this.route);
+
     constructor(
+        route: ActivatedRoute,
+        router: Router,
         private dataService: DataService,
         private notificationService: NotificationService,
         private modalService: ModalService,
-        private route: ActivatedRoute,
-        private router: Router,
         private serverConfigService: ServerConfigService,
-    ) {}
+        private dataTableService: DataTableService,
+    ) {
+        super(router, route);
+        super.setQueryFn(
+            (...args: any[]) => this.dataService.settings.getZones(...args).refetchOnChannelChange(),
+            data => data.zones,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        name: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    filterOperator: this.searchTermControl.value ? LogicalOperator.OR : LogicalOperator.AND,
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
+        );
+    }
 
     ngOnInit(): void {
-        const zonesQueryRef = this.dataService.settings.getZones().ref;
-        this.zones$ = zonesQueryRef.valueChanges.pipe(map(data => data.data.zones));
+        super.ngOnInit();
         const activeZoneId$ = this.route.paramMap.pipe(
             map(pm => pm.get('contents')),
             distinctUntilChanged(),
             tap(() => (this.selectedMemberIds = [])),
         );
-        this.activeZone$ = combineLatest(this.zones$, activeZoneId$).pipe(
+        this.activeZone$ = combineLatest(this.items$, activeZoneId$).pipe(
             map(([zones, activeZoneId]) => {
                 if (activeZoneId) {
                     return zones.find(z => z.id === activeZoneId);
                 }
             }),
         );
+        this.activeIndex$ = combineLatest(this.items$, activeZoneId$).pipe(
+            map(([zones, activeZoneId]) => {
+                if (activeZoneId) {
+                    return zones.findIndex(g => g.id === activeZoneId);
+                } else {
+                    return -1;
+                }
+            }),
+        );
         this.availableLanguages$ = this.serverConfigService.getAvailableLanguages();
         this.contentLanguage$ = this.dataService.client
             .uiState()
-            .mapStream(({ uiState }) => uiState.contentLanguage)
-            .pipe(tap(() => zonesQueryRef.refetch()));
+            .mapStream(({ uiState }) => uiState.contentLanguage);
+        super.refreshListOnChanges(this.contentLanguage$, this.filters.valueChanges, this.sorts.valueChanges);
     }
 
     setLanguage(code: LanguageCode) {
@@ -72,14 +128,13 @@ export class ZoneListComponent implements OnInit {
                 switchMap(result =>
                     result ? this.dataService.settings.createZone({ ...result, memberIds: [] }) : EMPTY,
                 ),
-                // refresh list
-                switchMap(() => this.dataService.settings.getZones().single$),
             )
             .subscribe(
                 () => {
                     this.notificationService.success(_('common.notify-create-success'), {
                         entity: 'Zone',
                     });
+                    this.refresh();
                 },
                 err => {
                     this.notificationService.error(_('common.notify-create-error'), {
@@ -89,48 +144,7 @@ export class ZoneListComponent implements OnInit {
             );
     }
 
-    delete(zoneId: string) {
-        this.modalService
-            .dialog({
-                title: _('catalog.confirm-delete-zone'),
-                buttons: [
-                    { type: 'secondary', label: _('common.cancel') },
-                    { type: 'danger', label: _('common.delete'), returnValue: true },
-                ],
-            })
-            .pipe(
-                switchMap(response => (response ? this.dataService.settings.deleteZone(zoneId) : EMPTY)),
-
-                switchMap(result => {
-                    if (result.deleteZone.result === DeletionResult.DELETED) {
-                        // refresh list
-                        return this.dataService.settings
-                            .getZones()
-                            .mapSingle(() => ({ errorMessage: false }));
-                    } else {
-                        return of({ errorMessage: result.deleteZone.message });
-                    }
-                }),
-            )
-            .subscribe(
-                result => {
-                    if (typeof result.errorMessage === 'string') {
-                        this.notificationService.error(result.errorMessage);
-                    } else {
-                        this.notificationService.success(_('common.notify-delete-success'), {
-                            entity: 'Zone',
-                        });
-                    }
-                },
-                err => {
-                    this.notificationService.error(_('common.notify-delete-error'), {
-                        entity: 'Zone',
-                    });
-                },
-            );
-    }
-
-    update(zone: GetZonesQuery['zones'][number]) {
+    update(zone: ItemOf<GetZoneListQuery, 'zones'>) {
         this.modalService
             .fromComponent(ZoneDetailDialogComponent, { locals: { zone } })
             .pipe(
@@ -158,7 +172,7 @@ export class ZoneListComponent implements OnInit {
         this.router.navigate(['./', params], { relativeTo: this.route, queryParamsHandling: 'preserve' });
     }
 
-    addToZone(zone: GetZonesQuery['zones'][number]) {
+    addToZone(zone: ItemOf<GetZoneListQuery, 'zones'>) {
         this.modalService
             .fromComponent(AddCountryToZoneDialogComponent, {
                 locals: {
@@ -188,15 +202,4 @@ export class ZoneListComponent implements OnInit {
                 },
             });
     }
-
-    removeFromZone(zone: GetZonesQuery['zones'][number], memberIds: string[]) {
-        this.dataService.settings.removeMembersFromZone(zone.id, memberIds).subscribe({
-            complete: () => {
-                this.notificationService.success(_(`settings.remove-countries-from-zone-success`), {
-                    countryCount: memberIds.length,
-                    zoneName: zone.name,
-                });
-            },
-        });
-    }
 }

+ 28 - 0
packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list-bulk-actions.ts

@@ -0,0 +1,28 @@
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { BulkAction, DataService, NotificationService, Permission } from '@vendure/admin-ui/core';
+import { ZoneMember, ZoneMemberListComponent } from '@vendure/admin-ui/settings';
+
+export const removeZoneMembersBulkAction: BulkAction<ZoneMember, ZoneMemberListComponent> = {
+    location: 'zone-members-list',
+    label: _('settings.remove-from-zone'),
+    icon: 'trash',
+    iconClass: 'is-danger',
+    requiresPermission: Permission.UpdateCustomerGroup,
+    onClick: ({ injector, selection, hostComponent, clearSelection }) => {
+        const dataService = injector.get(DataService);
+        const notificationService = injector.get(NotificationService);
+
+        const zone = hostComponent.activeZone;
+        const memberIds = selection.map(s => s.id);
+
+        dataService.settings.removeMembersFromZone(zone.id, memberIds).subscribe({
+            complete: () => {
+                notificationService.success(_(`settings.remove-countries-from-zone-success`), {
+                    countryCount: memberIds.length,
+                    zoneName: zone.name,
+                });
+                clearSelection();
+            },
+        });
+    },
+};

+ 52 - 38
packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.html

@@ -1,39 +1,53 @@
-<div class="members-header">
-    <ng-container *ngIf="headerTemplate">
-        <ng-container *ngTemplateOutlet="headerTemplate.templateRef"></ng-container>
-    </ng-container>
-    <input
-        type="text"
-        [placeholder]="'settings.filter-by-member-name' | translate"
-        [(ngModel)]="filterTerm"
-    />
-</div>
-<vdr-data-table
-    [items]="filteredMembers()"
-    [allSelected]="areAllSelected()"
-    [isRowSelectedFn]="(['UpdateSettings', 'UpdateZone'] | hasPermission) && isMemberSelected"
-    (rowSelectChange)="toggleSelectMember($event)"
-    (allSelectChange)="toggleSelectAll()"
+<vdr-data-table-2
+    [id]="locationId"
+    [items]="filteredMembers$ | async"
+    [totalItems]="totalItems$ | async"
+    [itemsPerPage]="itemsPerPage"
+    [currentPage]="currentPage"
+    (itemsPerPageChange)="itemsPerPage = $event"
+    (pageChange)="currentPage = $event"
 >
-    <vdr-dt-column>{{ 'common.code' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'common.enabled' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-member="item">
-        <td class="left align-middle">{{ member.code }}</td>
-        <td class="left align-middle">{{ member.name }}</td>
-        <td class="left align-middle">
-            <clr-icon
-                [class.is-success]="member.enabled"
-                [attr.shape]="member.enabled ? 'check' : 'times'"
-            ></clr-icon>
-        </td>
-        <td class="right align-middle">
-            <ng-container *ngIf="controlsTemplate">
-                <ng-container
-                    *ngTemplateOutlet="controlsTemplate.templateRef; context: { member: member }"
-                ></ng-container>
-            </ng-container>
-        </td>
-    </ng-template>
-</vdr-data-table>
+    <vdr-bulk-action-menu
+        [locationId]="locationId"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    ></vdr-bulk-action-menu>
+    <vdr-dt2-search
+        [searchTermControl]="filterTermControl"
+        [searchTermPlaceholder]="'common.search-by-name' | translate"
+    ></vdr-dt2-search>
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-member="item">
+            {{ member.id }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.created-at' | translate" [hiddenByDefault]="true">
+        <ng-template let-member="item">
+            {{ member.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.updated-at' | translate" [hiddenByDefault]="true">
+        <ng-template let-member="item">
+            {{ member.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false">
+        <ng-template let-member="item">
+            <a class="button-ghost" [routerLink]="['/settings/countries', member.id]"
+                ><span> {{ member.name }} {{ member.firstName }} {{ member.lastName }} </span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.code' | translate">
+        <ng-template let-member="item">
+            {{ member.code }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.enabled' | translate">
+        <ng-template let-member="item">
+            <div class="badge success" *ngIf="member.enabled">{{ 'common.enabled' }}</div>
+            <div class="badge warning" *ngIf="!member.enabled">{{ 'common.disabled' }}</div>
+        </ng-template>
+    </vdr-dt2-column>
+</vdr-data-table-2>

+ 61 - 33
packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.ts

@@ -1,4 +1,19 @@
-import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, Output } from '@angular/core';
+import {
+    ChangeDetectionStrategy,
+    Component,
+    ContentChild,
+    EventEmitter,
+    Input,
+    OnChanges,
+    OnDestroy,
+    OnInit,
+    Output,
+    SimpleChanges,
+} from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { BulkActionLocationId, GetZoneListQuery, ItemOf, SelectionManager } from '@vendure/admin-ui/core';
+import { Observable, Subject, switchMap } from 'rxjs';
+import { map, startWith, takeUntil } from 'rxjs/operators';
 
 import { ZoneMemberControlsDirective } from './zone-member-controls.directive';
 import { ZoneMemberListHeaderDirective } from './zone-member-list-header.directive';
@@ -11,48 +26,61 @@ export type ZoneMember = { id: string; name: string; code: string };
     styleUrls: ['./zone-member-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ZoneMemberListComponent {
+export class ZoneMemberListComponent implements OnInit, OnChanges, OnDestroy {
+    @Input() locationId: BulkActionLocationId;
     @Input() members: ZoneMember[] = [];
     @Input() selectedMemberIds: string[] = [];
+    @Input() activeZone: ItemOf<GetZoneListQuery, 'zones'>;
     @Output() selectionChange = new EventEmitter<string[]>();
     @ContentChild(ZoneMemberListHeaderDirective) headerTemplate: ZoneMemberListHeaderDirective;
     @ContentChild(ZoneMemberControlsDirective) controlsTemplate: ZoneMemberControlsDirective;
-    filterTerm = '';
+    filterTermControl = new FormControl('');
+    filteredMembers$: Observable<ZoneMember[]>;
+    totalItems$: Observable<number>;
+    currentPage = 1;
+    itemsPerPage = 10;
+    selectionManager = new SelectionManager<ZoneMember>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+    private members$ = new Subject<ZoneMember[]>();
+    private destroy$ = new Subject<void>();
 
-    filteredMembers(): ZoneMember[] {
-        if (this.filterTerm !== '') {
-            const term = this.filterTerm.toLocaleLowerCase();
-            return this.members.filter(
-                m => m.name.toLocaleLowerCase().includes(term) || m.code.toLocaleLowerCase().includes(term),
-            );
-        } else {
-            return this.members;
-        }
-    }
-
-    areAllSelected(): boolean {
-        if (this.members) {
-            return this.selectedMemberIds.length === this.members.length;
-        } else {
-            return false;
-        }
+    ngOnInit() {
+        this.selectionManager.setCurrentItems(
+            this.members?.filter(m => this.selectedMemberIds.includes(m.id)) ?? [],
+        );
+        this.selectionManager.selectionChanges$.pipe(takeUntil(this.destroy$)).subscribe(selection => {
+            this.selectionChange.emit(selection.map(s => s.id));
+        });
+        this.filteredMembers$ = this.members$.pipe(
+            startWith(this.members),
+            switchMap(() => this.filterTermControl.valueChanges.pipe(startWith(''))),
+            map(filterTerm => {
+                if (filterTerm) {
+                    const term = filterTerm?.toLocaleLowerCase() ?? '';
+                    return this.members.filter(
+                        m =>
+                            m.name.toLocaleLowerCase().includes(term) ||
+                            m.code.toLocaleLowerCase().includes(term),
+                    );
+                } else {
+                    return this.members;
+                }
+            }),
+        );
+        this.totalItems$ = this.filteredMembers$.pipe(map(members => members.length));
     }
 
-    toggleSelectAll() {
-        if (this.areAllSelected()) {
-            this.selectionChange.emit([]);
-        } else {
-            this.selectionChange.emit(this.members.map(v => v.id));
+    ngOnChanges(changes: SimpleChanges) {
+        if ('members' in changes) {
+            this.members$.next(this.members);
         }
     }
 
-    toggleSelectMember({ item: member }: { item: ZoneMember }) {
-        if (this.selectedMemberIds.includes(member.id)) {
-            this.selectionChange.emit(this.selectedMemberIds.filter(id => id !== member.id));
-        } else {
-            this.selectionChange.emit([...this.selectedMemberIds, member.id]);
-        }
+    ngOnDestroy() {
+        this.destroy$.next();
+        this.destroy$.complete();
     }
-
-    isMemberSelected = (member: ZoneMember): boolean => -1 < this.selectedMemberIds.indexOf(member.id);
 }

+ 4 - 0
packages/admin-ui/src/lib/settings/src/settings.module.ts

@@ -37,8 +37,10 @@ import { TaxRateListComponent } from './components/tax-rate-list/tax-rate-list.c
 import { TestAddressFormComponent } from './components/test-address-form/test-address-form.component';
 import { TestOrderBuilderComponent } from './components/test-order-builder/test-order-builder.component';
 import { ZoneDetailDialogComponent } from './components/zone-detail-dialog/zone-detail-dialog.component';
+import { deleteZonesBulkAction } from './components/zone-list/zone-list-bulk-actions';
 import { ZoneListComponent } from './components/zone-list/zone-list.component';
 import { ZoneMemberControlsDirective } from './components/zone-member-list/zone-member-controls.directive';
+import { removeZoneMembersBulkAction } from './components/zone-member-list/zone-member-list-bulk-actions';
 import { ZoneMemberListHeaderDirective } from './components/zone-member-list/zone-member-list-header.directive';
 import { ZoneMemberListComponent } from './components/zone-member-list/zone-member-list.component';
 import { settingsRoutes } from './settings.routes';
@@ -89,5 +91,7 @@ export class SettingsModule {
         bulkActionRegistryService.registerBulkAction(deleteTaxCategoriesBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteTaxRatesBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteCountriesBulkAction);
+        bulkActionRegistryService.registerBulkAction(deleteZonesBulkAction);
+        bulkActionRegistryService.registerBulkAction(removeZoneMembersBulkAction);
     }
 }

+ 31 - 0
packages/admin-ui/src/lib/static/styles/theme/dark.scss

@@ -23,6 +23,37 @@
     --color-text-300: var(--color-grey-300);
     --color-text-inverse: var(--clr-global-font-color);
 
+    --color-weight-100: hsl(0 0% 95%);
+    --color-weight-125: hsl(0 0% 93%);
+    --color-weight-150: hsl(0 0% 90%);
+    --color-weight-200: hsl(0 0% 85%);
+    --color-weight-300: hsl(0 0% 75%);
+    --color-weight-400: hsl(0 0% 65%);
+    --color-weight-500: hsl(0 0% 55%);
+    --color-weight-600: hsl(0 0% 45%);
+    --color-weight-700: hsl(0 0% 35%);
+    --color-weight-800: hsl(0 0% 25%);
+    --color-weight-900: hsl(0 0% 15%);
+    --color-weight-950: hsl(0 0% 10%);
+    --color-weight-975: hsl(0 0% 7%);
+    --color-weight-1000: hsl(0 0% 5%);
+
+
+    --color-weight-100: hsl(0 0% 5%);
+    --color-weight-125: hsl(0 0% 7%);
+    --color-weight-150: hsl(0 0% 10%);
+    --color-weight-200: hsl(0 0% 15%);
+    --color-weight-300: hsl(0 0% 25%);
+    --color-weight-400: hsl(0 0% 35%);
+    --color-weight-500: hsl(0 0% 45%);
+    --color-weight-600: hsl(0 0% 55%);
+    --color-weight-700: hsl(0 0% 65%);
+    --color-weight-800: hsl(0 0% 75%);
+    --color-weight-900: hsl(0 0% 85%);
+    --color-weight-950: hsl(0 0% 90%);
+    --color-weight-975: hsl(0 0% 93%);
+    --color-weight-1000: hsl(0 0% 95%);
+
     --color-chip-warning-border: var(--color-warning-700);
     --color-chip-warning-text: #fff;
     --color-chip-warning-bg: var(--color-warning-600);

+ 46 - 42
packages/admin-ui/src/lib/static/styles/theme/default.scss

@@ -14,47 +14,6 @@
     --color-grey-800: #202223;
     --color-grey-900: #0f1011;
 
-    // Universal semantic colors
-    --color-component-bg-100: var(--color-grey-100);
-    --color-component-bg-200: var(--color-grey-200);
-    --color-component-bg-300: var(--color-grey-300);
-    --color-component-border-100: var(--color-grey-200);
-    --color-component-border-200: var(--color-grey-300);
-    --color-component-border-300: var(--color-grey-400);
-    --color-text-100: var(--clr-global-font-color);
-    --color-text-200: var(--clr-global-font-color-secondary);
-    --color-text-300: var(--color-grey-400);
-    --color-text-inverse: white;
-
-
-
-    // Component-specific colors
-    --color-top-bar-bg: white;
-
-    --color-icon-button: var(--color-grey-600);
-    --color-form-input-bg: white;
-    --color-timeline-thread: var(--color-primary-100);
-
-    --color-chip-warning-border: var(--color-warning-200);
-    --color-chip-warning-text: var(--color-warning-600);
-    --color-chip-warning-bg: var(--color-warning-100);
-    --color-chip-success-border: var(--color-success-200);
-    --color-chip-success-text: var(--color-success-600);
-    --color-chip-success-bg: var(--color-success-100);
-    --color-chip-error-border: var(--color-error-200);
-    --color-chip-error-text: var(--color-error-600);
-    --color-chip-error-bg: var(--color-error-100);
-
-    --color-json-editor-background-color: var(--color-grey-200);
-    --color-json-editor-text: var(--color-grey-600);
-    --color-json-editor-string: var(--color-secondary-600);
-    --color-json-editor-number: var(--color-primary-600);
-    --color-json-editor-boolean: var(--color-primary-600);
-    --color-json-editor-null: var(--color-grey-500);
-    --color-json-editor-key: var(--color-success-500);
-    --color-json-editor-error: var(--color-error-500);
-
-
     --color-white: #fff;
     --color-black: #000;
     --color-weight-100: hsl(0 0% 95%);
@@ -151,7 +110,6 @@
     --color-left-nav-text: var(--color-text);
     --color-left-nav-text-hover: var(--color-primary-700);
 
-    --color-table-alternate-row-bg: hsl(0 0% 98% / 1);
 
     // Layout
     --layout-content-max-width: 1200px;
@@ -181,4 +139,50 @@
 
     // spacing
     --space-unit: 8px;
+
+
+    // Universal semantic colors
+    --color-component-bg-100: var(--color-grey-100);
+    --color-component-bg-200: var(--color-grey-200);
+    --color-component-bg-300: var(--color-grey-300);
+    --color-component-border-100: var(--color-grey-200);
+    --color-component-border-200: var(--color-grey-300);
+    --color-component-border-300: var(--color-grey-400);
+    --color-text-100: var(--clr-global-font-color);
+    --color-text-200: var(--clr-global-font-color-secondary);
+    --color-text-300: var(--color-grey-400);
+    --color-text-inverse: white;
+
+    // Component-specific colors
+    --color-top-bar-bg: white;
+
+    --color-icon-button: var(--color-grey-600);
+    --color-form-input-bg: white;
+    --color-timeline-thread: var(--color-primary-100);
+
+    --color-chip-warning-border: var(--color-warning-200);
+    --color-chip-warning-text: var(--color-warning-600);
+    --color-chip-warning-bg: var(--color-warning-100);
+    --color-chip-success-border: var(--color-success-200);
+    --color-chip-success-text: var(--color-success-600);
+    --color-chip-success-bg: var(--color-success-100);
+    --color-chip-error-border: var(--color-error-200);
+    --color-chip-error-text: var(--color-error-600);
+    --color-chip-error-bg: var(--color-error-100);
+
+    --color-json-editor-background-color: var(--color-grey-200);
+    --color-json-editor-text: var(--color-grey-600);
+    --color-json-editor-string: var(--color-secondary-600);
+    --color-json-editor-number: var(--color-primary-600);
+    --color-json-editor-boolean: var(--color-primary-600);
+    --color-json-editor-null: var(--color-grey-500);
+    --color-json-editor-key: var(--color-success-500);
+    --color-json-editor-error: var(--color-error-500);
+
+    --color-table-alternate-row-bg: hsl(0 0% 98% / 1);
+    --color-table-row-hover-bg: var(--color-weight-100);
+    --color-table-row-active-bg: var(--color-primary-100);
+
+    --color-split-view-separator-border: var(--color-weight-150);
+    --color-split-view-separator-resize-border: var(--color-primary-400);
 }