Przeglądaj źródła

feat(admin-ui): Support custom fields on Administrator & Channel

Relates to #598
Michael Bromley 5 lat temu
rodzic
commit
ecd1b1756d

+ 33 - 6
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -1052,6 +1052,7 @@ export type CreateAdministratorInput = {
   emailAddress: Scalars['String'];
   password: Scalars['String'];
   roleIds: Array<Scalars['ID']>;
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type UpdateAdministratorInput = {
@@ -1061,6 +1062,7 @@ export type UpdateAdministratorInput = {
   emailAddress?: Maybe<Scalars['String']>;
   password?: Maybe<Scalars['String']>;
   roleIds?: Maybe<Array<Scalars['ID']>>;
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type UpdateActiveAdministratorInput = {
@@ -1068,6 +1070,7 @@ export type UpdateActiveAdministratorInput = {
   lastName?: Maybe<Scalars['String']>;
   emailAddress?: Maybe<Scalars['String']>;
   password?: Maybe<Scalars['String']>;
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type Administrator = Node & {
@@ -1079,6 +1082,7 @@ export type Administrator = Node & {
   lastName: Scalars['String'];
   emailAddress: Scalars['String'];
   user: User;
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type AdministratorList = PaginatedList & {
@@ -1476,7 +1480,7 @@ export type ImportInfo = {
 /**
  * @description
  * The state of a Job in the JobQueue
- *
+ * 
  * @docsCategory common
  */
 export enum JobState {
@@ -2569,7 +2573,7 @@ export enum DeletionResult {
  * @description
  * Permissions for administrators and customers. Used to control access to
  * GraphQL resolvers via the {@link Allow} decorator.
- *
+ * 
  * @docsCategory common
  */
 export enum Permission {
@@ -2966,7 +2970,7 @@ export type CountryList = PaginatedList & {
 /**
  * @description
  * ISO 4217 currency code
- *
+ * 
  * @docsCategory common
  */
 export enum CurrencyCode {
@@ -3503,7 +3507,7 @@ export type HistoryEntryList = PaginatedList & {
  * region or script modifier (e.g. de_AT). The selection available is based
  * on the [Unicode CLDR summary list](https://unicode-org.github.io/cldr-staging/charts/37/summary/root.html)
  * and includes the major spoken languages of the world and any widely-used variants.
- *
+ * 
  * @docsCategory common
  */
 export enum LanguageCode {
@@ -3882,7 +3886,7 @@ export type OrderItem = Node & {
   unitPriceWithTax: Scalars['Int'];
   /**
    * The price of a single unit including discounts, excluding tax.
-   *
+   * 
    * If Order-level discounts have been applied, this will not be the
    * actual taxable unit price (see `proratedUnitPrice`), but is generally the
    * correct price to display to customers to avoid confusion
@@ -3922,7 +3926,7 @@ export type OrderLine = Node & {
   unitPriceWithTax: Scalars['Int'];
   /**
    * The price of a single unit including discounts, excluding tax.
-   *
+   * 
    * If Order-level discounts have been applied, this will not be the
    * actual taxable unit price (see `proratedUnitPrice`), but is generally the
    * correct price to display to customers to avoid confusion
@@ -4755,6 +4759,7 @@ export type NativeAuthInput = {
 export type CustomFields = {
   __typename?: 'CustomFields';
   Address: Array<CustomFieldConfig>;
+  Administrator: Array<CustomFieldConfig>;
   Channel: Array<CustomFieldConfig>;
   Collection: Array<CustomFieldConfig>;
   Customer: Array<CustomFieldConfig>;
@@ -7427,6 +7432,27 @@ export type GetServerConfigQuery = { globalSettings: (
         ) | (
           { __typename?: 'RelationCustomFieldConfig' }
           & CustomFields_RelationCustomFieldConfig_Fragment
+        )>, Administrator: Array<(
+          { __typename?: 'StringCustomFieldConfig' }
+          & CustomFields_StringCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'LocaleStringCustomFieldConfig' }
+          & CustomFields_LocaleStringCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'IntCustomFieldConfig' }
+          & CustomFields_IntCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'FloatCustomFieldConfig' }
+          & CustomFields_FloatCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'BooleanCustomFieldConfig' }
+          & CustomFields_BooleanCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'DateTimeCustomFieldConfig' }
+          & CustomFields_DateTimeCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'RelationCustomFieldConfig' }
+          & CustomFields_RelationCustomFieldConfig_Fragment
         )>, Channel: Array<(
           { __typename?: 'StringCustomFieldConfig' }
           & CustomFields_StringCustomFieldConfig_Fragment
@@ -9392,6 +9418,7 @@ export namespace GetServerConfig {
   export type Permissions = NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['permissions']>)[number]>;
   export type CustomFieldConfig = (NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>);
   export type Address = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Address']>)[number]>;
+  export type Administrator = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Administrator']>)[number]>;
   export type Channel = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Channel']>)[number]>;
   export type Collection = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Collection']>)[number]>;
   export type Customer = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Customer']>)[number]>;

+ 3 - 0
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -614,6 +614,9 @@ export const GET_SERVER_CONFIG = gql`
                     Address {
                         ...CustomFields
                     }
+                    Administrator {
+                        ...CustomFields
+                    }
                     Channel {
                         ...CustomFields
                     }

+ 5 - 0
packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.ts

@@ -17,6 +17,11 @@ export function isEntityCreateOrUpdateMutation(documentNode: DocumentNode): stri
             const namedType = extractInputType(variableDef.type);
             const inputTypeName = namedType.name.value;
 
+            // special cases which don't follow the usual pattern
+            if (inputTypeName === 'UpdateActiveAdministratorInput') {
+                return 'Administrator';
+            }
+
             const createMatch = inputTypeName.match(CREATE_ENTITY_REGEX);
             if (createMatch) {
                 return createMatch[1];

+ 2 - 2
packages/admin-ui/src/lib/core/src/data/utils/transform-relation-custom-field-inputs.ts

@@ -4,8 +4,8 @@ import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
 import { CustomFieldConfig } from '../../common/generated-types';
 
 /**
- * Removes any `readonly` custom fields from an entity (including its translations).
- * To be used before submitting the entity for a create or update request.
+ * Transforms any custom field "relation" type inputs into the corresponding `<name>Id` format,
+ * as expected by the server.
  */
 export function transformRelationCustomFieldInputs<T extends { input?: any } & Record<string, any> = any>(
     variables: T,

+ 11 - 0
packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.html

@@ -61,6 +61,17 @@
     >
         <input id="password" type="password" formControlName="password" />
     </vdr-form-field>
+    <section formGroupName="customFields" *ngIf="customFields.length">
+        <label>{{ 'common.custom-fields' | translate }}</label>
+        <ng-container *ngFor="let customField of customFields">
+            <vdr-custom-field-control
+                *ngIf="customFieldIsSet(customField.name)"
+                entityName="GlobalSettings"
+                [customFieldsFormGroup]="detailForm.get('customFields')"
+                [customField]="customField"
+            ></vdr-custom-field-control>
+        </ng-container>
+    </section>
     <label class="clr-control-label">{{ 'settings.roles' | translate }}</label>
     <ng-select
         [items]="allRoles$ | async"

+ 24 - 1
packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.ts

@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { BaseDetailComponent, PermissionDefinition } from '@vendure/admin-ui/core';
+import { BaseDetailComponent, CustomFieldConfig, PermissionDefinition } from '@vendure/admin-ui/core';
 import {
     Administrator,
     CreateAdministratorInput,
@@ -34,6 +34,7 @@ export interface PermissionsByChannel {
 export class AdminDetailComponent
     extends BaseDetailComponent<GetAdministrator.Administrator>
     implements OnInit, OnDestroy {
+    customFields: CustomFieldConfig[];
     administrator$: Observable<GetAdministrator.Administrator>;
     permissionDefinitions: PermissionDefinition[];
     allRoles$: Observable<Role.Fragment[]>;
@@ -56,12 +57,16 @@ export class AdminDetailComponent
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService, dataService);
+        this.customFields = this.getCustomFieldConfig('Administrator');
         this.detailForm = this.formBuilder.group({
             emailAddress: ['', Validators.required],
             firstName: ['', Validators.required],
             lastName: ['', Validators.required],
             password: [''],
             roles: [[]],
+            customFields: this.formBuilder.group(
+                this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
+            ),
         });
     }
 
@@ -84,6 +89,10 @@ export class AdminDetailComponent
         this.destroy();
     }
 
+    customFieldIsSet(name: string): boolean {
+        return !!this.detailForm.get(['customFields', name]);
+    }
+
     rolesChanged(roles: Role[]) {
         this.buildPermissionsMap();
     }
@@ -116,6 +125,7 @@ export class AdminDetailComponent
             firstName: formValue.firstName,
             lastName: formValue.lastName,
             password: formValue.password,
+            customFields: formValue.customFields,
             roleIds: formValue.roles.map(role => role.id),
         };
         this.dataService.administrator.createAdministrator(administrator).subscribe(
@@ -147,6 +157,7 @@ export class AdminDetailComponent
                         firstName: formValue.firstName,
                         lastName: formValue.lastName,
                         password: formValue.password,
+                        customFields: formValue.customFields,
                         roleIds: formValue.roles.map(role => role.id),
                     };
                     return this.dataService.administrator.updateAdministrator(administrator);
@@ -175,6 +186,18 @@ export class AdminDetailComponent
             lastName: administrator.lastName,
             roles: administrator.user.roles,
         });
+        if (this.customFields.length) {
+            const customFieldsGroup = this.detailForm.get('customFields') as FormGroup;
+
+            for (const fieldDef of this.customFields) {
+                const key = fieldDef.name;
+                const value = (administrator as any).customFields[key];
+                const control = customFieldsGroup.get(key);
+                if (control) {
+                    control.patchValue(value);
+                }
+            }
+        }
         const passwordControl = this.detailForm.get('password');
         if (passwordControl) {
             if (!administrator.id) {

+ 25 - 5
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.html

@@ -50,9 +50,9 @@
             formControlName="defaultLanguageCode"
             [vdrDisabled]="!('SuperAdmin' | hasPermission)"
         >
-            <option *ngFor="let languageCode of availableLanguageCodes$ | async" [value]="languageCode"
-                >{{ 'lang.' + languageCode | translate }} ({{ languageCode | uppercase }})</option
-            >
+            <option *ngFor="let languageCode of availableLanguageCodes$ | async" [value]="languageCode">
+                {{ 'lang.' + languageCode | translate }} ({{ languageCode | uppercase }})
+            </option>
         </select>
     </vdr-form-field>
     <vdr-form-field [label]="'settings.prices-include-tax' | translate" for="pricesIncludeTax">
@@ -76,7 +76,11 @@
             <option *ngFor="let zone of zones$ | async" [value]="zone.id">{{ zone.name }}</option>
         </select>
     </vdr-form-field>
-    <clr-alert *ngIf="detailForm.value.code && !detailForm.value.defaultTaxZoneId" clrAlertType="danger" [clrAlertClosable]="false">
+    <clr-alert
+        *ngIf="detailForm.value.code && !detailForm.value.defaultTaxZoneId"
+        clrAlertType="danger"
+        [clrAlertClosable]="false"
+    >
         <clr-alert-item>
             <span class="alert-text">
                 {{ 'error.no-default-tax-zone-set' | translate }}
@@ -94,11 +98,27 @@
             <option *ngFor="let zone of zones$ | async" [value]="zone.id">{{ zone.name }}</option>
         </select>
     </vdr-form-field>
-    <clr-alert *ngIf="detailForm.value.code && !detailForm.value.defaultShippingZoneId" clrAlertType="warning" [clrAlertClosable]="false">
+    <clr-alert
+        *ngIf="detailForm.value.code && !detailForm.value.defaultShippingZoneId"
+        clrAlertType="warning"
+        [clrAlertClosable]="false"
+    >
         <clr-alert-item>
             <span class="alert-text">
                 {{ 'error.no-default-shipping-zone-set' | translate }}
             </span>
         </clr-alert-item>
     </clr-alert>
+
+    <section formGroupName="customFields" *ngIf="customFields.length">
+        <label>{{ 'common.custom-fields' | translate }}</label>
+        <ng-container *ngFor="let customField of customFields">
+            <vdr-custom-field-control
+                *ngIf="customFieldIsSet(customField.name)"
+                entityName="GlobalSettings"
+                [customFieldsFormGroup]="detailForm.get('customFields')"
+                [customField]="customField"
+            ></vdr-custom-field-control>
+        </ng-container>
+    </section>
 </form>

+ 26 - 2
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts

@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { BaseDetailComponent } from '@vendure/admin-ui/core';
+import { BaseDetailComponent, CustomFieldConfig } from '@vendure/admin-ui/core';
 import {
     Channel,
     CreateChannelInput,
@@ -24,8 +24,10 @@ import { map, mergeMap, take } from 'rxjs/operators';
     styleUrls: ['./channel-detail.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment>
+export class ChannelDetailComponent
+    extends BaseDetailComponent<Channel.Fragment>
     implements OnInit, OnDestroy {
+    customFields: CustomFieldConfig[];
     zones$: Observable<GetZones.Zones[]>;
     detailForm: FormGroup;
     currencyCodes = Object.values(CurrencyCode);
@@ -41,6 +43,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService, dataService);
+        this.customFields = this.getCustomFieldConfig('Channel');
         this.detailForm = this.formBuilder.group({
             code: ['', Validators.required],
             token: ['', Validators.required],
@@ -49,6 +52,9 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
             defaultShippingZoneId: ['', Validators.required],
             defaultLanguageCode: [],
             defaultTaxZoneId: ['', Validators.required],
+            customFields: this.formBuilder.group(
+                this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
+            ),
         });
     }
 
@@ -62,6 +68,10 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
         this.destroy();
     }
 
+    customFieldIsSet(name: string): boolean {
+        return !!this.detailForm.get(['customFields', name]);
+    }
+
     saveButtonEnabled(): boolean {
         return this.detailForm.dirty && this.detailForm.valid;
     }
@@ -79,6 +89,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
             currencyCode: formValue.currencyCode,
             defaultShippingZoneId: formValue.defaultShippingZoneId,
             defaultTaxZoneId: formValue.defaultTaxZoneId,
+            customFields: formValue.customFields,
         };
         this.dataService.settings
             .createChannel(input)
@@ -130,6 +141,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
                         defaultShippingZoneId: formValue.defaultShippingZoneId,
                         defaultLanguageCode: formValue.defaultLanguageCode,
                         defaultTaxZoneId: formValue.defaultTaxZoneId,
+                        customFields: formValue.customFields,
                     } as UpdateChannelInput;
                     return this.dataService.settings.updateChannel(input);
                 }),
@@ -162,6 +174,18 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
             defaultLanguageCode: entity.defaultLanguageCode,
             defaultTaxZoneId: entity.defaultTaxZone ? entity.defaultTaxZone.id : '',
         });
+        if (this.customFields.length) {
+            const customFieldsGroup = this.detailForm.get('customFields') as FormGroup;
+
+            for (const fieldDef of this.customFields) {
+                const key = fieldDef.name;
+                const value = (entity as any).customFields[key];
+                const control = customFieldsGroup.get(key);
+                if (control) {
+                    control.patchValue(value);
+                }
+            }
+        }
         if (entity.code === DEFAULT_CHANNEL_CODE) {
             const codeControl = this.detailForm.get('code');
             if (codeControl) {

+ 11 - 0
packages/admin-ui/src/lib/settings/src/components/profile/profile.component.html

@@ -30,4 +30,15 @@
     <vdr-form-field [label]="'settings.password' | translate" for="password" [readOnlyToggle]="true">
         <input id="password" type="password" formControlName="password" />
     </vdr-form-field>
+    <section formGroupName="customFields" *ngIf="customFields.length">
+        <label>{{ 'common.custom-fields' | translate }}</label>
+        <ng-container *ngFor="let customField of customFields">
+            <vdr-custom-field-control
+                *ngIf="customFieldIsSet(customField.name)"
+                entityName="GlobalSettings"
+                [customFieldsFormGroup]="detailForm.get('customFields')"
+                [customField]="customField"
+            ></vdr-custom-field-control>
+        </ng-container>
+    </section>
 </form>

+ 23 - 0
packages/admin-ui/src/lib/settings/src/components/profile/profile.component.ts

@@ -5,6 +5,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     Administrator,
     BaseDetailComponent,
+    CustomFieldConfig,
     DataService,
     GetActiveAdministrator,
     LanguageCode,
@@ -23,6 +24,7 @@ import { mergeMap, take } from 'rxjs/operators';
 export class ProfileComponent
     extends BaseDetailComponent<GetActiveAdministrator.ActiveAdministrator>
     implements OnInit, OnDestroy {
+    customFields: CustomFieldConfig[];
     detailForm: FormGroup;
 
     constructor(
@@ -35,11 +37,15 @@ export class ProfileComponent
         private notificationService: NotificationService,
     ) {
         super(route, router, serverConfigService, dataService);
+        this.customFields = this.getCustomFieldConfig('Administrator');
         this.detailForm = this.formBuilder.group({
             emailAddress: ['', Validators.required],
             firstName: ['', Validators.required],
             lastName: ['', Validators.required],
             password: [''],
+            customFields: this.formBuilder.group(
+                this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
+            ),
         });
     }
 
@@ -51,6 +57,10 @@ export class ProfileComponent
         this.destroy();
     }
 
+    customFieldIsSet(name: string): boolean {
+        return !!this.detailForm.get(['customFields', name]);
+    }
+
     save() {
         this.entity$
             .pipe(
@@ -62,6 +72,7 @@ export class ProfileComponent
                         firstName: formValue.firstName,
                         lastName: formValue.lastName,
                         password: formValue.password,
+                        customFields: formValue.customFields,
                     };
                     return this.dataService.administrator.updateActiveAdministrator(administrator);
                 }),
@@ -88,5 +99,17 @@ export class ProfileComponent
             firstName: administrator.firstName,
             lastName: administrator.lastName,
         });
+        if (this.customFields.length) {
+            const customFieldsGroup = this.detailForm.get('customFields') as FormGroup;
+
+            for (const fieldDef of this.customFields) {
+                const key = fieldDef.name;
+                const value = (administrator as any).customFields[key];
+                const control = customFieldsGroup.get(key);
+                if (control) {
+                    control.patchValue(value);
+                }
+            }
+        }
     }
 }