Bläddra i källkod

refactor(admin-ui): Consolidate extension route logic

Michael Bromley 2 år sedan
förälder
incheckning
0db5d4d5ac

+ 13 - 0
packages/admin-ui/src/lib/core/src/extension/components/angular-route.component.ts

@@ -0,0 +1,13 @@
+import { Component, inject } from '@angular/core';
+import { SharedModule } from '../../shared/shared.module';
+import { ROUTE_COMPONENT_OPTIONS, RouteComponent } from './route.component';
+
+@Component({
+    selector: 'vdr-angular-route-component',
+    template: ` <vdr-route-component><ng-container *ngComponentOutlet="component" /></vdr-route-component> `,
+    standalone: true,
+    imports: [SharedModule, RouteComponent],
+})
+export class AngularRouteComponent {
+    protected component = inject(ROUTE_COMPONENT_OPTIONS).component;
+}

+ 6 - 6
packages/admin-ui/src/lib/core/src/extension/components/route.component.ts

@@ -1,14 +1,16 @@
 import { Component, inject, InjectionToken } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
-import { Observable, combineLatest, switchMap, of } from 'rxjs';
+import { combineLatest, Observable, of, switchMap } from 'rxjs';
 import { filter, map } from 'rxjs/operators';
 import { BreadcrumbValue } from '../../providers/breadcrumb/breadcrumb.service';
 import { SharedModule } from '../../shared/shared.module';
 import { PageMetadataService } from '../providers/page-metadata.service';
-import { RouteComponentOptions } from '../types';
+import { AngularRouteComponentOptions } from '../types';
 
-export const ROUTE_COMPONENT_OPTIONS = new InjectionToken<RouteComponentOptions>('ROUTE_COMPONENT_OPTIONS');
+export const ROUTE_COMPONENT_OPTIONS = new InjectionToken<AngularRouteComponentOptions>(
+    'ROUTE_COMPONENT_OPTIONS',
+);
 
 @Component({
     selector: 'vdr-route-component',
@@ -16,16 +18,14 @@ export const ROUTE_COMPONENT_OPTIONS = new InjectionToken<RouteComponentOptions>
         <vdr-page-header>
             <vdr-page-title *ngIf="title$ | async as title" [title]="title"></vdr-page-title>
         </vdr-page-header>
-        <vdr-page-body><ng-container *ngComponentOutlet="component" /></vdr-page-body>
+        <vdr-page-body><ng-content /></vdr-page-body>
     `,
     standalone: true,
     imports: [SharedModule],
     providers: [PageMetadataService],
 })
 export class RouteComponent {
-    protected component = inject(ROUTE_COMPONENT_OPTIONS).component;
     protected title$: Observable<string | undefined>;
-    protected context = inject(ROUTE_COMPONENT_OPTIONS);
 
     constructor(private route: ActivatedRoute) {
         const breadcrumbLabel$ = this.route.data.pipe(

+ 19 - 13
packages/admin-ui/src/lib/core/src/extension/register-route-component.ts

@@ -9,25 +9,31 @@ import { BreadcrumbValue } from '../providers/breadcrumb/breadcrumb.service';
 import { ROUTE_COMPONENT_OPTIONS, RouteComponent } from './components/route.component';
 import { RouteComponentOptions } from './types';
 
+export type RegisterRouteComponentOptions<
+    Component extends any | BaseDetailComponent<Entity>,
+    Entity extends { id: string; updatedAt?: string },
+    T extends DocumentNode | TypedDocumentNode<any, { id: string }>,
+    Field extends keyof ResultOf<T>,
+    R extends Field,
+> = {
+    component: Type<Component> | Component;
+    title?: string;
+    breadcrumb?: BreadcrumbValue;
+    path?: string;
+    query?: T;
+    getBreadcrumbs?: (entity: Exclude<ResultOf<T>[R], 'Query'>) => BreadcrumbValue;
+    entityKey?: Component extends BaseDetailComponent<Entity> ? R : undefined;
+    variables?: T extends TypedDocumentNode<any, infer V> ? Omit<V, 'id'> : never;
+    routeConfig?: Route;
+} & (Component extends BaseDetailComponent<Entity> ? { entityKey: R } : unknown);
+
 export function registerRouteComponent<
     Component extends any | BaseDetailComponent<Entity>,
     Entity extends { id: string; updatedAt?: string },
     T extends DocumentNode | TypedDocumentNode<any, { id: string }>,
     Field extends keyof ResultOf<T>,
     R extends Field,
->(
-    options: {
-        component: Type<Component>;
-        title?: string;
-        breadcrumb?: BreadcrumbValue;
-        path?: string;
-        query?: T;
-        getBreadcrumbs?: (entity: Exclude<ResultOf<T>[R], 'Query'>) => BreadcrumbValue;
-        entityKey?: Component extends BaseDetailComponent<Entity> ? R : undefined;
-        variables?: T extends TypedDocumentNode<any, infer V> ? Omit<V, 'id'> : never;
-        routeConfig?: Route;
-    } & (Component extends BaseDetailComponent<Entity> ? { entityKey: R } : unknown),
-) {
+>(options: RegisterRouteComponentOptions<Component, Entity, T, Field, R>) {
     const { query, entityKey, variables, getBreadcrumbs } = options;
 
     const breadcrumbSubject$ = new BehaviorSubject<BreadcrumbValue>(options.breadcrumb ?? '');

+ 5 - 1
packages/admin-ui/src/lib/core/src/extension/types.ts

@@ -3,7 +3,11 @@ import { Subject } from 'rxjs';
 import { BreadcrumbValue } from '../providers/breadcrumb/breadcrumb.service';
 
 export interface RouteComponentOptions {
-    component: Type<any>;
+    component: any;
     title$: Subject<string | undefined>;
     breadcrumb$: Subject<BreadcrumbValue>;
 }
+
+export interface AngularRouteComponentOptions extends RouteComponentOptions {
+    component: Type<any>;
+}

+ 1 - 0
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -74,6 +74,7 @@ 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/components/angular-route.component';
 export * from './extension/components/route.component';
 export * from './extension/providers/page-metadata.service';
 export * from './extension/register-route-component';

+ 8 - 13
packages/admin-ui/src/lib/react/src/components/react-route.component.ts

@@ -1,28 +1,23 @@
 import { Component, inject, InjectionToken } from '@angular/core';
-import { SharedModule } from '@vendure/admin-ui/core';
+import { ROUTE_COMPONENT_OPTIONS, RouteComponent, SharedModule } from '@vendure/admin-ui/core';
 import { ReactComponentHostDirective } from '../react-component-host.directive';
 import { ReactRouteComponentOptions } from '../types';
 
-export const ROUTE_COMPONENT_OPTIONS = new InjectionToken<ReactRouteComponentOptions>(
-    'ROUTE_COMPONENT_OPTIONS',
+export const REACT_ROUTE_COMPONENT_OPTIONS = new InjectionToken<ReactRouteComponentOptions>(
+    'REACT_ROUTE_COMPONENT_OPTIONS',
 );
 
 @Component({
     selector: 'vdr-react-route-component',
     template: `
-        <vdr-page-header>
-            <vdr-page-title *ngIf="title$ | async as title" [title]="title"></vdr-page-title>
-        </vdr-page-header>
-        <vdr-page-body
-            ><div [vdrReactComponentHost]="reactComponent" [props]="props" [context]="context"></div
-        ></vdr-page-body>
+        <vdr-route-component
+            ><div [vdrReactComponentHost]="reactComponent" [props]="props"></div
+        ></vdr-route-component>
     `,
     standalone: true,
-    imports: [ReactComponentHostDirective, SharedModule],
+    imports: [ReactComponentHostDirective, RouteComponent, SharedModule],
 })
 export class ReactRouteComponent {
-    protected title$ = inject(ROUTE_COMPONENT_OPTIONS).title$;
-    protected props = inject(ROUTE_COMPONENT_OPTIONS).props;
-    protected context = inject(ROUTE_COMPONENT_OPTIONS);
+    protected props = inject(REACT_ROUTE_COMPONENT_OPTIONS).props;
     protected reactComponent = inject(ROUTE_COMPONENT_OPTIONS).component;
 }

+ 2 - 2
packages/admin-ui/src/lib/react/src/hooks/use-page-metadata.ts

@@ -33,10 +33,10 @@ export function usePageMetadata() {
         HostedComponentContext,
     ) as HostedReactComponentContext<ReactRouteComponentOptions>;
     const setBreadcrumb = (newValue: BreadcrumbValue) => {
-        context.breadcrumb$.next(newValue);
+        context.pageMetadataService?.setBreadcrumbs(newValue);
     };
     const setTitle = (newTitle: string) => {
-        context.title$.next(newTitle);
+        context.pageMetadataService?.setTitle(newTitle);
     };
 
     return {

+ 1 - 38
packages/admin-ui/src/lib/react/src/providers.ts

@@ -1,11 +1,7 @@
 import { APP_INITIALIZER, FactoryProvider } from '@angular/core';
-import { Route } from '@angular/router';
-import { BreadcrumbValue, ComponentRegistryService } from '@vendure/admin-ui/core';
+import { ComponentRegistryService } from '@vendure/admin-ui/core';
 import { ElementType } from 'react';
-import { BehaviorSubject } from 'rxjs';
 import { ReactFormInputComponent } from './components/react-form-input.component';
-import { ReactRouteComponent, ROUTE_COMPONENT_OPTIONS } from './components/react-route.component';
-import { ReactRouteComponentOptions } from './types';
 
 export function registerReactFormInputComponent(id: string, component: ElementType): FactoryProvider {
     return {
@@ -17,36 +13,3 @@ export function registerReactFormInputComponent(id: string, component: ElementTy
         deps: [ComponentRegistryService],
     };
 }
-
-export function registerReactRouteComponent(options: {
-    component: ElementType;
-    title?: string;
-    breadcrumb?: BreadcrumbValue;
-    path?: string;
-    props?: Record<string, any>;
-    routeConfig?: Route;
-}): Route {
-    const breadcrumbSubject$ = new BehaviorSubject<BreadcrumbValue>(options.breadcrumb ?? '');
-    const titleSubject$ = new BehaviorSubject<string | undefined>(options.title);
-    return {
-        path: options.path ?? '',
-        providers: [
-            {
-                provide: ROUTE_COMPONENT_OPTIONS,
-                useValue: {
-                    component: options.component,
-                    title$: titleSubject$,
-                    breadcrumb$: breadcrumbSubject$,
-                    props: options.props,
-                } satisfies ReactRouteComponentOptions,
-            },
-            ...(options.routeConfig?.providers ?? []),
-        ],
-        data: {
-            breadcrumb: breadcrumbSubject$,
-            ...(options.routeConfig?.data ?? {}),
-        },
-        ...(options.routeConfig ?? {}),
-        component: ReactRouteComponent,
-    };
-}

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

@@ -8,4 +8,5 @@ export * from './hooks/use-query';
 export * from './providers';
 export * from './react-component-host.directive';
 export * from './react-components/Link';
+export * from './register-react-route-component';
 export * from './types';

+ 13 - 3
packages/admin-ui/src/lib/react/src/react-component-host.directive.ts

@@ -1,4 +1,5 @@
-import { Directive, ElementRef, Injector, Input } from '@angular/core';
+import { Directive, ElementRef, Injector, Input, Optional } from '@angular/core';
+import { PageMetadataService } from '@vendure/admin-ui/core';
 import { ComponentProps, createContext, createElement, ElementType } from 'react';
 import { createRoot, Root } from 'react-dom/client';
 import { HostedReactComponentContext } from './types';
@@ -19,7 +20,11 @@ export class ReactComponentHostDirective<Comp extends ElementType> {
 
     private root: Root | null = null;
 
-    constructor(private host: ElementRef, private injector: Injector) {}
+    constructor(
+        private host: ElementRef,
+        private injector: Injector,
+        @Optional() private pageMetadataService?: PageMetadataService,
+    ) {}
 
     async ngOnChanges() {
         const Comp = this.reactComponent;
@@ -32,7 +37,12 @@ export class ReactComponentHostDirective<Comp extends ElementType> {
             createElement(
                 HostedComponentContext.Provider,
                 {
-                    value: { ...this.props, ...this.context, injector: this.injector },
+                    value: {
+                        ...this.props,
+                        ...this.context,
+                        injector: this.injector,
+                        pageMetadataService: this.pageMetadataService,
+                    },
                 },
                 createElement(Comp, this.props),
             ),

+ 38 - 0
packages/admin-ui/src/lib/react/src/register-react-route-component.ts

@@ -0,0 +1,38 @@
+import { Route } from '@angular/router';
+import { ResultOf, TypedDocumentNode } from '@graphql-typed-document-node/core';
+import { registerRouteComponent, RegisterRouteComponentOptions } from '@vendure/admin-ui/core';
+import { DocumentNode } from 'graphql/index';
+import { ElementType } from 'react';
+import { REACT_ROUTE_COMPONENT_OPTIONS, ReactRouteComponent } from './components/react-route.component';
+import { ReactRouteComponentOptions } from './types';
+
+type RegisterReactRouteComponentOptions<
+    Entity extends { id: string; updatedAt?: string },
+    T extends DocumentNode | TypedDocumentNode<any, { id: string }>,
+    Field extends keyof ResultOf<T>,
+    R extends Field,
+> = RegisterRouteComponentOptions<ElementType, Entity, T, Field, R> & {
+    props?: Record<string, any>;
+};
+
+export function registerReactRouteComponent<
+    Entity extends { id: string; updatedAt?: string },
+    T extends DocumentNode | TypedDocumentNode<any, { id: string }>,
+    Field extends keyof ResultOf<T>,
+    R extends Field,
+>(options: RegisterReactRouteComponentOptions<Entity, T, Field, R>): Route {
+    const routeDef = registerRouteComponent(options);
+    return {
+        ...routeDef,
+        providers: [
+            {
+                provide: REACT_ROUTE_COMPONENT_OPTIONS,
+                useValue: {
+                    props: options.props,
+                } satisfies ReactRouteComponentOptions,
+            },
+            ...(routeDef.providers ?? []),
+        ],
+        component: ReactRouteComponent,
+    };
+}

+ 2 - 5
packages/admin-ui/src/lib/react/src/types.ts

@@ -1,7 +1,6 @@
 import { Injector } from '@angular/core';
 import { FormControl } from '@angular/forms';
-import { BreadcrumbValue, CustomField } from '@vendure/admin-ui/core';
-import { Subject } from 'rxjs';
+import { CustomField, PageMetadataService } from '@vendure/admin-ui/core';
 
 export interface ReactFormInputOptions {
     formControl: FormControl;
@@ -12,12 +11,10 @@ export interface ReactFormInputOptions {
 export interface ReactFormInputProps extends ReactFormInputOptions {}
 
 export interface ReactRouteComponentOptions {
-    component: any;
-    title$: Subject<string | undefined>;
-    breadcrumb$: Subject<BreadcrumbValue>;
     props?: Record<string, any>;
 }
 
 export type HostedReactComponentContext<T extends Record<string, any> = Record<string, any>> = {
     injector: Injector;
+    pageMetadataService?: PageMetadataService;
 } & T;

+ 3 - 0
packages/admin-ui/tsconfig.json

@@ -51,6 +51,9 @@
       "@vendure/admin-ui/order": [
         "./src/lib/order/src/public_api"
       ],
+      "@vendure/admin-ui/react": [
+        "./src/lib/react/src/public_api"
+      ],
       "@vendure/admin-ui/settings": [
         "./src/lib/settings/src/public_api"
       ],