Browse Source

Merge branch 'master' into next

Michael Bromley 5 years ago
parent
commit
80a036d32a
48 changed files with 378 additions and 179 deletions
  1. 1 1
      .github/workflows/scripts/smoke-tests.js
  2. 17 0
      CHANGELOG.md
  3. 1 1
      lerna.json
  4. 3 3
      packages/admin-ui-plugin/package.json
  5. 1 1
      packages/admin-ui/i18n-coverage.json
  6. 2 2
      packages/admin-ui/package.json
  7. 1 1
      packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.html
  8. 17 11
      packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.ts
  9. 16 8
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  10. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  11. 3 3
      packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts
  12. 5 5
      packages/admin-ui/src/lib/core/src/data/providers/product-data.service.ts
  13. 3 2
      packages/admin-ui/src/lib/core/src/providers/modal/modal.service.ts
  14. 20 26
      packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.html
  15. 11 7
      packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.scss
  16. 12 3
      packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.ts
  17. 3 3
      packages/admin-ui/src/lib/core/src/shared/components/simple-dialog/simple-dialog.component.html
  18. 1 0
      packages/admin-ui/src/lib/core/src/shared/components/simple-dialog/simple-dialog.component.ts
  19. 23 19
      packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts
  20. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  21. 2 2
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  22. 2 2
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  23. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  24. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  25. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  26. 7 1
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  27. 3 3
      packages/asset-server-plugin/package.json
  28. 1 1
      packages/common/package.json
  29. 12 5
      packages/common/src/generated-types.ts
  30. 7 1
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  31. 2 2
      packages/core/package.json
  32. 22 0
      packages/core/src/api/config/__snapshots__/graphql-custom-fields.spec.ts.snap
  33. 7 3
      packages/core/src/api/config/configure-graphql-module.ts
  34. 52 7
      packages/core/src/api/config/graphql-custom-fields.spec.ts
  35. 37 6
      packages/core/src/api/config/graphql-custom-fields.ts
  36. 8 1
      packages/core/src/api/resolvers/admin/asset.resolver.ts
  37. 2 0
      packages/core/src/api/schema/admin-api/asset.api.graphql
  38. 1 1
      packages/core/src/i18n/messages/en.json
  39. 30 17
      packages/core/src/service/services/asset.service.ts
  40. 2 0
      packages/core/src/service/services/customer.service.ts
  41. 3 3
      packages/create/package.json
  42. 9 9
      packages/dev-server/package.json
  43. 7 1
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  44. 3 3
      packages/elasticsearch-plugin/package.json
  45. 3 3
      packages/email-plugin/package.json
  46. 3 3
      packages/testing/package.json
  47. 4 4
      packages/ui-devkit/package.json
  48. 0 0
      schema-admin.json

+ 1 - 1
.github/workflows/scripts/smoke-tests.js

@@ -34,7 +34,7 @@ function awaitServerStartup() {
             attempts++;
             if (attempts < 30) {
                 console.log('Server not yet available, waiting 1s...');
-                setTimeout(poll, 1000);
+                setTimeout(poll, 2000);
             } else {
                 reject('Unable to establish connection to Vendure server!');
             }

+ 17 - 0
CHANGELOG.md

@@ -1,3 +1,20 @@
+## <small>0.13.1 (2020-06-30)</small>
+
+
+#### Features
+
+* **admin-ui** Display billing address in Order detail view ([c8992a5](https://github.com/vendure-ecommerce/vendure/commit/c8992a5)), closes [#372](https://github.com/vendure-ecommerce/vendure/issues/372)
+* **core** Add setOrderBillingAddress mutation to Shop API ([83347b2](https://github.com/vendure-ecommerce/vendure/commit/83347b2)), closes [#372](https://github.com/vendure-ecommerce/vendure/issues/372)
+* **core** Allow phoneNumber in registerCustomerAccount mutation ([2c710b9](https://github.com/vendure-ecommerce/vendure/commit/2c710b9)), closes [#389](https://github.com/vendure-ecommerce/vendure/issues/389)
+
+#### Fixes
+
+* **admin-ui** Add custom field controls to ProductOption dialog ([4678360](https://github.com/vendure-ecommerce/vendure/commit/4678360)), closes [#382](https://github.com/vendure-ecommerce/vendure/issues/382)
+* **admin-ui** Correctly render channels in Role detail view ([cfb3c03](https://github.com/vendure-ecommerce/vendure/commit/cfb3c03))
+* **admin-ui** Only check jobs if Admin has ReadSettings permission ([daca6b6](https://github.com/vendure-ecommerce/vendure/commit/daca6b6)), closes [#383](https://github.com/vendure-ecommerce/vendure/issues/383)
+* **core** Correctly resolve activeCustomer order lines ([56449b8](https://github.com/vendure-ecommerce/vendure/commit/56449b8)), closes [#374](https://github.com/vendure-ecommerce/vendure/issues/374) [#375](https://github.com/vendure-ecommerce/vendure/issues/375)
+* **core** Implement field resolvers for Facet & FacetValue ([7a4d046](https://github.com/vendure-ecommerce/vendure/commit/7a4d046))
+
 ## 0.13.0 (2020-06-12)
 
 

+ 1 - 1
lerna.json

@@ -2,7 +2,7 @@
   "packages": [
     "packages/*"
   ],
-  "version": "0.13.0",
+  "version": "0.13.1",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "command": {

+ 3 - 3
packages/admin-ui-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui-plugin",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -19,8 +19,8 @@
   "devDependencies": {
     "@types/express": "^4.0.39",
     "@types/fs-extra": "^8.0.1",
-    "@vendure/common": "^0.13.0",
-    "@vendure/core": "^0.13.0",
+    "@vendure/common": "^0.13.1",
+    "@vendure/core": "^0.13.1",
     "express": "^4.16.4",
     "rimraf": "^3.0.0",
     "typescript": "3.8.3"

+ 1 - 1
packages/admin-ui/i18n-coverage.json

@@ -33,4 +33,4 @@
       "percentage": 84
     }
   }
-}
+}

+ 2 - 2
packages/admin-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "license": "MIT",
   "scripts": {
     "ng": "ng",
@@ -35,7 +35,7 @@
     "@ng-select/ng-select": "^3.7.2",
     "@ngx-translate/core": "^12.1.2",
     "@ngx-translate/http-loader": "^4.0.0",
-    "@vendure/common": "^0.13.0",
+    "@vendure/common": "^0.13.1",
     "@webcomponents/custom-elements": "^1.2.4",
     "apollo-angular": "^1.8.0",
     "apollo-cache-inmemory": "^1.6.5",

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.html

@@ -22,7 +22,7 @@
     [assets]="(items$ | async)! | paginate: (paginationConfig$ | async) || {}"
     [multiSelect]="true"
     [canDelete]="'DeleteCatalog' | hasPermission"
-    (deleteAsset)="deleteAsset($event)"
+    (deleteAssets)="deleteAssets($event)"
 ></vdr-asset-gallery>
 
 <div class="paging-controls">

+ 17 - 11
packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.ts

@@ -80,38 +80,42 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
         }
     }
 
-    deleteAsset(asset: Asset) {
-        this.showModalAndDelete(asset.id)
+    deleteAssets(assets: Asset[]) {
+        this.showModalAndDelete(assets.map((a) => a.id))
             .pipe(
                 switchMap((response) => {
                     if (response.result === DeletionResult.DELETED) {
                         return [true];
                     } else {
-                        return this.showModalAndDelete(asset.id, response.message || '').pipe(
-                            map((r) => r.result === DeletionResult.DELETED),
-                        );
+                        return this.showModalAndDelete(
+                            assets.map((a) => a.id),
+                            response.message || '',
+                        ).pipe(map((r) => r.result === DeletionResult.DELETED));
                     }
                 }),
             )
             .subscribe(
                 () => {
                     this.notificationService.success(_('common.notify-delete-success'), {
-                        entity: 'Asset',
+                        entity: 'Assets',
                     });
                     this.refresh();
                 },
                 (err) => {
                     this.notificationService.error(_('common.notify-delete-error'), {
-                        entity: 'Asset',
+                        entity: 'Assets',
                     });
                 },
             );
     }
 
-    private showModalAndDelete(assetId: string, message?: string) {
+    private showModalAndDelete(assetIds: string[], message?: string) {
         return this.modalService
             .dialog({
-                title: _('catalog.confirm-delete-asset'),
+                title: _('catalog.confirm-delete-assets'),
+                translationVars: {
+                    count: assetIds.length,
+                },
                 body: message,
                 buttons: [
                     { type: 'secondary', label: _('common.cancel') },
@@ -119,8 +123,10 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
                 ],
             })
             .pipe(
-                switchMap((res) => (res ? this.dataService.product.deleteAsset(assetId, !!message) : EMPTY)),
-                map((res) => res.deleteAsset),
+                switchMap((res) =>
+                    res ? this.dataService.product.deleteAssets(assetIds, !!message) : EMPTY,
+                ),
+                map((res) => res.deleteAssets),
             );
     }
 }

+ 16 - 8
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -1831,6 +1831,8 @@ export type Mutation = {
   deleteAdministrator: DeletionResponse;
   /** Delete an Asset */
   deleteAsset: DeletionResponse;
+  /** Delete multiple Assets */
+  deleteAssets: DeletionResponse;
   /** Delete a Channel */
   deleteChannel: DeletionResponse;
   /** Delete a Collection and all of its descendants */
@@ -2102,6 +2104,12 @@ export type MutationDeleteAssetArgs = {
 };
 
 
+export type MutationDeleteAssetsArgs = {
+  ids: Array<Scalars['ID']>;
+  force?: Maybe<Scalars['Boolean']>;
+};
+
+
 export type MutationDeleteChannelArgs = {
   id: Scalars['ID'];
 };
@@ -5356,15 +5364,15 @@ export type UpdateAssetMutation = (
   ) }
 );
 
-export type DeleteAssetMutationVariables = {
-  id: Scalars['ID'];
+export type DeleteAssetsMutationVariables = {
+  ids: Array<Scalars['ID']>;
   force?: Maybe<Scalars['Boolean']>;
 };
 
 
-export type DeleteAssetMutation = (
+export type DeleteAssetsMutation = (
   { __typename?: 'Mutation' }
-  & { deleteAsset: (
+  & { deleteAssets: (
     { __typename?: 'DeletionResponse' }
     & Pick<DeletionResponse, 'result' | 'message'>
   ) }
@@ -7441,10 +7449,10 @@ export namespace UpdateAsset {
   export type UpdateAsset = AssetFragment;
 }
 
-export namespace DeleteAsset {
-  export type Variables = DeleteAssetMutationVariables;
-  export type Mutation = DeleteAssetMutation;
-  export type DeleteAsset = DeleteAssetMutation['deleteAsset'];
+export namespace DeleteAssets {
+  export type Variables = DeleteAssetsMutationVariables;
+  export type Mutation = DeleteAssetsMutation;
+  export type DeleteAssets = DeleteAssetsMutation['deleteAssets'];
 }
 
 export namespace SearchProducts {

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/version.ts

@@ -1,2 +1,2 @@
 // Auto-generated by the set-version.js script.
-export const ADMIN_UI_VERSION = '0.13.0';
+export const ADMIN_UI_VERSION = '0.13.1';

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

@@ -385,9 +385,9 @@ export const UPDATE_ASSET = gql`
     ${ASSET_FRAGMENT}
 `;
 
-export const DELETE_ASSET = gql`
-    mutation DeleteAsset($id: ID!, $force: Boolean) {
-        deleteAsset(id: $id, force: $force) {
+export const DELETE_ASSETS = gql`
+    mutation DeleteAssets($ids: [ID!]!, $force: Boolean) {
+        deleteAssets(ids: $ids, force: $force) {
             result
             message
         }

+ 5 - 5
packages/admin-ui/src/lib/core/src/data/providers/product-data.service.ts

@@ -13,7 +13,7 @@ import {
     CreateProductOptionInput,
     CreateProductVariantInput,
     CreateProductVariants,
-    DeleteAsset,
+    DeleteAssets,
     DeleteProduct,
     DeleteProductVariant,
     GetAsset,
@@ -46,7 +46,7 @@ import {
     CREATE_PRODUCT,
     CREATE_PRODUCT_OPTION_GROUP,
     CREATE_PRODUCT_VARIANTS,
-    DELETE_ASSET,
+    DELETE_ASSETS,
     DELETE_PRODUCT,
     DELETE_PRODUCT_VARIANT,
     GET_ASSET,
@@ -284,9 +284,9 @@ export class ProductDataService {
         });
     }
 
-    deleteAsset(id: string, force: boolean) {
-        return this.baseDataService.mutate<DeleteAsset.Mutation, DeleteAsset.Variables>(DELETE_ASSET, {
-            id,
+    deleteAssets(ids: string[], force: boolean) {
+        return this.baseDataService.mutate<DeleteAssets.Mutation, DeleteAssets.Variables>(DELETE_ASSETS, {
+            ids,
             force,
         });
     }

+ 3 - 2
packages/admin-ui/src/lib/core/src/providers/modal/modal.service.ts

@@ -32,6 +32,7 @@ export interface DialogButtonConfig<T> {
 export interface DialogConfig<T> {
     title: string;
     body?: string;
+    translationVars?: { [key: string]: string | number };
     buttons: Array<DialogButtonConfig<T>>;
 }
 
@@ -113,13 +114,13 @@ export class ModalService {
         const modalFactory = this.componentFactoryResolver.resolveComponentFactory(ModalDialogComponent);
 
         return from(this.overlayHostService.getHostView()).pipe(
-            mergeMap(hostView => {
+            mergeMap((hostView) => {
                 const modalComponentRef = hostView.createComponent(modalFactory);
                 const modalInstance: ModalDialogComponent<any> = modalComponentRef.instance;
                 modalInstance.childComponentType = component;
                 modalInstance.options = options;
 
-                return new Observable<R>(subscriber => {
+                return new Observable<R>((subscriber) => {
                     modalInstance.closeModal = (result: R) => {
                         modalComponentRef.destroy();
                         subscriber.next(result);

+ 20 - 26
packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.html

@@ -7,7 +7,7 @@
     >
         <div class="card-img">
             <div class="selected-checkbox"><clr-icon shape="check-circle" size="32"></clr-icon></div>
-            <img [src]="asset | assetPreview:'thumb'" />
+            <img [src]="asset | assetPreview: 'thumb'" />
         </div>
         <div class="detail">
             <vdr-entity-info
@@ -34,21 +34,21 @@
         </div>
         <div class="card-block details" *ngIf="selection.length >= 1">
             <div class="name">{{ lastSelected().name }}</div>
-            <div>
-                {{ 'asset.original-asset-size' | translate }}: {{ lastSelected().fileSize | filesize }}
-            </div>
-            <div>
-                <button (click)="previewAsset(lastSelected())" class="btn btn-link">
-                    <clr-icon shape="eye"></clr-icon> {{ 'asset.preview' | translate }}
-                </button>
-            </div>
-            <div>
-                <a [routerLink]="['./', lastSelected().id]" class="btn btn-link">
-                    <clr-icon shape="pencil"></clr-icon> {{ 'common.edit' | translate }}
-                </a>
-            </div>
-            <div *ngIf="selection.length === 1 && canDelete">
-                <button (click)="deleteAsset.emit(lastSelected())" class="btn btn-link">
+            <div>{{ 'asset.original-asset-size' | translate }}: {{ lastSelected().fileSize | filesize }}</div>
+            <ng-container *ngIf="selection.length === 1">
+                <div>
+                    <button (click)="previewAsset(lastSelected())" class="btn btn-link">
+                        <clr-icon shape="eye"></clr-icon> {{ 'asset.preview' | translate }}
+                    </button>
+                </div>
+                <div>
+                    <a [routerLink]="['./', lastSelected().id]" class="btn btn-link">
+                        <clr-icon shape="pencil"></clr-icon> {{ 'common.edit' | translate }}
+                    </a>
+                </div>
+            </ng-container>
+            <div *ngIf="canDelete">
+                <button (click)="deleteAssets.emit(selection)" class="btn btn-link">
                     <clr-icon shape="trash" class="is-danger"></clr-icon> {{ 'common.delete' | translate }}
                 </button>
             </div>
@@ -56,15 +56,9 @@
     </div>
     <div class="card stack" [class.visible]="selection.length > 1"></div>
     <div class="selection-count" [class.visible]="selection.length > 1">
-        <clr-tooltip>
-            <div clrTooltipTrigger class="trigger">
-                {{ 'asset.assets-selected-count' | translate: { count: selection.length } }}
-            </div>
-            <clr-tooltip-content vdrPosition="top-left" clrSize="lg">
-                <ul>
-                    <li *ngFor="let asset of selection">{{ asset.name }}</li>
-                </ul>
-            </clr-tooltip-content>
-        </clr-tooltip>
+        {{ 'asset.assets-selected-count' | translate: { count: selection.length } }}
+        <ul>
+            <li *ngFor="let asset of selection">{{ asset.name }}</li>
+        </ul>
     </div>
 </div>

+ 11 - 7
packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.scss

@@ -1,4 +1,4 @@
-@import "variables";
+@import 'variables';
 
 :host {
     display: flex;
@@ -36,7 +36,7 @@
     border-radius: 50%;
     top: -12px;
     left: -12px;
-    box-shadow: 0px 5px 5px -4px rgba(0,0,0,0.75);
+    box-shadow: 0px 5px 5px -4px rgba(0, 0, 0, 0.75);
     transition: opacity 0.1s;
 }
 
@@ -87,19 +87,23 @@
 
     .selection-count {
         opacity: 0;
+        position: relative;
         text-align: center;
         visibility: hidden;
         transition: opacity 0.3s, visibility 0s 0.3s;
-        .trigger {
-            cursor: pointer;
-            color: $color-grey-400;
-            text-decoration: underline;
-        }
         &.visible {
             opacity: 1;
             visibility: visible;
             transition: opacity 0.3s, visibility 0s;
         }
+        ul {
+            text-align: left;
+            list-style-type: none;
+            margin-left: 12px;
+            li {
+                font-size: 12px;
+            }
+        }
     }
 
     .placeholder {

+ 12 - 3
packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.ts

@@ -18,7 +18,7 @@ export class AssetGalleryComponent implements OnChanges {
     @Input() multiSelect = false;
     @Input() canDelete = false;
     @Output() selectionChange = new EventEmitter<Asset[]>();
-    @Output() deleteAsset = new EventEmitter<Asset>();
+    @Output() deleteAssets = new EventEmitter<Asset[]>();
 
     selection: Asset[] = [];
 
@@ -38,8 +38,17 @@ export class AssetGalleryComponent implements OnChanges {
 
     toggleSelection(event: MouseEvent, asset: Asset) {
         const index = this.selection.findIndex((a) => a.id === asset.id);
-        if (index === -1) {
-            if (this.multiSelect && event.ctrlKey) {
+        if (this.multiSelect && event.shiftKey && 1 <= this.selection.length) {
+            const lastSelection = this.selection[this.selection.length - 1];
+            const lastSelectionIndex = this.assets.findIndex((a) => a.id === lastSelection.id);
+            const currentIndex = this.assets.findIndex((a) => a.id === asset.id);
+            const start = currentIndex < lastSelectionIndex ? currentIndex : lastSelectionIndex;
+            const end = currentIndex > lastSelectionIndex ? currentIndex + 1 : lastSelectionIndex;
+            this.selection.push(
+                ...this.assets.slice(start, end).filter((a) => !this.selection.find((s) => s.id === a.id)),
+            );
+        } else if (index === -1) {
+            if (this.multiSelect && (event.ctrlKey || event.shiftKey)) {
                 this.selection.push(asset);
             } else {
                 this.selection = [asset];

+ 3 - 3
packages/admin-ui/src/lib/core/src/shared/components/simple-dialog/simple-dialog.component.html

@@ -1,5 +1,5 @@
-<ng-template vdrDialogTitle>{{ title | translate }}</ng-template>
-{{ body | translate }}
+<ng-template vdrDialogTitle>{{ title | translate:translationVars }}</ng-template>
+{{ body | translate:translationVars }}
 <ng-template vdrDialogButtons>
     <ng-container *ngFor="let button of buttons">
         <button
@@ -8,7 +8,7 @@
             [class.btn-danger]="button.type === 'danger'"
             (click)="resolveWith(button.returnValue)"
         >
-            {{ button.label | translate }}
+            {{ button.label | translate:translationVars }}
         </button>
     </ng-container>
 </ng-template>

+ 1 - 0
packages/admin-ui/src/lib/core/src/shared/components/simple-dialog/simple-dialog.component.ts

@@ -15,5 +15,6 @@ export class SimpleDialogComponent implements Dialog<any> {
     resolveWith: (result?: any) => void;
     title = '';
     body = '';
+    translationVars = {};
     buttons: Array<DialogButtonConfig<any>> = [];
 }

+ 23 - 19
packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts

@@ -94,12 +94,12 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
         this.init();
         this.availableCountries$ = this.dataService.settings
             .getAvailableCountries()
-            .mapSingle(result => result.countries.items)
+            .mapSingle((result) => result.countries.items)
             .pipe(shareReplay(1));
 
         const customerWithUpdates$ = this.entity$.pipe(merge(this.orderListUpdates$));
-        this.orders$ = customerWithUpdates$.pipe(map(customer => customer.orders.items));
-        this.ordersCount$ = this.entity$.pipe(map(customer => customer.orders.totalItems));
+        this.orders$ = customerWithUpdates$.pipe(map((customer) => customer.orders.items));
+        this.ordersCount$ = this.entity$.pipe(map((customer) => customer.orders.totalItems));
         this.history$ = this.fetchHistory.pipe(
             startWith(null),
             switchMap(() => {
@@ -109,7 +109,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                             createdAt: SortOrder.DESC,
                         },
                     })
-                    .mapStream(data => data.customer?.history.items);
+                    .mapStream((data) => data.customer?.history.items);
             }),
         );
     }
@@ -172,15 +172,17 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
             return;
         }
         const formValue = customerForm.value;
+        const customFields = customerForm.get('customFields')?.value;
         const customer: CreateCustomerInput = {
             title: formValue.title,
             emailAddress: formValue.emailAddress,
             firstName: formValue.firstName,
             lastName: formValue.lastName,
             phoneNumber: formValue.phoneNumber,
+            customFields,
         };
         this.dataService.customer.createCustomer(customer, formValue.password).subscribe(
-            data => {
+            (data) => {
                 this.notificationService.success(_('common.notify-create-success'), {
                     entity: 'Customer',
                 });
@@ -197,7 +199,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createCustomer.id], { relativeTo: this.route });
             },
-            err => {
+            (err) => {
                 this.notificationService.error(_('common.notify-create-error'), {
                     entity: 'Customer',
                 });
@@ -214,6 +216,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                     const customerForm = this.detailForm.get('customer');
                     if (customerForm && customerForm.dirty) {
                         const formValue = customerForm.value;
+                        const customFields = customerForm.get('customFields')?.value;
                         const customer: UpdateCustomerInput = {
                             id,
                             title: formValue.title,
@@ -221,6 +224,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                             firstName: formValue.firstName,
                             lastName: formValue.lastName,
                             phoneNumber: formValue.phoneNumber,
+                            customFields,
                         };
                         saveOperations.push(this.dataService.customer.updateCustomer(customer));
                     }
@@ -261,7 +265,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                 }),
             )
             .subscribe(
-                data => {
+                (data) => {
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'Customer',
                     });
@@ -270,7 +274,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                     this.changeDetector.markForCheck();
                     this.fetchHistory.next();
                 },
-                err => {
+                (err) => {
                     this.notificationService.error(_('common.notify-update-error'), {
                         entity: 'Customer',
                     });
@@ -284,11 +288,11 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                 size: 'md',
             })
             .pipe(
-                switchMap(groupIds => (groupIds ? from(groupIds) : EMPTY)),
-                concatMap(groupId => this.dataService.customer.addCustomersToGroup(groupId, [this.id])),
+                switchMap((groupIds) => (groupIds ? from(groupIds) : EMPTY)),
+                concatMap((groupId) => this.dataService.customer.addCustomersToGroup(groupId, [this.id])),
             )
             .subscribe({
-                next: res => {
+                next: (res) => {
                     this.notificationService.success(_(`customer.add-customers-to-group-success`), {
                         customerCount: 1,
                         groupName: res.addCustomersToGroup.name,
@@ -311,14 +315,14 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                 ],
             })
             .pipe(
-                switchMap(response =>
+                switchMap((response) =>
                     response
                         ? this.dataService.customer.removeCustomersFromGroup(group.id, [this.id])
                         : EMPTY,
                 ),
                 switchMap(() => this.dataService.customer.getCustomer(this.id, { take: 0 }).single$),
             )
-            .subscribe(result => {
+            .subscribe((result) => {
                 this.notificationService.success(_(`customer.remove-customers-from-group-success`), {
                     customerCount: 1,
                     groupName: group.name,
@@ -346,7 +350,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                 },
             })
             .pipe(
-                switchMap(result => {
+                switchMap((result) => {
                     if (result) {
                         return this.dataService.customer.updateCustomerNote({
                             noteId: entry.id,
@@ -357,7 +361,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                     }
                 }),
             )
-            .subscribe(result => {
+            .subscribe((result) => {
                 this.fetchHistory.next();
                 this.notificationService.success(_('common.notify-update-success'), {
                     entity: 'Note',
@@ -375,7 +379,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                     { type: 'danger', label: _('common.delete'), returnValue: true },
                 ],
             })
-            .pipe(switchMap(res => (res ? this.dataService.customer.deleteCustomerNote(entry.id) : EMPTY)))
+            .pipe(switchMap((res) => (res ? this.dataService.customer.deleteCustomerNote(entry.id) : EMPTY)))
             .subscribe(() => {
                 this.fetchHistory.next();
                 this.notificationService.success(_('common.notify-delete-success'), {
@@ -417,7 +421,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
 
             for (const fieldDef of this.customFields) {
                 const key = fieldDef.name;
-                const value = (entity as any).customFields[key];
+                const value = (entity as any).customFields?.[key];
                 const control = customFieldsGroup.get(key);
                 if (control) {
                     control.patchValue(value);
@@ -437,9 +441,9 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                 skip: (this.currentOrdersPage - 1) * this.ordersPerPage,
             })
             .single$.pipe(
-                map(data => data.customer),
+                map((data) => data.customer),
                 filter(notNullOrUndefined),
             )
-            .subscribe(result => this.orderListUpdates$.next(result));
+            .subscribe((result) => this.orderListUpdates$.next(result));
     }
 }

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -60,6 +60,7 @@
     "collection-contents": "Inhalt der Sammlung",
     "confirm-adding-options-delete-default-body": "Das Hinzufügen von Optionen zu diesem Produkt führt dazu, dass die vorhandene Standardvariante gelöscht wird. Möchten Sie fortfahren?",
     "confirm-adding-options-delete-default-title": "Standardvariante löschen?",
+    "confirm-delete-assets": "{count} {count, plural, one {Asset} other {Assets}} löschen?",
     "confirm-delete-administrator": "",
     "confirm-delete-asset": "Asset löschen?",
     "confirm-delete-channel": "Kanal löschen?",
@@ -685,4 +686,4 @@
     "job-result": "Job-Ergebnis",
     "job-state": "Job-Status"
   }
-}
+}

+ 2 - 2
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -61,7 +61,7 @@
     "confirm-adding-options-delete-default-body": "Adding options to this product will cause the existing default variant to be deleted. Do you wish to proceed?",
     "confirm-adding-options-delete-default-title": "Delete default variant?",
     "confirm-delete-administrator": "Delete administrator?",
-    "confirm-delete-asset": "Delete asset?",
+    "confirm-delete-assets": "Delete {count} {count, plural, one {asset} other {assets}}?",
     "confirm-delete-channel": "Delete channel?",
     "confirm-delete-collection": "Delete collection?",
     "confirm-delete-collection-and-children-body": "Deleting this collection will also delete all child collections",
@@ -685,4 +685,4 @@
     "job-result": "Job result",
     "job-state": "Job state"
   }
-}
+}

+ 2 - 2
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -61,7 +61,7 @@
     "confirm-adding-options-delete-default-body": "Añadir optiones a este producto eliminará la variante por defecto. ¿Desea continuar?",
     "confirm-adding-options-delete-default-title": "¿Eliminar la variante por defecto?",
     "confirm-delete-administrator": "",
-    "confirm-delete-asset": "¿Eliminar archivo?",
+    "confirm-delete-assets": "¿Eliminar archivo?",
     "confirm-delete-channel": "¿Eliminar canal de ventas?",
     "confirm-delete-collection": "¿Eliminar colección?",
     "confirm-delete-collection-and-children-body": "Eliminar esta colección también eliminará las sub-colecciones",
@@ -685,4 +685,4 @@
     "job-result": "Resultado",
     "job-state": "Estado"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -62,6 +62,7 @@
     "confirm-adding-options-delete-default-title": "Usunąć domyślny tytuł?",
     "confirm-delete-administrator": "",
     "confirm-delete-asset": "",
+    "confirm-delete-assets": "",
     "confirm-delete-channel": "Usunąć kanał?",
     "confirm-delete-collection": "Usunąć kolekcje?",
     "confirm-delete-collection-and-children-body": "Usunięcie tej kolekcji spowoduje usunięcie także podkategorii",
@@ -685,4 +686,4 @@
     "job-result": "Rezultat zlecenia",
     "job-state": "Status zlecenia"
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -62,6 +62,7 @@
     "confirm-adding-options-delete-default-title": "确认删除产品规格么?",
     "confirm-delete-administrator": "",
     "confirm-delete-asset": "",
+    "confirm-delete-assets": "",
     "confirm-delete-channel": "确认删除销售渠道?",
     "confirm-delete-collection": "确认删除商品系列吗?",
     "confirm-delete-collection-and-children-body": "删除这个系列会删除它所包含的子系列,确认删除码?",
@@ -685,4 +686,4 @@
     "job-result": "",
     "job-state": ""
   }
-}
+}

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -62,6 +62,7 @@
     "confirm-adding-options-delete-default-title": "確認移除產品規格嗎?",
     "confirm-delete-administrator": "",
     "confirm-delete-asset": "",
+    "confirm-delete-assets": "",
     "confirm-delete-channel": "確認移除渠道?",
     "confirm-delete-collection": "確認移除商品系列吗?",
     "confirm-delete-collection-and-children-body": "移除這個系列會移除它所包含的子系列,確認移除嗎?",
@@ -685,4 +686,4 @@
     "job-result": "",
     "job-state": ""
   }
-}
+}

+ 7 - 1
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -1781,7 +1781,8 @@ export type Mutation = {
     updateAsset: Asset;
     /** Delete an Asset */
     deleteAsset: DeletionResponse;
-    /** @deprecated Use `authenticate` mutation with the 'native' strategy instead. */
+    /** Delete multiple Assets */
+    deleteAssets: DeletionResponse;
     login: LoginResult;
     authenticate: LoginResult;
     logout: Scalars['Boolean'];
@@ -1954,6 +1955,11 @@ export type MutationDeleteAssetArgs = {
     force?: Maybe<Scalars['Boolean']>;
 };
 
+export type MutationDeleteAssetsArgs = {
+    ids: Array<Scalars['ID']>;
+    force?: Maybe<Scalars['Boolean']>;
+};
+
 export type MutationLoginArgs = {
     username: Scalars['String'];
     password: Scalars['String'];

+ 3 - 3
packages/asset-server-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/asset-server-plugin",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -22,8 +22,8 @@
     "@types/fs-extra": "^8.0.1",
     "@types/node-fetch": "^2.5.4",
     "@types/sharp": "^0.24.0",
-    "@vendure/common": "^0.13.0",
-    "@vendure/core": "^0.13.0",
+    "@vendure/common": "^0.13.1",
+    "@vendure/core": "^0.13.1",
     "aws-sdk": "^2.670.0",
     "express": "^4.16.4",
     "node-fetch": "^2.6.0",

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/common",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "main": "index.js",
   "license": "MIT",
   "scripts": {

+ 12 - 5
packages/common/src/generated-types.ts

@@ -605,7 +605,7 @@ export type CreateZoneInput = {
 /**
  * @description
  * ISO 4217 currency code
- * 
+ *
  * @docsCategory common
  */
 export enum CurrencyCode {
@@ -1393,7 +1393,7 @@ export type JobSortParameter = {
 /**
  * @description
  * The state of a Job in the JobQueue
- * 
+ *
  * @docsCategory common
  */
 export enum JobState {
@@ -1411,7 +1411,7 @@ export enum JobState {
  * 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 {
@@ -1780,7 +1780,8 @@ export type Mutation = {
   updateAsset: Asset;
   /** Delete an Asset */
   deleteAsset: DeletionResponse;
-  /** @deprecated Use `authenticate` mutation with the 'native' strategy instead. */
+  /** Delete multiple Assets */
+  deleteAssets: DeletionResponse;
   login: LoginResult;
   authenticate: LoginResult;
   logout: Scalars['Boolean'];
@@ -1961,6 +1962,12 @@ export type MutationDeleteAssetArgs = {
 };
 
 
+export type MutationDeleteAssetsArgs = {
+  ids: Array<Scalars['ID']>;
+  force?: Maybe<Scalars['Boolean']>;
+};
+
+
 export type MutationLoginArgs = {
   username: Scalars['String'];
   password: Scalars['String'];
@@ -2580,7 +2587,7 @@ export type PaymentMethodSortParameter = {
  * @description
  * Permissions for administrators and customers. Used to control access to
  * GraphQL resolvers via the {@link Allow} decorator.
- * 
+ *
  * @docsCategory common
  */
 export enum Permission {

+ 7 - 1
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1781,7 +1781,8 @@ export type Mutation = {
     updateAsset: Asset;
     /** Delete an Asset */
     deleteAsset: DeletionResponse;
-    /** @deprecated Use `authenticate` mutation with the 'native' strategy instead. */
+    /** Delete multiple Assets */
+    deleteAssets: DeletionResponse;
     login: LoginResult;
     authenticate: LoginResult;
     logout: Scalars['Boolean'];
@@ -1954,6 +1955,11 @@ export type MutationDeleteAssetArgs = {
     force?: Maybe<Scalars['Boolean']>;
 };
 
+export type MutationDeleteAssetsArgs = {
+    ids: Array<Scalars['ID']>;
+    force?: Maybe<Scalars['Boolean']>;
+};
+
 export type MutationLoginArgs = {
     username: Scalars['String'];
     password: Scalars['String'];

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/core",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "description": "A modern, headless ecommerce framework",
   "repository": {
     "type": "git",
@@ -47,7 +47,7 @@
     "@nestjs/testing": "7.0.5",
     "@nestjs/typeorm": "7.0.0",
     "@types/fs-extra": "^8.0.1",
-    "@vendure/common": "^0.13.0",
+    "@vendure/common": "^0.13.1",
     "apollo-server-express": "2.11.0",
     "bcrypt": "^4.0.1",
     "body-parser": "^1.19.0",

+ 22 - 0
packages/core/src/api/config/__snapshots__/graphql-custom-fields.spec.ts.snap

@@ -257,3 +257,25 @@ input OrderLineCustomFieldsInput {
 }
 "
 `;
+
+exports[`addRegisterCustomerCustomFieldsInput() add public writable custom fields to RegisterCustomerInput 1`] = `
+"type Mutation {
+  registerCustomerAccount(input: RegisterCustomerInput!): Boolean!
+}
+
+input RegisterCustomerCustomFieldsInput {
+  isB2B: Boolean
+  message: String
+}
+
+input RegisterCustomerInput {
+  emailAddress: String!
+  title: String
+  firstName: String
+  lastName: String
+  phoneNumber: String
+  password: String
+  customFields: RegisterCustomerCustomFieldsInput
+}
+"
+`;

+ 7 - 3
packages/core/src/api/config/configure-graphql-module.ts

@@ -25,6 +25,7 @@ import { generateListOptions } from './generate-list-options';
 import {
     addGraphQLCustomFields,
     addOrderLineCustomFieldsInput,
+    addRegisterCustomerCustomFieldsInput,
     addServerConfigCustomFields,
 } from './graphql-custom-fields';
 
@@ -159,7 +160,7 @@ async function createGraphQLOptions(
         const customFields = configService.customFields;
         // Paths must be normalized to use forward-slash separators.
         // See https://github.com/nestjs/graphql/issues/336
-        const normalizedPaths = options.typePaths.map(p => p.split(path.sep).join('/'));
+        const normalizedPaths = options.typePaths.map((p) => p.split(path.sep).join('/'));
         const typeDefs = await typesLoader.mergeTypesByPaths(normalizedPaths);
         const authStrategies =
             apiType === 'shop'
@@ -168,14 +169,17 @@ async function createGraphQLOptions(
         let schema = buildSchema(typeDefs);
 
         getPluginAPIExtensions(configService.plugins, apiType)
-            .map(e => (typeof e.schema === 'function' ? e.schema() : e.schema))
+            .map((e) => (typeof e.schema === 'function' ? e.schema() : e.schema))
             .filter(notNullOrUndefined)
-            .forEach(documentNode => (schema = extendSchema(schema, documentNode)));
+            .forEach((documentNode) => (schema = extendSchema(schema, documentNode)));
         schema = generateListOptions(schema);
         schema = addGraphQLCustomFields(schema, customFields, apiType === 'shop');
         schema = addServerConfigCustomFields(schema, customFields);
         schema = addOrderLineCustomFieldsInput(schema, customFields.OrderLine || []);
         schema = generateAuthenticationTypes(schema, authStrategies);
+        if (apiType === 'shop') {
+            schema = addRegisterCustomerCustomFieldsInput(schema, customFields.Customer || []);
+        }
 
         return printSchema(schema);
     }

+ 52 - 7
packages/core/src/api/config/graphql-custom-fields.spec.ts

@@ -2,7 +2,11 @@ import { printSchema } from 'graphql';
 
 import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custom-field-types';
 
-import { addGraphQLCustomFields, addOrderLineCustomFieldsInput } from './graphql-custom-fields';
+import {
+    addGraphQLCustomFields,
+    addOrderLineCustomFieldsInput,
+    addRegisterCustomerCustomFieldsInput,
+} from './graphql-custom-fields';
 
 describe('addGraphQLCustomFields()', () => {
     it('uses JSON scalar if no custom fields defined', () => {
@@ -43,7 +47,10 @@ describe('addGraphQLCustomFields()', () => {
                     }
                 `;
         const customFieldConfig: CustomFields = {
-            Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
+            Product: [
+                { name: 'available', type: 'boolean' },
+                { name: 'shortName', type: 'localeString' },
+            ],
         };
         const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
@@ -60,7 +67,10 @@ describe('addGraphQLCustomFields()', () => {
                     }
                 `;
         const customFieldConfig: CustomFields = {
-            Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
+            Product: [
+                { name: 'available', type: 'boolean' },
+                { name: 'shortName', type: 'localeString' },
+            ],
         };
         const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
@@ -77,7 +87,10 @@ describe('addGraphQLCustomFields()', () => {
                     }
                 `;
         const customFieldConfig: CustomFields = {
-            Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
+            Product: [
+                { name: 'available', type: 'boolean' },
+                { name: 'shortName', type: 'localeString' },
+            ],
         };
         const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
@@ -102,7 +115,10 @@ describe('addGraphQLCustomFields()', () => {
                     }
                 `;
         const customFieldConfig: CustomFields = {
-            Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
+            Product: [
+                { name: 'available', type: 'boolean' },
+                { name: 'shortName', type: 'localeString' },
+            ],
         };
         const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
@@ -124,7 +140,10 @@ describe('addGraphQLCustomFields()', () => {
                     }
                 `;
         const customFieldConfig: CustomFields = {
-            Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
+            Product: [
+                { name: 'available', type: 'boolean' },
+                { name: 'shortName', type: 'localeString' },
+            ],
         };
         const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
@@ -187,7 +206,6 @@ describe('addGraphQLCustomFields()', () => {
 });
 
 describe('addOrderLineCustomFieldsInput()', () => {
-
     it('Modifies the schema when the addItemToOrder & adjustOrderLine mutation is present', () => {
         const input = `
             type Mutation {
@@ -217,3 +235,30 @@ describe('addOrderLineCustomFieldsInput()', () => {
         expect(printSchema(result)).toMatchSnapshot();
     });
 });
+
+describe('addRegisterCustomerCustomFieldsInput()', () => {
+    it('add public writable custom fields to RegisterCustomerInput', () => {
+        const input = `
+            input RegisterCustomerInput {
+                emailAddress: String!
+                title: String
+                firstName: String
+                lastName: String
+                phoneNumber: String
+                password: String
+            }
+
+            type Mutation {
+                registerCustomerAccount(input: RegisterCustomerInput!): Boolean!
+            }
+        `;
+        const customFieldConfig: CustomFieldConfig[] = [
+            { name: 'isB2B', type: 'boolean', nullable: false },
+            { name: 'message', type: 'string' },
+            { name: 'rating', type: 'int', public: false },
+            { name: 'dbRef', type: 'int', internal: true },
+        ];
+        const result = addRegisterCustomerCustomFieldsInput(input, customFieldConfig);
+        expect(printSchema(result)).toMatchSnapshot();
+    });
+});

+ 37 - 6
packages/core/src/api/config/graphql-custom-fields.ts

@@ -32,15 +32,15 @@ export function addGraphQLCustomFields(
 
     for (const entityName of Object.keys(customFieldConfig)) {
         const customEntityFields = (customFieldConfig[entityName as keyof CustomFields] || []).filter(
-            config => {
+            (config) => {
                 return !config.internal && (publicOnly === true ? config.public !== false : true);
             },
         );
 
-        const localeStringFields = customEntityFields.filter(field => field.type === 'localeString');
-        const nonLocaleStringFields = customEntityFields.filter(field => field.type !== 'localeString');
-        const writeableLocaleStringFields = localeStringFields.filter(field => !field.readonly);
-        const writeableNonLocaleStringFields = nonLocaleStringFields.filter(field => !field.readonly);
+        const localeStringFields = customEntityFields.filter((field) => field.type === 'localeString');
+        const nonLocaleStringFields = customEntityFields.filter((field) => field.type !== 'localeString');
+        const writeableLocaleStringFields = localeStringFields.filter((field) => !field.readonly);
+        const writeableNonLocaleStringFields = nonLocaleStringFields.filter((field) => !field.readonly);
 
         if (schema.getType(entityName)) {
             if (customEntityFields.length) {
@@ -184,6 +184,37 @@ export function addServerConfigCustomFields(
     return extendSchema(schema, parse(customFieldTypeDefs));
 }
 
+/**
+ * If CustomFields are defined on the Customer entity, then an extra `customFields` field is added to
+ * the `RegisterCustomerInput` so that public writable custom fields can be set when a new customer
+ * is registered.
+ */
+export function addRegisterCustomerCustomFieldsInput(
+    typeDefsOrSchema: string | GraphQLSchema,
+    customerCustomFields: CustomFieldConfig[],
+): GraphQLSchema {
+    const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
+    if (!customerCustomFields || customerCustomFields.length === 0) {
+        return schema;
+    }
+    const publicWritableCustomFields = customerCustomFields.filter((fieldDef) => {
+        return fieldDef.public !== false && !fieldDef.readonly && !fieldDef.internal;
+    });
+    if (publicWritableCustomFields.length < 1) {
+        return schema;
+    }
+    const customFieldTypeDefs = `
+        input RegisterCustomerCustomFieldsInput {
+            ${mapToFields(publicWritableCustomFields, getGraphQlType)}
+        }
+
+        extend input RegisterCustomerInput {
+            customFields: RegisterCustomerCustomFieldsInput
+        }
+    `;
+    return extendSchema(schema, parse(customFieldTypeDefs));
+}
+
 /**
  * If CustomFields are defined on the OrderLine entity, then an extra `customFields` argument
  * must be added to the `addItemToOrder` and `adjustOrderLine` mutations.
@@ -240,7 +271,7 @@ type GraphQLFieldType = 'DateTime' | 'String' | 'Int' | 'Float' | 'Boolean' | 'I
  * Maps an array of CustomFieldConfig objects into a string of SDL fields.
  */
 function mapToFields(fieldDefs: CustomFieldConfig[], typeFn: (fieldType: CustomFieldType) => string): string {
-    return fieldDefs.map(field => `${field.name}: ${typeFn(field.type)}`).join('\n');
+    return fieldDefs.map((field) => `${field.name}: ${typeFn(field.type)}`).join('\n');
 }
 
 function getFilterOperator(type: CustomFieldType): string {

+ 8 - 1
packages/core/src/api/resolvers/admin/asset.resolver.ts

@@ -2,6 +2,7 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
     MutationCreateAssetsArgs,
     MutationDeleteAssetArgs,
+    MutationDeleteAssetsArgs,
     MutationUpdateAssetArgs,
     Permission,
     QueryAssetArgs,
@@ -53,6 +54,12 @@ export class AssetResolver {
     @Mutation()
     @Allow(Permission.DeleteCatalog)
     async deleteAsset(@Ctx() ctx: RequestContext, @Args() { id, force }: MutationDeleteAssetArgs) {
-        return this.assetService.delete(ctx, id, force || undefined);
+        return this.assetService.delete(ctx, [id], force || undefined);
+    }
+
+    @Mutation()
+    @Allow(Permission.DeleteCatalog)
+    async deleteAssets(@Ctx() ctx: RequestContext, @Args() { ids, force }: MutationDeleteAssetsArgs) {
+        return this.assetService.delete(ctx, ids, force || undefined);
     }
 }

+ 2 - 0
packages/core/src/api/schema/admin-api/asset.api.graphql

@@ -12,6 +12,8 @@ type Mutation {
     updateAsset(input: UpdateAssetInput!): Asset!
     "Delete an Asset"
     deleteAsset(id: ID!, force: Boolean): DeletionResponse!
+    "Delete multiple Assets"
+    deleteAssets(ids: [ID!]!, force: Boolean): DeletionResponse!
 }
 
 # generated by generateListOptions function

+ 1 - 1
packages/core/src/i18n/messages/en.json

@@ -79,7 +79,7 @@
     "unexpected-password-on-registration": "Do not provide a password when `authOptions.requireVerification` is set to \"true\""
   },
   "message": {
-    "asset-to-be-deleted-is-featured": "The selected Asset is featured by {products, plural, =0 {} one {1 Product} other {# Products}}{variants, plural, =0 {} one { 1 ProductVariant} other { # ProductVariants}}{collections, plural, =0 {} one { 1 Collection} other { # Collections}}",
+    "asset-to-be-deleted-is-featured": "The selected {assetCount, plural, one {Asset is} other {Assets are}} featured by {products, plural, =0 {} one {1 Product} other {# Products}} {variants, plural, =0 {} one { 1 ProductVariant} other { # ProductVariants}} {collections, plural, =0 {} one { 1 Collection} other { # Collections}}",
     "country-used-in-addresses": "The selected Country cannot be deleted as it is used in {count, plural, one {1 Address} other {# Addresses}}",
     "facet-force-deleted": "The Facet was deleted and its FacetValues were removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
     "facet-used": "The selected Facet includes FacetValues which are assigned to {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",

+ 30 - 17
packages/core/src/service/services/asset.service.ts

@@ -155,31 +155,44 @@ export class AssetService {
         return updatedAsset;
     }
 
-    async delete(ctx: RequestContext, id: ID, force: boolean = false): Promise<DeletionResponse> {
-        const asset = await getEntityOrThrow(this.connection, Asset, id);
-        const usages = await this.findAssetUsages(asset);
-        const hasUsages = !!(usages.products.length || usages.variants.length || usages.collections.length);
+    async delete(ctx: RequestContext, ids: ID[], force: boolean = false): Promise<DeletionResponse> {
+        const assets = await this.connection.getRepository(Asset).findByIds(ids);
+        const usageCount = {
+            products: 0,
+            variants: 0,
+            collections: 0,
+        };
+        for (const asset of assets) {
+            const usages = await this.findAssetUsages(asset);
+            usageCount.products += usages.products.length;
+            usageCount.variants += usages.variants.length;
+            usageCount.collections += usages.collections.length;
+        }
+        const hasUsages = !!(usageCount.products || usageCount.variants || usageCount.collections);
         if (hasUsages && !force) {
             return {
                 result: DeletionResult.NOT_DELETED,
                 message: ctx.translate('message.asset-to-be-deleted-is-featured', {
-                    products: usages.products.length,
-                    variants: usages.variants.length,
-                    collections: usages.collections.length,
+                    assetCount: assets.length,
+                    products: usageCount.products,
+                    variants: usageCount.variants,
+                    collections: usageCount.collections,
                 }),
             };
         }
-        // Create a new asset so that the id is still available
-        // after deletion (the .remove() method sets it to undefined)
-        const deletedAsset = new Asset(asset);
-        await this.connection.getRepository(Asset).remove(asset);
-        try {
-            await this.configService.assetOptions.assetStorageStrategy.deleteFile(asset.source);
-            await this.configService.assetOptions.assetStorageStrategy.deleteFile(asset.preview);
-        } catch (e) {
-            Logger.error(`error.could-not-delete-asset-file`, undefined, e.stack);
+        for (const asset of assets) {
+            // Create a new asset so that the id is still available
+            // after deletion (the .remove() method sets it to undefined)
+            const deletedAsset = new Asset(asset);
+            await this.connection.getRepository(Asset).remove(asset);
+            try {
+                await this.configService.assetOptions.assetStorageStrategy.deleteFile(asset.source);
+                await this.configService.assetOptions.assetStorageStrategy.deleteFile(asset.preview);
+            } catch (e) {
+                Logger.error(`error.could-not-delete-asset-file`, undefined, e.stack);
+            }
+            this.eventBus.publish(new AssetEvent(ctx, deletedAsset, 'deleted'));
         }
-        this.eventBus.publish(new AssetEvent(ctx, deletedAsset, 'deleted'));
         return {
             result: DeletionResult.DELETED,
         };

+ 2 - 0
packages/core/src/service/services/customer.service.ts

@@ -181,12 +181,14 @@ export class CustomerService {
                 return false;
             }
         }
+        const customFields = (input as any).customFields;
         const customer = await this.createOrUpdate({
             emailAddress: input.emailAddress,
             title: input.title || '',
             firstName: input.firstName || '',
             lastName: input.lastName || '',
             phoneNumber: input.phoneNumber || '',
+            ...(customFields ? { customFields } : {}),
         });
         await this.historyService.createHistoryEntryForCustomer({
             customerId: customer.id,

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/create",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "license": "MIT",
   "bin": {
     "create": "./index.js"
@@ -26,13 +26,13 @@
     "@types/handlebars": "^4.1.0",
     "@types/listr": "^0.14.0",
     "@types/semver": "^6.0.0",
-    "@vendure/core": "^0.13.0",
+    "@vendure/core": "^0.13.1",
     "rimraf": "^3.0.0",
     "ts-node": "^8.4.1",
     "typescript": "3.8.3"
   },
   "dependencies": {
-    "@vendure/common": "^0.13.0",
+    "@vendure/common": "^0.13.1",
     "chalk": "^3.0.0",
     "commander": "^5.0.0",
     "cross-spawn": "^7.0.1",

+ 9 - 9
packages/dev-server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "dev-server",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "main": "index.js",
   "license": "MIT",
   "private": true,
@@ -14,18 +14,18 @@
     "load-test:100k": "node -r ts-node/register load-testing/run-load-test.ts 100000"
   },
   "dependencies": {
-    "@vendure/admin-ui-plugin": "^0.13.0",
-    "@vendure/asset-server-plugin": "^0.13.0",
-    "@vendure/common": "^0.13.0",
-    "@vendure/core": "^0.13.0",
-    "@vendure/elasticsearch-plugin": "^0.13.0",
-    "@vendure/email-plugin": "^0.13.0",
+    "@vendure/admin-ui-plugin": "^0.13.1",
+    "@vendure/asset-server-plugin": "^0.13.1",
+    "@vendure/common": "^0.13.1",
+    "@vendure/core": "^0.13.1",
+    "@vendure/elasticsearch-plugin": "^0.13.1",
+    "@vendure/email-plugin": "^0.13.1",
     "typescript": "3.8.3"
   },
   "devDependencies": {
     "@types/csv-stringify": "^3.1.0",
-    "@vendure/testing": "^0.13.0",
-    "@vendure/ui-devkit": "^0.13.0",
+    "@vendure/testing": "^0.13.1",
+    "@vendure/ui-devkit": "^0.13.1",
     "concurrently": "^5.0.0",
     "csv-stringify": "^5.3.3"
   }

+ 7 - 1
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -1781,7 +1781,8 @@ export type Mutation = {
     updateAsset: Asset;
     /** Delete an Asset */
     deleteAsset: DeletionResponse;
-    /** @deprecated Use `authenticate` mutation with the 'native' strategy instead. */
+    /** Delete multiple Assets */
+    deleteAssets: DeletionResponse;
     login: LoginResult;
     authenticate: LoginResult;
     logout: Scalars['Boolean'];
@@ -1954,6 +1955,11 @@ export type MutationDeleteAssetArgs = {
     force?: Maybe<Scalars['Boolean']>;
 };
 
+export type MutationDeleteAssetsArgs = {
+    ids: Array<Scalars['ID']>;
+    force?: Maybe<Scalars['Boolean']>;
+};
+
 export type MutationLoginArgs = {
     username: Scalars['String'];
     password: Scalars['String'];

+ 3 - 3
packages/elasticsearch-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/elasticsearch-plugin",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -22,8 +22,8 @@
     "deepmerge": "^4.0.0"
   },
   "devDependencies": {
-    "@vendure/common": "^0.13.0",
-    "@vendure/core": "^0.13.0",
+    "@vendure/common": "^0.13.1",
+    "@vendure/core": "^0.13.1",
     "rimraf": "^3.0.0",
     "typescript": "3.8.3"
   }

+ 3 - 3
packages/email-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/email-plugin",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -33,8 +33,8 @@
     "@types/handlebars": "^4.1.0",
     "@types/mjml": "^4.0.2",
     "@types/nodemailer": "^6.4.0",
-    "@vendure/common": "^0.13.0",
-    "@vendure/core": "^0.13.0",
+    "@vendure/common": "^0.13.1",
+    "@vendure/core": "^0.13.1",
     "rimraf": "^3.0.0",
     "typescript": "3.8.3"
   }

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/testing",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "description": "End-to-end testing tools for Vendure projects",
   "keywords": [
     "vendure",
@@ -33,7 +33,7 @@
   },
   "dependencies": {
     "@types/node-fetch": "^2.5.4",
-    "@vendure/common": "^0.13.0",
+    "@vendure/common": "^0.13.1",
     "faker": "^4.1.0",
     "form-data": "^3.0.0",
     "graphql": "^14.5.8",
@@ -44,7 +44,7 @@
   "devDependencies": {
     "@types/mysql": "^2.15.8",
     "@types/pg": "^7.14.1",
-    "@vendure/core": "^0.13.0",
+    "@vendure/core": "^0.13.1",
     "mysql": "^2.17.1",
     "pg": "^7.17.1",
     "rimraf": "^3.0.0",

+ 4 - 4
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/ui-devkit",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "description": "A library for authoring Vendure Admin UI extensions",
   "keywords": [
     "vendure",
@@ -39,8 +39,8 @@
     "@angular/cli": "^9.0.5",
     "@angular/compiler": "^9.0.6",
     "@angular/compiler-cli": "^9.0.6",
-    "@vendure/admin-ui": "^0.13.0",
-    "@vendure/common": "^0.13.0",
+    "@vendure/admin-ui": "^0.13.1",
+    "@vendure/common": "^0.13.1",
     "chalk": "^3.0.0",
     "chokidar": "^3.3.1",
     "fs-extra": "^9.0.0",
@@ -51,7 +51,7 @@
     "@rollup/plugin-node-resolve": "^7.1.1",
     "@types/fs-extra": "^8.1.0",
     "@types/glob": "^7.1.1",
-    "@vendure/core": "^0.13.0",
+    "@vendure/core": "^0.13.1",
     "rimraf": "^3.0.0",
     "rollup": "^2.2.0",
     "rollup-plugin-terser": "^5.3.0",

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


Some files were not shown because too many files changed in this diff