Browse Source

feat(admin-ui): Add support for custom fields in DataTable2

Michael Bromley 2 years ago
parent
commit
6428a77b90
29 changed files with 412 additions and 82 deletions
  1. 2 1
      packages/admin-ui/src/lib/catalog/src/components/collection-data-table/collection-data-table.component.html
  2. 3 1
      packages/admin-ui/src/lib/catalog/src/components/collection-data-table/collection-data-table.component.ts
  3. 9 9
      packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.html
  4. 1 0
      packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.ts
  5. 5 0
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html
  6. 5 1
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.ts
  7. 74 4
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  8. 27 20
      packages/admin-ui/src/lib/core/src/data/definitions/collection-definitions.ts
  9. 27 20
      packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts
  10. 49 1
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter-collection.ts
  11. 3 2
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter.ts
  12. 30 0
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort-collection.ts
  13. 5 2
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-column.component.ts
  14. 44 0
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.html
  15. 3 0
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.scss
  16. 42 0
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.ts
  17. 2 1
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.html
  18. 30 10
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.ts
  19. 2 0
      packages/admin-ui/src/lib/core/src/shared/components/data-table-column-picker/data-table-column-picker.component.ts
  20. 2 4
      packages/admin-ui/src/lib/core/src/shared/components/data-table-filter-label/data-table-filter-label.component.html
  21. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/data-table-filter-label/data-table-filter-label.component.ts
  22. 2 2
      packages/admin-ui/src/lib/core/src/shared/components/data-table-filters/data-table-filters.component.html
  23. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/data-table-filters/data-table-filters.component.ts
  24. 2 0
      packages/admin-ui/src/lib/core/src/shared/components/localized-text/localized-text.component.html
  25. 0 0
      packages/admin-ui/src/lib/core/src/shared/components/localized-text/localized-text.component.scss
  26. 22 0
      packages/admin-ui/src/lib/core/src/shared/components/localized-text/localized-text.component.ts
  27. 14 2
      packages/admin-ui/src/lib/core/src/shared/pipes/custom-field-label.pipe.ts
  28. 4 0
      packages/admin-ui/src/lib/core/src/shared/shared.module.ts
  29. 1 0
      packages/admin-ui/src/lib/order/src/order.module.ts

+ 2 - 1
packages/admin-ui/src/lib/catalog/src/components/collection-data-table/collection-data-table.component.html

@@ -33,7 +33,8 @@
 
                     <div *ngIf="isLast" class="column-picker">
                         <vdr-data-table-colum-picker
-                            [columns]="columns?.toArray()"
+                            [uiLanguage]="uiLanguage$ | async"
+                            [columns]="allColumns"
                         ></vdr-data-table-colum-picker>
                     </div>
                 </th>

+ 3 - 1
packages/admin-ui/src/lib/catalog/src/components/collection-data-table/collection-data-table.component.ts

@@ -13,6 +13,7 @@ import {
     ViewChildren,
 } from '@angular/core';
 import {
+    DataService,
     DataTable2Component,
     GetCollectionListQuery,
     ItemOf,
@@ -49,9 +50,10 @@ export class CollectionDataTableComponent
     constructor(
         protected changeDetectorRef: ChangeDetectorRef,
         protected localStorageService: LocalStorageService,
+        protected dataService: DataService,
         private dragDrop: DragDrop,
     ) {
-        super(changeDetectorRef, localStorageService);
+        super(changeDetectorRef, localStorageService, dataService);
     }
 
     ngOnInit() {

+ 9 - 9
packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.html

@@ -95,10 +95,7 @@
                             *ngIf="collection.children?.length"
                             (click)="toggleExpanded(collection)"
                         >
-                            <clr-icon
-                                shape="folder"
-                                *ngIf="!expandedIds.includes(collection.id)"
-                            ></clr-icon>
+                            <clr-icon shape="folder" *ngIf="!expandedIds.includes(collection.id)"></clr-icon>
                             <clr-icon
                                 shape="folder-open"
                                 *ngIf="expandedIds.includes(collection.id)"
@@ -129,10 +126,7 @@
                         {{ collection.slug }}
                     </ng-template>
                 </vdr-dt2-column>
-                <vdr-dt2-column
-                    [heading]="'common.view-contents' | translate"
-                    [optional]="false"
-                >
+                <vdr-dt2-column [heading]="'common.view-contents' | translate" [optional]="false">
                     <ng-template let-collection="item">
                         <a
                             class="button-small bg-weight-150"
@@ -144,11 +138,17 @@
                         </a>
                     </ng-template>
                 </vdr-dt2-column>
+                <vdr-dt2-custom-field-column
+                    *ngFor="let customField of customFields"
+                    [customField]="customField"
+                />
             </vdr-collection-data-table>
         </ng-template>
         <ng-template vdrSplitViewRight [splitViewTitle]="activeCollectionTitle$ | async">
             <ng-container *ngIf="activeCollectionId$ | async as activeGroup">
-                <vdr-collection-contents [collectionId]="activeCollectionId$ | async"></vdr-collection-contents>
+                <vdr-collection-contents
+                    [collectionId]="activeCollectionId$ | async"
+                ></vdr-collection-contents>
             </ng-container>
         </ng-template>
     </vdr-split-view>

+ 1 - 0
packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.ts

@@ -35,6 +35,7 @@ export class CollectionListComponent
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
     expandedIds: string[] = [];
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Collection');
 
     readonly filters = this.dataTableService
         .createFilterCollection<CollectionFilterParameter>()

+ 5 - 0
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html

@@ -144,4 +144,9 @@
             {{ 'catalog.variant-count' | translate : { count: product.variantList?.totalItems } }}
         </ng-template>
     </vdr-dt2-column>
+    <vdr-dt2-custom-field-column
+        *ngFor="let customField of customFields"
+        [customField]="customField"
+        [sorts]="sorts"
+    />
 </vdr-data-table-2>

+ 5 - 1
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.ts

@@ -38,6 +38,8 @@ export class ProductListComponent
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
     pendingSearchIndexUpdates = 0;
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Product');
+
     readonly filters = this.dataTableService
         .createFilterCollection<ProductFilterParameter>()
         .addDateFilters()
@@ -90,6 +92,7 @@ export class ProductListComponent
                 },
             }),
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -100,6 +103,7 @@ export class ProductListComponent
         .addSort({ name: 'updatedAt' })
         .addSort({ name: 'name' })
         .addSort({ name: 'slug' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
@@ -107,9 +111,9 @@ export class ProductListComponent
         private modalService: ModalService,
         private notificationService: NotificationService,
         private jobQueueService: JobQueueService,
-        private serverConfigService: ServerConfigService,
         private dataTableService: DataTableService,
         private navBuilderService: NavBuilderService,
+        private serverConfigService: ServerConfigService,
         router: Router,
         route: ActivatedRoute,
     ) {

+ 74 - 4
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -793,9 +793,20 @@ export type CreatePaymentMethodInput = {
   translations: Array<PaymentMethodTranslationInput>;
 };
 
+export type CreateProductCustomFieldsInput = {
+  boost?: InputMaybe<Scalars['Int']>;
+  keyFeatures?: InputMaybe<Scalars['String']>;
+  minimumOrderQuantity?: InputMaybe<Scalars['Int']>;
+  pageType?: InputMaybe<Scalars['String']>;
+  searchKeywords?: InputMaybe<Scalars['String']>;
+  seoImageId?: InputMaybe<Scalars['ID']>;
+  variantOrdering?: InputMaybe<Scalars['String']>;
+  videoUrls?: InputMaybe<Array<Scalars['String']>>;
+};
+
 export type CreateProductInput = {
   assetIds?: InputMaybe<Array<Scalars['ID']>>;
-  customFields?: InputMaybe<Scalars['JSON']>;
+  customFields?: InputMaybe<CreateProductCustomFieldsInput>;
   enabled?: InputMaybe<Scalars['Boolean']>;
   facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
   featuredAssetId?: InputMaybe<Scalars['ID']>;
@@ -4392,7 +4403,7 @@ export type Product = Node & {
   channels: Array<Channel>;
   collections: Array<Collection>;
   createdAt: Scalars['DateTime'];
-  customFields?: Maybe<Scalars['JSON']>;
+  customFields?: Maybe<ProductCustomFields>;
   description: Scalars['String'];
   enabled: Scalars['Boolean'];
   facetValues: Array<FacetValue>;
@@ -4415,16 +4426,39 @@ export type ProductVariantListArgs = {
   options?: InputMaybe<ProductVariantListOptions>;
 };
 
+export type ProductCustomFields = {
+  __typename?: 'ProductCustomFields';
+  boost?: Maybe<Scalars['Int']>;
+  keyFeatures?: Maybe<Scalars['String']>;
+  minimumOrderQuantity?: Maybe<Scalars['Int']>;
+  pageType?: Maybe<Scalars['String']>;
+  searchKeywords?: Maybe<Scalars['String']>;
+  seoDescription?: Maybe<Scalars['String']>;
+  seoImage?: Maybe<Asset>;
+  seoTitle?: Maybe<Scalars['String']>;
+  variantOrdering?: Maybe<Scalars['String']>;
+  videoUrls?: Maybe<Array<Scalars['String']>>;
+};
+
 export type ProductFilterParameter = {
+  boost?: InputMaybe<NumberOperators>;
   createdAt?: InputMaybe<DateOperators>;
   description?: InputMaybe<StringOperators>;
   enabled?: InputMaybe<BooleanOperators>;
   facetValueId?: InputMaybe<IdOperators>;
   id?: InputMaybe<IdOperators>;
+  keyFeatures?: InputMaybe<StringOperators>;
   languageCode?: InputMaybe<StringOperators>;
+  minimumOrderQuantity?: InputMaybe<NumberOperators>;
   name?: InputMaybe<StringOperators>;
+  pageType?: InputMaybe<StringOperators>;
+  searchKeywords?: InputMaybe<StringOperators>;
+  seoDescription?: InputMaybe<StringOperators>;
+  seoTitle?: InputMaybe<StringOperators>;
   slug?: InputMaybe<StringOperators>;
   updatedAt?: InputMaybe<DateOperators>;
+  variantOrdering?: InputMaybe<StringOperators>;
+  videoUrls?: InputMaybe<StringListOperators>;
 };
 
 export type ProductList = PaginatedList & {
@@ -4514,17 +4548,27 @@ export type ProductOptionTranslationInput = {
 };
 
 export type ProductSortParameter = {
+  boost?: InputMaybe<SortOrder>;
   createdAt?: InputMaybe<SortOrder>;
   description?: InputMaybe<SortOrder>;
   id?: InputMaybe<SortOrder>;
+  keyFeatures?: InputMaybe<SortOrder>;
+  minimumOrderQuantity?: InputMaybe<SortOrder>;
   name?: InputMaybe<SortOrder>;
+  pageType?: InputMaybe<SortOrder>;
+  searchKeywords?: InputMaybe<SortOrder>;
+  seoDescription?: InputMaybe<SortOrder>;
+  seoImage?: InputMaybe<SortOrder>;
+  seoTitle?: InputMaybe<SortOrder>;
   slug?: InputMaybe<SortOrder>;
   updatedAt?: InputMaybe<SortOrder>;
+  variantOrdering?: InputMaybe<SortOrder>;
 };
 
 export type ProductTranslation = {
   __typename?: 'ProductTranslation';
   createdAt: Scalars['DateTime'];
+  customFields?: Maybe<ProductTranslationCustomFields>;
   description: Scalars['String'];
   id: Scalars['ID'];
   languageCode: LanguageCode;
@@ -4533,8 +4577,14 @@ export type ProductTranslation = {
   updatedAt: Scalars['DateTime'];
 };
 
+export type ProductTranslationCustomFields = {
+  __typename?: 'ProductTranslationCustomFields';
+  seoDescription?: Maybe<Scalars['String']>;
+  seoTitle?: Maybe<Scalars['String']>;
+};
+
 export type ProductTranslationInput = {
-  customFields?: InputMaybe<Scalars['JSON']>;
+  customFields?: InputMaybe<ProductTranslationInputCustomFields>;
   description?: InputMaybe<Scalars['String']>;
   id?: InputMaybe<Scalars['ID']>;
   languageCode: LanguageCode;
@@ -4542,6 +4592,11 @@ export type ProductTranslationInput = {
   slug?: InputMaybe<Scalars['String']>;
 };
 
+export type ProductTranslationInputCustomFields = {
+  seoDescription?: InputMaybe<Scalars['String']>;
+  seoTitle?: InputMaybe<Scalars['String']>;
+};
+
 export type ProductVariant = Node & {
   __typename?: 'ProductVariant';
   assets: Array<Asset>;
@@ -6173,9 +6228,20 @@ export type UpdatePaymentMethodInput = {
   translations?: InputMaybe<Array<PaymentMethodTranslationInput>>;
 };
 
+export type UpdateProductCustomFieldsInput = {
+  boost?: InputMaybe<Scalars['Int']>;
+  keyFeatures?: InputMaybe<Scalars['String']>;
+  minimumOrderQuantity?: InputMaybe<Scalars['Int']>;
+  pageType?: InputMaybe<Scalars['String']>;
+  searchKeywords?: InputMaybe<Scalars['String']>;
+  seoImageId?: InputMaybe<Scalars['ID']>;
+  variantOrdering?: InputMaybe<Scalars['String']>;
+  videoUrls?: InputMaybe<Array<Scalars['String']>>;
+};
+
 export type UpdateProductInput = {
   assetIds?: InputMaybe<Array<Scalars['ID']>>;
-  customFields?: InputMaybe<Scalars['JSON']>;
+  customFields?: InputMaybe<UpdateProductCustomFieldsInput>;
   enabled?: InputMaybe<Scalars['Boolean']>;
   facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
   featuredAssetId?: InputMaybe<Scalars['ID']>;
@@ -6631,6 +6697,8 @@ export type GetCollectionFiltersQuery = { collectionFilters: Array<{ __typename?
 
 export type CollectionFragment = { __typename?: 'Collection', id: string, createdAt: any, updatedAt: any, name: string, slug: string, description: string, isPrivate: boolean, languageCode?: LanguageCode | null, inheritFilters: boolean, breadcrumbs: Array<{ __typename?: 'CollectionBreadcrumb', id: string, name: string, slug: string }>, featuredAsset?: { __typename?: 'Asset', id: string, createdAt: any, updatedAt: any, name: string, fileSize: number, mimeType: string, type: AssetType, preview: string, source: string, width: number, height: number, focalPoint?: { __typename?: 'Coordinate', x: number, y: number } | null } | null, assets: Array<{ __typename?: 'Asset', id: string, createdAt: any, updatedAt: any, name: string, fileSize: number, mimeType: string, type: AssetType, preview: string, source: string, width: number, height: number, focalPoint?: { __typename?: 'Coordinate', x: number, y: number } | null }>, filters: Array<{ __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }>, translations: Array<{ __typename?: 'CollectionTranslation', id: string, languageCode: LanguageCode, name: string, slug: string, description: string }>, parent?: { __typename?: 'Collection', id: string, name: string } | null, children?: Array<{ __typename?: 'Collection', id: string, name: string }> | null };
 
+export type CollectionForListFragment = { __typename?: 'Collection', id: string, createdAt: any, updatedAt: any, name: string, slug: string, position: number, isPrivate: boolean, parentId: string, breadcrumbs: Array<{ __typename?: 'CollectionBreadcrumb', id: string, name: string, slug: string }>, featuredAsset?: { __typename?: 'Asset', id: string, createdAt: any, updatedAt: any, name: string, fileSize: number, mimeType: string, type: AssetType, preview: string, source: string, width: number, height: number, focalPoint?: { __typename?: 'Coordinate', x: number, y: number } | null } | null, children?: Array<{ __typename?: 'Collection', id: string }> | null };
+
 export type GetCollectionListQueryVariables = Exact<{
   options?: InputMaybe<CollectionListOptions>;
 }>;
@@ -7319,6 +7387,8 @@ export type GetProductSimpleQueryVariables = Exact<{
 
 export type GetProductSimpleQuery = { product?: { __typename?: 'Product', id: string, name: string, featuredAsset?: { __typename?: 'Asset', id: string, createdAt: any, updatedAt: any, name: string, fileSize: number, mimeType: string, type: AssetType, preview: string, source: string, width: number, height: number, focalPoint?: { __typename?: 'Coordinate', x: number, y: number } | null } | null } | null };
 
+export type ProductForListFragment = { __typename?: 'Product', id: string, createdAt: any, updatedAt: any, enabled: boolean, languageCode: LanguageCode, name: string, slug: string, featuredAsset?: { __typename?: 'Asset', id: string, createdAt: any, updatedAt: any, preview: string, focalPoint?: { __typename?: 'Coordinate', x: number, y: number } | null } | null, variantList: { __typename?: 'ProductVariantList', totalItems: number } };
+
 export type GetProductListQueryVariables = Exact<{
   options?: InputMaybe<ProductListOptions>;
 }>;

+ 27 - 20
packages/admin-ui/src/lib/core/src/data/definitions/collection-definitions.ts

@@ -57,34 +57,41 @@ export const COLLECTION_FRAGMENT = gql`
     ${CONFIGURABLE_OPERATION_FRAGMENT}
 `;
 
+export const COLLECTION_FOR_LIST_FRAGMENT = gql`
+    fragment CollectionForList on Collection {
+        id
+        createdAt
+        updatedAt
+        name
+        slug
+        position
+        isPrivate
+        breadcrumbs {
+            id
+            name
+            slug
+        }
+        featuredAsset {
+            ...Asset
+        }
+        parentId
+        children {
+            id
+        }
+    }
+    ${ASSET_FRAGMENT}
+`;
+
 export const GET_COLLECTION_LIST = gql`
     query GetCollectionList($options: CollectionListOptions) {
         collections(options: $options) {
             items {
-                id
-                createdAt
-                updatedAt
-                name
-                slug
-                position
-                isPrivate
-                breadcrumbs {
-                    id
-                    name
-                    slug
-                }
-                featuredAsset {
-                    ...Asset
-                }
-                parentId
-                children {
-                    id
-                }
+                ...CollectionForList
             }
             totalItems
         }
     }
-    ${ASSET_FRAGMENT}
+    ${COLLECTION_FOR_LIST_FRAGMENT}
 `;
 
 export const GET_COLLECTION = gql`

+ 27 - 20
packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts

@@ -364,34 +364,41 @@ export const GET_PRODUCT_SIMPLE = gql`
     ${ASSET_FRAGMENT}
 `;
 
+export const PRODUCT_FOR_LIST_FRAGMENT = gql`
+    fragment ProductForList on Product {
+        id
+        createdAt
+        updatedAt
+        enabled
+        languageCode
+        name
+        slug
+        featuredAsset {
+            id
+            createdAt
+            updatedAt
+            preview
+            focalPoint {
+                x
+                y
+            }
+        }
+        variantList {
+            totalItems
+        }
+    }
+`;
+
 export const GET_PRODUCT_LIST = gql`
     query GetProductList($options: ProductListOptions) {
         products(options: $options) {
             items {
-                id
-                createdAt
-                updatedAt
-                enabled
-                languageCode
-                name
-                slug
-                featuredAsset {
-                    id
-                    createdAt
-                    updatedAt
-                    preview
-                    focalPoint {
-                        x
-                        y
-                    }
-                }
-                variantList {
-                    totalItems
-                }
+                ...ProductForList
             }
             totalItems
         }
     }
+    ${PRODUCT_FOR_LIST_FRAGMENT}
 `;
 
 export const GET_PRODUCT_OPTION_GROUPS = gql`

+ 49 - 1
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter-collection.ts

@@ -1,9 +1,15 @@
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { CustomFieldType } from '@vendure/common/lib/shared-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { Subject } from 'rxjs';
 import extend from 'just-extend';
-import { DateOperators, NumberOperators, StringOperators } from '../../common/generated-types';
+import {
+    CustomFieldConfig,
+    DateOperators,
+    NumberOperators,
+    StringOperators,
+} from '../../common/generated-types';
 import {
     DataTableFilter,
     DataTableFilterBooleanType,
@@ -118,6 +124,48 @@ export class DataTableFilterCollection<FilterInput extends Record<string, any> =
         return this as any;
     }
 
+    addCustomFieldFilters(customFields: CustomFieldConfig[]) {
+        for (const config of customFields) {
+            const type = config.type as CustomFieldType;
+            if (config.list) {
+                continue;
+            }
+            let filterType: DataTableFilterType | undefined;
+            switch (type) {
+                case 'boolean':
+                    filterType = { kind: 'boolean' };
+                    break;
+                case 'int':
+                case 'float':
+                    filterType = { kind: 'number' };
+                    break;
+                case 'datetime':
+                    filterType = { kind: 'dateRange' };
+                    break;
+                case 'string':
+                case 'localeString':
+                case 'localeText':
+                case 'text':
+                    filterType = { kind: 'text' };
+                    break;
+                case 'relation':
+                    // Cannot sort relations
+                    break;
+                default:
+                    assertNever(type);
+            }
+            if (filterType) {
+                this.addFilter({
+                    name: config.name,
+                    type: filterType,
+                    label: config.label ?? config.name,
+                    filterField: config.name,
+                });
+            }
+        }
+        return this;
+    }
+
     getFilter(name: string): DataTableFilter<FilterInput> | undefined {
         return this.#filters.find(f => f.name === name);
     }

+ 3 - 2
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter.ts

@@ -1,4 +1,5 @@
 import { Type as ComponentType } from '@angular/core';
+import { LocalizedString } from '@vendure/common/lib/generated-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { FormInputComponent } from '../../common/component-registry-types';
 import {
@@ -67,7 +68,7 @@ export interface DataTableFilterOptions<
 > {
     readonly name: string;
     readonly type: Type;
-    readonly label: string;
+    readonly label: string | LocalizedString[];
     readonly filterField?: keyof FilterInput;
     readonly toFilterInput?: (value: DataTableFilterValue<Type>) => Partial<FilterInput>;
 }
@@ -95,7 +96,7 @@ export class DataTableFilter<
         return this.options.type;
     }
 
-    get label(): string {
+    get label(): string | LocalizedString[] {
         return this.options.label;
     }
 

+ 30 - 0
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort-collection.ts

@@ -1,5 +1,8 @@
 import { ActivatedRoute, Router } from '@angular/router';
+import { CustomFieldType } from '@vendure/common/lib/shared-types';
+import { assertNever } from '@vendure/common/lib/shared-utils';
 import { Subject } from 'rxjs';
+import { CustomFieldConfig } from '../../common/generated-types';
 import { DataTableSort, DataTableSortOptions, DataTableSortOrder } from './data-table-sort';
 
 export class DataTableSortCollection<
@@ -31,6 +34,33 @@ export class DataTableSortCollection<
         return this as unknown as DataTableSortCollection<SortInput, [...Names, Name]>;
     }
 
+    addCustomFieldSorts(customFields: CustomFieldConfig[]) {
+        for (const config of customFields) {
+            const type = config.type as CustomFieldType;
+            if (config.list) {
+                continue;
+            }
+            switch (type) {
+                case 'string':
+                case 'localeString':
+                case 'boolean':
+                case 'int':
+                case 'float':
+                case 'datetime':
+                case 'localeText':
+                case 'text':
+                    this.addSort({ name: config.name });
+                    break;
+                case 'relation':
+                    // Cannot sort relations
+                    break;
+                default:
+                    assertNever(type);
+            }
+        }
+        return this;
+    }
+
     defaultSort(name: keyof SortInput, sortOrder: DataTableSortOrder) {
         this.#defaultSort = { name, sortOrder };
         return this;

+ 5 - 2
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-column.component.ts

@@ -1,4 +1,5 @@
 import { Component, ContentChild, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { LocalizedString } from '../../../common/generated-types';
 import { DataTableSort } from '../../../providers/data-table/data-table-sort';
 
 @Component({
@@ -13,16 +14,18 @@ export class DataTable2ColumnComponent<T> implements OnInit {
     @Input() expand = false;
     @Input() heading: string;
     @Input() align: 'left' | 'right' | 'center' = 'left';
-    @Input() sort: DataTableSort<any>;
+    @Input() sort?: DataTableSort<any>;
     @Input() optional = true;
     @Input() hiddenByDefault = false;
     #visible = true;
     #onColumnChangeFns: Array<() => void> = [];
+    get id(): string {
+        return this.heading.toLowerCase().replace(/ /g, '-');
+    }
     get visible() {
         return this.#visible;
     }
     @ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
-    item: T;
 
     ngOnInit() {
         this.#visible = this.hiddenByDefault ? false : true;

+ 44 - 0
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.html

@@ -0,0 +1,44 @@
+<ng-template let-item="item">
+    <ng-container
+        *ngIf="item.customFields[customField.name] == null || item.customFields[customField.name] === ''"
+    >
+        <span class="empty">-</span>
+    </ng-container>
+    <ng-container *ngIf="item.customFields[customField.name] != null">
+        <ng-container [ngSwitch]="customField.type">
+            <ng-container *ngSwitchCase="'boolean'">
+                <clr-icon
+                    *ngIf="item.customFields[customField.name]"
+                    shape="check"
+                    class="color-success-700"
+                ></clr-icon>
+                <clr-icon *ngIf="!item.customFields[customField.name]" shape="times"></clr-icon>
+            </ng-container>
+            <ng-container *ngSwitchCase="'datetime'">
+                {{ item.customFields[customField.name] | localeDate }}
+            </ng-container>
+            <ng-container *ngSwitchCase="'text'">
+                {{ item.customFields[customField.name] | slice : 0 : 50 }}
+            </ng-container>
+            <ng-container *ngSwitchCase="'relation'">
+                <vdr-dropdown>
+                    <button
+                        class="btn btn-link btn-icon"
+                        vdrDropdownTrigger
+                        [title]="'common.info' | translate"
+                    >
+                        <clr-icon shape="details"></clr-icon>
+                    </button>
+                    <vdr-dropdown-menu>
+                        <div class="result-detail">
+                            <vdr-object-tree [value]="item.customFields[customField.name]"></vdr-object-tree>
+                        </div>
+                    </vdr-dropdown-menu>
+                </vdr-dropdown>
+            </ng-container>
+            <ng-container *ngSwitchDefault>
+                {{ item.customFields[customField.name] }}
+            </ng-container>
+        </ng-container>
+    </ng-container>
+</ng-template>

+ 3 - 0
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.scss

@@ -0,0 +1,3 @@
+.empty {
+    color: var(--color-weight-300);
+}

+ 42 - 0
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.ts

@@ -0,0 +1,42 @@
+import { Component, ContentChild, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { DataService, DataTableSortCollection, LanguageCode } from '@vendure/admin-ui/core';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { CustomFieldConfig } from '../../../common/generated-types';
+import { DataTable2ColumnComponent } from './data-table-column.component';
+
+@Component({
+    selector: 'vdr-dt2-custom-field-column',
+    templateUrl: './data-table-custom-field-column.component.html',
+    styleUrls: ['./data-table-custom-field-column.component.scss'],
+    exportAs: 'row',
+})
+export class DataTableCustomFieldColumnComponent<T> extends DataTable2ColumnComponent<T> implements OnInit {
+    @Input() customField: CustomFieldConfig;
+    @Input() sorts: DataTableSortCollection<any, any[]>;
+    @ViewChild(TemplateRef, { static: false }) template: TemplateRef<any>;
+    protected uiLanguage$: Observable<LanguageCode>;
+    constructor(protected dataService: DataService) {
+        super();
+        this.uiLanguage$ = this.dataService.client
+            .uiState()
+            .stream$.pipe(map(({ uiState }) => uiState.language));
+    }
+
+    get id(): string {
+        return this.customField.name.toLowerCase().replace(/ /g, '-');
+    }
+
+    ngOnInit() {
+        this.uiLanguage$.subscribe(uiLanguage => {
+            this.heading =
+                Array.isArray(this.customField.label) && this.customField.label.length > 0
+                    ? this.customField.label.find(l => l.languageCode === uiLanguage)?.value ??
+                      this.customField.name
+                    : this.customField.name;
+        });
+        this.hiddenByDefault = true;
+        this.sort = this.sorts.get(this.customField.name);
+        super.ngOnInit();
+    }
+}

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

@@ -32,7 +32,8 @@
 
                     <div *ngIf="isLast" class="column-picker">
                         <vdr-data-table-colum-picker
-                            [columns]="columns?.toArray()"
+                            [uiLanguage]="uiLanguage$ | async"
+                            [columns]="allColumns"
                         ></vdr-data-table-colum-picker>
                     </div>
                 </th>

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

@@ -15,14 +15,19 @@ import {
     SimpleChanges,
     TemplateRef,
 } from '@angular/core';
-import { FormControl } from '@angular/forms';
-import { BulkActionMenuComponent, LocalStorageService } from '@vendure/admin-ui/core';
+import {
+    BulkActionMenuComponent,
+    DataService,
+    LanguageCode,
+    LocalStorageService,
+} from '@vendure/admin-ui/core';
 import { PaginationService } from 'ngx-pagination';
-import { Subscription } from 'rxjs';
-import { SelectionManager } from '../../../common/utilities/selection-manager';
+import { Observable, Subscription } from 'rxjs';
+import { map } from 'rxjs/operators';
 import { DataTableFilterCollection } from '../../../providers/data-table/data-table-filter-collection';
 
 import { DataTable2ColumnComponent } from './data-table-column.component';
+import { DataTableCustomFieldColumnComponent } from './data-table-custom-field-column.component';
 import { DataTable2SearchComponent } from './data-table-search.component';
 
 /**
@@ -104,6 +109,8 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnIn
     @Output() itemsPerPageChange = new EventEmitter<number>();
 
     @ContentChildren(DataTable2ColumnComponent) columns: QueryList<DataTable2ColumnComponent<T>>;
+    @ContentChildren(DataTableCustomFieldColumnComponent)
+    customFieldColumns: QueryList<DataTableCustomFieldColumnComponent<T>>;
     @ContentChild(DataTable2SearchComponent) searchComponent: DataTable2SearchComponent;
     @ContentChild(BulkActionMenuComponent) bulkActionMenuComponent: BulkActionMenuComponent;
     @ContentChild('vdrDt2CustomSearch') customSearchTemplate: TemplateRef<any>;
@@ -115,19 +122,32 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnIn
     // which allows shift-click multi-row selection
     disableSelect = false;
     showSearchFilterRow = false;
+    protected uiLanguage$: Observable<LanguageCode>;
     private subscription: Subscription | undefined;
 
     constructor(
         protected changeDetectorRef: ChangeDetectorRef,
         protected localStorageService: LocalStorageService,
-    ) {}
+        protected dataService: DataService,
+    ) {
+        this.uiLanguage$ = this.dataService.client
+            .uiState()
+            .stream$.pipe(map(({ uiState }) => uiState.language));
+    }
 
     get selectionManager() {
         return this.bulkActionMenuComponent?.selectionManager;
     }
 
+    get allColumns() {
+        return [...(this.columns ?? []), ...(this.customFieldColumns ?? [])];
+    }
+
     get visibleColumns() {
-        return this.columns?.filter(c => c.visible) ?? [];
+        return [
+            ...(this.columns?.filter(c => c.visible) ?? []),
+            ...(this.customFieldColumns?.filter(c => c.visible) ?? []),
+        ];
     }
 
     private shiftDownHandler = (event: KeyboardEvent) => {
@@ -177,14 +197,14 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnIn
             if (!dataTableConfig[this.id]) {
                 dataTableConfig[this.id] = { visibility: [], showSearchFilterRow: false };
             }
-            dataTableConfig[this.id].visibility = this.columns
+            dataTableConfig[this.id].visibility = this.allColumns
                 .filter(c => (c.visible && c.hiddenByDefault) || (!c.visible && !c.hiddenByDefault))
-                .map(c => c.heading);
+                .map(c => c.id);
             this.localStorageService.set('dataTableConfig', dataTableConfig);
         };
 
-        this.columns.forEach(column => {
-            if (dataTableConfig?.[this.id]?.visibility.includes(column.heading)) {
+        this.allColumns.forEach(column => {
+            if (dataTableConfig?.[this.id]?.visibility.includes(column.id)) {
                 column.setVisibility(column.hiddenByDefault);
             }
             column.onColumnChange(updateColumnVisibility);

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

@@ -1,4 +1,5 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { LanguageCode } from '../../../common/generated-types';
 import { DataTable2ColumnComponent } from '../data-table-2/data-table-column.component';
 
 @Component({
@@ -9,6 +10,7 @@ import { DataTable2ColumnComponent } from '../data-table-2/data-table-column.com
 })
 export class DataTableColumnPickerComponent {
     @Input() columns: Array<DataTable2ColumnComponent<any>>;
+    @Input() uiLanguage: LanguageCode;
 
     toggleColumn(column: DataTable2ColumnComponent<any>) {
         column.setVisibility(!column.visible);

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

@@ -1,4 +1,4 @@
-<span>{{ filterWithValue.filter.label | translate }}:</span>
+<span><vdr-localized-text [text]="filterWithValue.filter.label" />:</span>
 <div>
     <ng-container *ngIf="filterWithValue.isSelect()">
         {{ filterWithValue.value?.join(', ') }}
@@ -7,9 +7,7 @@
         <span *ngIf="filterWithValue.value?.operator === 'contains'">{{
             'common.operator-contains' | translate
         }}</span>
-        <span *ngIf="filterWithValue.value?.operator === 'eq'">{{
-            'common.operator-eq' | translate
-        }}</span>
+        <span *ngIf="filterWithValue.value?.operator === 'eq'">{{ 'common.operator-eq' | translate }}</span>
         <span *ngIf="filterWithValue.value?.operator === 'notContains'">{{
             'common.operator-notContains' | translate
         }}</span>

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/data-table-filter-label/data-table-filter-label.component.ts

@@ -1,5 +1,6 @@
 import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 import { from, merge, Observable, of, Subject, switchMap } from 'rxjs';
+import { LanguageCode, LocalizedString } from '../../../common/generated-types';
 import { DataTableFilter } from '../../../providers/data-table/data-table-filter';
 import { FilterWithValue } from '../../../providers/data-table/data-table-filter-collection';
 
@@ -11,7 +12,6 @@ import { FilterWithValue } from '../../../providers/data-table/data-table-filter
 })
 export class DataTableFilterLabelComponent implements OnInit {
     @Input() filterWithValue: FilterWithValue;
-
     protected customFilterLabel$?: Observable<string>;
 
     ngOnInit() {

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

@@ -19,13 +19,13 @@
             <div class="filter-heading">Filter by:</div>
             <div *ngFor="let filter of filters.getFilters()">
                 <button vdrDropdownItem (click)="selectFilter(filter)">
-                    {{ filter.label | translate }}
+                    <vdr-localized-text [text]="filter?.label" />
                 </button>
             </div>
         </div>
 
         <div class="filter-heading" *ngIf="selectedFilter">
-            Filter by {{ selectedFilter.label | translate }}:
+            Filter by <vdr-localized-text [text]="selectedFilter.label" />:
         </div>
         <div class="mx-2 mt-1">
             <div vdrCustomFilterComponentHost #customComponentHost></div>

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

@@ -10,7 +10,7 @@ import {
 import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { FormInputComponent } from '../../../common/component-registry-types';
-import { DateOperators } from '../../../common/generated-types';
+import { DateOperators, LanguageCode } from '../../../common/generated-types';
 import { DataTableFilter, KindValueMap } from '../../../providers/data-table/data-table-filter';
 import {
     DataTableFilterCollection,

+ 2 - 0
packages/admin-ui/src/lib/core/src/shared/components/localized-text/localized-text.component.html

@@ -0,0 +1,2 @@
+<ng-container *ngIf="isString(text)">{{ text | translate }}</ng-container>
+<ng-container *ngIf="!isString(text)">{{ text | customFieldLabel : (uiLanguage$ | async) }}</ng-container>

+ 0 - 0
packages/admin-ui/src/lib/core/src/shared/components/localized-text/localized-text.component.scss


+ 22 - 0
packages/admin-ui/src/lib/core/src/shared/components/localized-text/localized-text.component.ts

@@ -0,0 +1,22 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { DataService } from '@vendure/admin-ui/core';
+import { Observable } from 'rxjs';
+import { LanguageCode, LocalizedString } from '../../../common/generated-types';
+
+@Component({
+    selector: 'vdr-localized-text',
+    templateUrl: './localized-text.component.html',
+    styleUrls: ['./localized-text.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class LocalizedTextComponent {
+    @Input() text: LocalizedString[] | string;
+    uiLanguage$: Observable<LanguageCode>;
+    constructor(private dataService: DataService) {
+        this.uiLanguage$ = this.dataService.client.uiState().mapStream(data => data.uiState.language);
+    }
+
+    isString(value: string | LocalizedString[]): value is string {
+        return typeof value === 'string';
+    }
+}

+ 14 - 2
packages/admin-ui/src/lib/core/src/shared/pipes/custom-field-label.pipe.ts

@@ -1,6 +1,11 @@
 import { Pipe, PipeTransform } from '@angular/core';
 
-import { CustomFieldConfig, LanguageCode, StringFieldOption } from '../../common/generated-types';
+import {
+    CustomFieldConfig,
+    LanguageCode,
+    LocalizedString,
+    StringFieldOption,
+} from '../../common/generated-types';
 
 /**
  * Displays a localized label for a CustomField or StringFieldOption, falling back to the
@@ -11,10 +16,17 @@ import { CustomFieldConfig, LanguageCode, StringFieldOption } from '../../common
     pure: true,
 })
 export class CustomFieldLabelPipe implements PipeTransform {
-    transform(value: CustomFieldConfig | StringFieldOption, uiLanguageCode: LanguageCode | null): string {
+    transform(
+        value: CustomFieldConfig | StringFieldOption | LocalizedString[],
+        uiLanguageCode: LanguageCode | null,
+    ): string {
         if (!value) {
             return value;
         }
+        if (Array.isArray(value)) {
+            const match = value.find(l => l.languageCode === uiLanguageCode);
+            return match ? match.value : value[0].value;
+        }
         const { label } = value;
         const name = this.isCustomFieldConfig(value) ? value.name : value.value;
         if (label) {

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

@@ -40,6 +40,7 @@ import { CustomDetailComponentHostComponent } from './components/custom-detail-c
 import { CustomFieldControlComponent } from './components/custom-field-control/custom-field-control.component';
 import { CustomerLabelComponent } from './components/customer-label/customer-label.component';
 import { DataTable2ColumnComponent } from './components/data-table-2/data-table-column.component';
+import { DataTableCustomFieldColumnComponent } from './components/data-table-2/data-table-custom-field-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';
@@ -158,6 +159,7 @@ 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 { PageComponent } from './components/page/page.component';
+import { LocalizedTextComponent } from './components/localized-text/localized-text.component';
 
 const IMPORTS = [
     ClarityModule,
@@ -277,6 +279,7 @@ const DECLARATIONS = [
     DataTableFilterLabelComponent,
     DataTableColumnPickerComponent,
     DataTable2SearchComponent,
+    DataTableCustomFieldColumnComponent,
     SplitViewComponent,
     SplitViewLeftDirective,
     SplitViewRightDirective,
@@ -314,6 +317,7 @@ const DYNAMIC_FORM_INPUTS = [
     PageHeaderDescriptionComponent,
     PageHeaderTabsComponent,
     PageBodyComponent,
+    LocalizedTextComponent,
 ];
 
 @NgModule({

+ 1 - 0
packages/admin-ui/src/lib/order/src/order.module.ts

@@ -80,5 +80,6 @@ import { orderRoutes } from './order.routes';
         OrderHistoryEntryHostComponent,
         SellerOrdersCardComponent,
     ],
+    exports: [OrderCustomFieldsCardComponent],
 })
 export class OrderModule {}