Преглед изворни кода

feat(admin-ui): Allow custom components in data table columns

Closes #2347, closes #2353
Michael Bromley пре 2 година
родитељ
комит
d3474ddd42

+ 53 - 0
packages/admin-ui/src/lib/core/src/extension/register-data-table-component.ts

@@ -0,0 +1,53 @@
+import { APP_INITIALIZER } from '@angular/core';
+import {
+    DataTableComponentConfig,
+    DataTableCustomComponentService,
+} from '../shared/components/data-table-2/data-table-custom-component.service';
+
+/**
+ * @description
+ * Allows you to override the default component used to render the data of a particular column in a DataTable.
+ * The component should implement the {@link CustomDataTableColumnComponent} interface.
+ *
+ * @example
+ * ```ts title="components/custom-table.component.ts"
+ * import { Component, Input } from '\@angular/core';
+ * import { CustomColumnComponent } from '\@vendure/admin-ui/core';
+ *
+ * @Component({
+ *     selector: 'custom-slug-component',
+ *     template: `
+ *         <a [href]="'https://example.com/products/' + rowItem.slug" target="_blank">{{ rowItem.slug }}</a>
+ *     `,
+ *     standalone: true,
+ * })
+ * export class CustomTableComponent implements CustomColumnComponent {
+ *     @Input() rowItem: any;
+ * }
+ * ```
+ *
+ * ```ts title="providers.ts"
+ * import { registerDataTableComponent } from '\@vendure/admin-ui/core';
+ * import { CustomTableComponent } from './components/custom-table.component';
+ *
+ * export default [
+ *     registerDataTableComponent({
+ *         component: CustomTableComponent,
+ *         tableId: 'product-list',
+ *         columnId: 'slug',
+ *     }),
+ * ];
+ * ```
+ *
+ * @docsCategory custom-table-components
+ */
+export function registerDataTableComponent(config: DataTableComponentConfig) {
+    return {
+        provide: APP_INITIALIZER,
+        multi: true,
+        useFactory: (dataTableCustomComponentService: DataTableCustomComponentService) => () => {
+            dataTableCustomComponentService.registerCustomComponent(config);
+        },
+        deps: [DataTableCustomComponentService],
+    };
+}

+ 10 - 9
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -74,9 +74,17 @@ export * from './data/utils/add-custom-fields';
 export * from './data/utils/get-server-location';
 export * from './data/utils/remove-readonly-custom-fields';
 export * from './data/utils/transform-relation-custom-field-inputs';
+export * from './extension/add-action-bar-item';
+export * from './extension/add-nav-menu-item';
 export * from './extension/components/angular-route.component';
 export * from './extension/components/route.component';
 export * from './extension/providers/page-metadata.service';
+export * from './extension/register-bulk-action';
+export * from './extension/register-custom-detail-component';
+export * from './extension/register-dashboard-widget';
+export * from './extension/register-data-table-component';
+export * from './extension/register-form-input-component';
+export * from './extension/register-history-entry-component';
 export * from './extension/register-route-component';
 export * from './extension/types';
 export * from './providers/alerts/alerts.service';
@@ -84,7 +92,6 @@ export * from './providers/auth/auth.service';
 export * from './providers/breadcrumb/breadcrumb.service';
 export * from './providers/bulk-action-registry/bulk-action-registry.service';
 export * from './providers/bulk-action-registry/bulk-action-types';
-export * from './extension/register-bulk-action';
 export * from './providers/channel/channel.service';
 export * from './providers/component-registry/component-registry.service';
 export * from './providers/custom-detail-component/custom-detail-component-types';
@@ -94,7 +101,6 @@ export * from './providers/custom-history-entry-component/history-entry-componen
 export * from './providers/custom-history-entry-component/history-entry-component.service';
 export * from './providers/dashboard-widget/dashboard-widget-types';
 export * from './providers/dashboard-widget/dashboard-widget.service';
-export * from './extension/register-dashboard-widget';
 export * from './providers/data-table/data-table-filter-collection';
 export * from './providers/data-table/data-table-filter';
 export * from './providers/data-table/data-table-sort-collection';
@@ -143,6 +149,7 @@ export * from './shared/components/customer-label/customer-label.component';
 export * from './shared/components/data-table/data-table-column.component';
 export * from './shared/components/data-table/data-table.component';
 export * from './shared/components/data-table-2/data-table-column.component';
+export * from './shared/components/data-table-2/data-table-custom-component.service';
 export * from './shared/components/data-table-2/data-table-custom-field-column.component';
 export * from './shared/components/data-table-2/data-table-search.component';
 export * from './shared/components/data-table-2/data-table2.component';
@@ -253,13 +260,13 @@ export * from './shared/dynamic-form-inputs/combination-mode-form-input/combinat
 export * from './shared/dynamic-form-inputs/currency-form-input/currency-form-input.component';
 export * from './shared/dynamic-form-inputs/customer-group-form-input/customer-group-form-input.component';
 export * from './shared/dynamic-form-inputs/date-form-input/date-form-input.component';
+export * from './shared/dynamic-form-inputs/default-form-inputs';
 export * from './shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component';
 export * from './shared/dynamic-form-inputs/facet-value-form-input/facet-value-form-input.component';
 export * from './shared/dynamic-form-inputs/number-form-input/number-form-input.component';
 export * from './shared/dynamic-form-inputs/password-form-input/password-form-input.component';
 export * from './shared/dynamic-form-inputs/product-multi-selector-form-input/product-multi-selector-form-input.component';
 export * from './shared/dynamic-form-inputs/product-selector-form-input/product-selector-form-input.component';
-export * from './shared/dynamic-form-inputs/default-form-inputs';
 export * from './shared/dynamic-form-inputs/relation-form-input/asset/relation-asset-input.component';
 export * from './shared/dynamic-form-inputs/relation-form-input/customer/relation-customer-input.component';
 export * from './shared/dynamic-form-inputs/relation-form-input/generic/relation-generic-input.component';
@@ -293,9 +300,3 @@ export * from './shared/pipes/time-ago.pipe';
 export * from './shared/providers/routing/can-deactivate-detail-guard';
 export * from './shared/shared.module';
 export * from './validators/unicode-pattern.validator';
-export { registerCustomDetailComponent } from './extension/register-custom-detail-component';
-export { registerHistoryEntryComponent } from './extension/register-history-entry-component';
-export { addNavMenuItem } from './extension/add-nav-menu-item';
-export { addNavMenuSection } from './extension/add-nav-menu-item';
-export { addActionBarItem } from './extension/add-action-bar-item';
-export { registerFormInputComponent } from './extension/register-form-input-component';

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

@@ -8,10 +8,10 @@ import { DataTableSort } from '../../../providers/data-table/data-table-sort';
     exportAs: 'row',
 })
 export class DataTable2ColumnComponent<T> implements OnInit {
+    @Input() id: string;
     /**
      * When set to true, this column will expand to use available width
      */
-    @Input() id: string;
     @Input() expand = false;
     @Input() heading: string;
     @Input() align: 'left' | 'right' | 'center' = 'left';

+ 96 - 0
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-component.service.ts

@@ -0,0 +1,96 @@
+import { Injectable, Provider, Type } from '@angular/core';
+import { PageLocationId } from '../../../common/component-registry-types';
+
+export type DataTableLocationId =
+    | {
+          [location in PageLocationId]: location extends `${string}-list` ? location : never;
+      }[PageLocationId]
+    | 'collection-contents'
+    | 'edit-options-list'
+    | 'manage-product-variant-list'
+    | 'customer-order-list'
+    | string;
+
+export type DataTableColumnId =
+    | 'id'
+    | 'created-at'
+    | 'updated-at'
+    | 'name'
+    | 'code'
+    | 'description'
+    | 'slug'
+    | 'enabled'
+    | 'sku'
+    | 'price'
+    | 'price-with-tax'
+    | 'status'
+    | 'state'
+    | 'image'
+    | 'quantity'
+    | 'total'
+    | 'stock-on-hand'
+    | string;
+
+/**
+ * @description
+ * Components which are to be used to render custom cells in a data table should implement this interface.
+ *
+ * The `rowItem` property is the data object for the row, e.g. the `Product` object if used
+ * in the `product-list` table.
+ *
+ * @docsCategory custom-table-components
+ */
+export interface CustomColumnComponent {
+    rowItem: any;
+}
+
+/**
+ * @description
+ * Configures a {@link CustomDetailComponent} to be placed in the given location.
+ *
+ * @docsCategory custom-table-components
+ */
+export interface DataTableComponentConfig {
+    /**
+     * @description
+     * The location in the UI where the custom component should be placed.
+     */
+    tableId: DataTableLocationId;
+    /**
+     * @description
+     * The column in the table where the custom component should be placed.
+     */
+    columnId: DataTableColumnId;
+    /**
+     * @description
+     * The component to render in the table cell. This component should implement the
+     * {@link CustomColumnComponent} interface.
+     */
+    component: Type<CustomColumnComponent>;
+    providers?: Provider[];
+}
+
+type CompoundId = `${DataTableLocationId}.${DataTableColumnId}`;
+
+@Injectable({
+    providedIn: 'root',
+})
+export class DataTableCustomComponentService {
+    private configMap = new Map<CompoundId, DataTableComponentConfig>();
+
+    registerCustomComponent(config: DataTableComponentConfig) {
+        const id = this.compoundId(config.tableId, config.columnId);
+        this.configMap.set(id, config);
+    }
+
+    getCustomComponentsFor(
+        tableId: DataTableLocationId,
+        columnId: DataTableColumnId,
+    ): DataTableComponentConfig | undefined {
+        return this.configMap.get(this.compoundId(tableId, columnId));
+    }
+
+    private compoundId(tableId: DataTableLocationId, columnId: DataTableColumnId): CompoundId {
+        return `${tableId}.${columnId}`;
+    }
+}

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

@@ -111,8 +111,20 @@
                 <td *ngFor="let column of visibleSortedColumns" [class.active]="activeIndex === i">
                     <div class="cell-content" [ngClass]="column.align">
                         <ng-container
-                            *ngTemplateOutlet="column.template; context: { item: item, index: i }"
-                        ></ng-container>
+                            *ngIf="
+                                customComponents.get(column.id) as customComponetConfig;
+                                else defaultComponent
+                            "
+                        >
+                            <ng-container
+                                *ngComponentOutlet="customComponetConfig.component; inputs: { rowItem: item }"
+                            ></ng-container>
+                        </ng-container>
+                        <ng-template #defaultComponent>
+                            <ng-container
+                                *ngTemplateOutlet="column.template; context: { item: item, index: i }"
+                            ></ng-container>
+                        </ng-template>
                     </div>
                 </td>
                 <td [class.active]="activeIndex === i"><!-- column select --></td>

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

@@ -27,6 +27,11 @@ import { BulkActionMenuComponent } from '../bulk-action-menu/bulk-action-menu.co
 
 import { FilterPresetService } from '../data-table-filter-presets/filter-preset.service';
 import { DataTable2ColumnComponent } from './data-table-column.component';
+import {
+    DataTableComponentConfig,
+    DataTableCustomComponentService,
+    DataTableLocationId,
+} from './data-table-custom-component.service';
 import { DataTableCustomFieldColumnComponent } from './data-table-custom-field-column.component';
 import { DataTable2SearchComponent } from './data-table-search.component';
 
@@ -100,7 +105,7 @@ import { DataTable2SearchComponent } from './data-table-search.component';
     providers: [PaginationService, FilterPresetService],
 })
 export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDestroy {
-    @Input() id: string;
+    @Input() id: DataTableLocationId;
     @Input() items: T[];
     @Input() itemsPerPage: number;
     @Input() currentPage: number;
@@ -121,6 +126,8 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDe
 
     route = inject(ActivatedRoute);
     filterPresetService = inject(FilterPresetService);
+    dataTableCustomComponentService = inject(DataTableCustomComponentService);
+    protected customComponents = new Map<string, DataTableComponentConfig>();
 
     rowTemplate: TemplateRef<any>;
     currentStart: number;
@@ -219,6 +226,13 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnDe
                 column.setVisibility(column.hiddenByDefault);
             }
             column.onColumnChange(updateColumnVisibility);
+            const customComponent = this.dataTableCustomComponentService.getCustomComponentsFor(
+                this.id,
+                column.id,
+            );
+            if (customComponent) {
+                this.customComponents.set(column.id, customComponent);
+            }
         });
 
         if (this.selectionManager) {

+ 4 - 4
packages/admin-ui/src/lib/react/src/public_api.ts

@@ -2,15 +2,15 @@
 export * from './components/react-custom-detail.component';
 export * from './components/react-form-input.component';
 export * from './components/react-route.component';
+export * from './directives/react-component-host.directive';
+export * from './react-components/Card';
+export * from './react-components/Link';
 export * from './react-hooks/use-detail-component-data';
 export * from './react-hooks/use-form-control';
 export * from './react-hooks/use-injector';
 export * from './react-hooks/use-page-metadata';
 export * from './react-hooks/use-query';
 export * from './register-react-custom-detail-component';
-export * from './directives/react-component-host.directive';
-export * from './react-components/Card';
-export * from './react-components/Link';
+export * from './register-react-form-input-component';
 export * from './register-react-route-component';
 export * from './types';
-export { registerReactFormInputComponent } from './register-react-form-input-component';

+ 13 - 0
packages/dev-server/test-plugins/experimental-ui/components/custom-table.component.ts

@@ -0,0 +1,13 @@
+import { Component, Input } from '@angular/core';
+import { CustomColumnComponent } from '@vendure/admin-ui/core';
+
+@Component({
+    selector: 'custom-slug-component',
+    template: `
+        <a [href]="'https://example.com/products/' + rowItem.slug" target="_blank">{{ rowItem.slug }}</a>
+    `,
+    standalone: true,
+})
+export class CustomTableComponent implements CustomColumnComponent {
+    @Input() rowItem: any;
+}

+ 8 - 2
packages/dev-server/test-plugins/experimental-ui/providers.ts

@@ -1,7 +1,8 @@
-import { addNavMenuSection } from '@vendure/admin-ui/core';
+import { addNavMenuSection, registerDataTableComponent } from '@vendure/admin-ui/core';
 import { registerReactFormInputComponent, registerReactCustomDetailComponent } from '@vendure/admin-ui/react';
-import { CustomDetailComponent } from './components/CustomDetailComponent';
 
+import { CustomTableComponent } from './components/custom-table.component';
+import { CustomDetailComponent } from './components/CustomDetailComponent';
 import { ReactNumberInput } from './components/ReactNumberInput';
 
 export default [
@@ -34,4 +35,9 @@ export default [
             foo: 'bar',
         },
     }),
+    registerDataTableComponent({
+        component: CustomTableComponent,
+        tableId: 'product-list',
+        columnId: 'slug',
+    }),
 ];