Browse Source

feat(admin-ui): Implement values pagination for Facet detail view

Closes #1257
Michael Bromley 2 years ago
parent
commit
4cf1826f4a

+ 95 - 68
packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.html

@@ -100,75 +100,102 @@
                 [title]="'catalog.facet-values' | translate"
                 [paddingX]="false"
             >
-                <table
-                    class="facet-values-list table"
-                    formArrayName="values"
-                    *ngIf="0 < getValuesFormArray().length"
-                >
-                    <thead>
-                        <tr>
-                            <th></th>
-                            <th>{{ 'common.name' | translate }}</th>
-                            <th>{{ 'common.code' | translate }}</th>
-                            <ng-container *ngIf="customValueFields.length">
-                                <th>{{ 'common.custom-fields' | translate }}</th>
-                            </ng-container>
-                            <th></th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <tr
-                            class="facet-value"
-                            *ngFor="let value of values; let i = index"
-                            [formGroup]="detailForm.get(['values', i])"
-                        >
-                            <td class="align-middle">
-                                <vdr-entity-info [entity]="value"></vdr-entity-info>
-                            </td>
-                            <td class="align-middle">
-                                <input
-                                    type="text"
-                                    formControlName="name"
-                                    [readonly]="!(updatePermission | hasPermission)"
-                                    (input)="updateValueCode(entity?.values[i]?.code, $event.target.value, i)"
-                                />
-                            </td>
-                            <td class="align-middle">
-                                <input type="text" formControlName="code" />
-                            </td>
-                            <td class="" *ngIf="customValueFields.length">
-                                <vdr-tabbed-custom-fields
-                                    entityName="FacetValue"
-                                    [customFields]="customValueFields"
-                                    [compact]="true"
-                                    [customFieldsFormGroup]="detailForm.get(['values', i, 'customFields'])"
-                                    [readonly]="!(updatePermission | hasPermission)"
-                                ></vdr-tabbed-custom-fields>
-                            </td>
-                            <td class="align-middle">
-                                <vdr-dropdown>
-                                    <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                                        {{ 'common.actions' | translate }}
-                                        <clr-icon shape="caret down"></clr-icon>
-                                    </button>
-                                    <vdr-dropdown-menu vdrPosition="bottom-right">
-                                        <button
-                                            type="button"
-                                            class="delete-button"
-                                            (click)="deleteFacetValue(entity?.values[i]?.id, i)"
-                                            [disabled]="!(updatePermission | hasPermission)"
-                                            vdrDropdownItem
-                                        >
-                                            <clr-icon shape="trash" class="is-danger"></clr-icon>
-                                            {{ 'common.delete' | translate }}
+                <ng-template vdrCardControls>
+                    <input
+                        type="text"
+                        class="mr-3"
+                        [formControl]="filterControl"
+                        [placeholder]="'catalog.filter-by-name' | translate"
+                    />
+                </ng-template>
+                <ng-container *ngIf="filteredValues$ | async as filteredValues">
+                    <table class="facet-values-list table" formArrayName="values">
+                        <thead>
+                            <tr>
+                                <th></th>
+                                <th>{{ 'common.name' | translate }}</th>
+                                <th>{{ 'common.code' | translate }}</th>
+                                <ng-container *ngIf="customValueFields.length">
+                                    <th>{{ 'common.custom-fields' | translate }}</th>
+                                </ng-container>
+                                <th></th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr
+                                class="facet-value"
+                                *ngFor="
+                                    let value of filteredValues
+                                        | paginate
+                                            : {
+                                                  currentPage: currentPage,
+                                                  itemsPerPage: itemsPerPage,
+                                                  totalItems: filteredValues.length
+                                              };
+                                    let i = index
+                                "
+                                [formGroup]="detailForm.get(['values', value.id])"
+                            >
+                                <td class="align-middle">
+                                    <vdr-entity-info [entity]="value"></vdr-entity-info>
+                                </td>
+                                <td class="align-middle">
+                                    <input
+                                        type="text"
+                                        formControlName="name"
+                                        [readonly]="!(updatePermission | hasPermission)"
+                                        (input)="updateValueCode(value.code, $event.target.value, value.id)"
+                                    />
+                                </td>
+                                <td class="align-middle">
+                                    <input type="text" formControlName="code" />
+                                </td>
+                                <td class="" *ngIf="customValueFields.length">
+                                    <vdr-tabbed-custom-fields
+                                        entityName="FacetValue"
+                                        [customFields]="customValueFields"
+                                        [compact]="true"
+                                        [customFieldsFormGroup]="
+                                            detailForm.get(['values', value.id, 'customFields'])
+                                        "
+                                        [readonly]="!(updatePermission | hasPermission)"
+                                    ></vdr-tabbed-custom-fields>
+                                </td>
+                                <td class="align-middle">
+                                    <vdr-dropdown>
+                                        <button type="button" class="icon-button" vdrDropdownTrigger>
+                                            <clr-icon shape="ellipsis-vertical"></clr-icon>
                                         </button>
-                                    </vdr-dropdown-menu>
-                                </vdr-dropdown>
-                            </td>
-                        </tr>
-                    </tbody>
-                </table>
-
+                                        <vdr-dropdown-menu vdrPosition="bottom-right">
+                                            <button
+                                                type="button"
+                                                class="delete-button"
+                                                (click)="deleteFacetValue(value.id)"
+                                                [disabled]="!(updatePermission | hasPermission)"
+                                                vdrDropdownItem
+                                            >
+                                                <clr-icon shape="trash" class="is-danger"></clr-icon>
+                                                {{ 'common.delete' | translate }}
+                                            </button>
+                                        </vdr-dropdown-menu>
+                                    </vdr-dropdown>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+                    <div class="pagination-wrapper">
+                        <vdr-items-per-page-controls
+                            [itemsPerPage]="itemsPerPage"
+                            (itemsPerPageChange)="itemsPerPage = $event"
+                        ></vdr-items-per-page-controls>
+                        <vdr-pagination-controls
+                            [currentPage]="currentPage"
+                            [itemsPerPage]="itemsPerPage"
+                            [totalItems]="filteredValues.length"
+                            (pageChange)="currentPage = $event"
+                        ></vdr-pagination-controls>
+                    </div>
+                </ng-container>
                 <div>
                     <button
                         type="button"

+ 10 - 0
packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.scss

@@ -2,3 +2,13 @@
 .visible-toggle {
     margin-top: -3px !important;
 }
+
+tr.facet-value td {
+    vertical-align: middle;
+}
+
+.pagination-wrapper {
+    display: flex;
+    justify-content: space-between;
+    padding: var(--card-padding);
+}

+ 95 - 50
packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.ts

@@ -1,7 +1,9 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
 import {
     FormBuilder,
-    UntypedFormArray,
+    FormControl,
+    FormGroup,
+    FormRecord,
     UntypedFormControl,
     UntypedFormGroup,
     Validators,
@@ -29,8 +31,8 @@ import {
 import { normalizeString } from '@vendure/common/lib/normalize-string';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { gql } from 'apollo-angular';
-import { combineLatest, EMPTY, forkJoin, Observable } from 'rxjs';
-import { map, mergeMap, switchMap, take } from 'rxjs/operators';
+import { BehaviorSubject, combineLatest, EMPTY, forkJoin, Observable } from 'rxjs';
+import { map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
 
 export const FACET_DETAIL_QUERY = gql`
     query GetFacetDetail($id: ID!) {
@@ -41,6 +43,8 @@ export const FACET_DETAIL_QUERY = gql`
     ${FACET_WITH_VALUES_FRAGMENT}
 `;
 
+type ValueItem = FacetWithValuesFragment['values'][number] | { id: string; name: string; code: string };
+
 @Component({
     selector: 'vdr-facet-detail',
     templateUrl: './facet-detail.component.html',
@@ -60,14 +64,20 @@ export class FacetDetailComponent
             visible: true,
             customFields: this.formBuilder.group(getCustomFieldsDefaults(this.customFields)),
         }),
-        values: this.formBuilder.array<{
-            id: string;
-            name: string;
-            code: string;
-            customFields: any;
-        }>([]),
+        values: this.formBuilder.record<
+            FormGroup<{
+                id: FormControl<string>;
+                name: FormControl<string>;
+                code: FormControl<string>;
+                customFields: FormGroup;
+            }>
+        >({}),
     });
-    values: Array<FacetWithValuesFragment['values'][number] | { name: string; code: string }> = [];
+    currentPage = 1;
+    itemsPerPage = 10;
+    filterControl = new FormControl('');
+    values$ = new BehaviorSubject<ValueItem[]>([]);
+    filteredValues$ = new Observable<ValueItem[]>();
     readonly updatePermission = [Permission.UpdateCatalog, Permission.UpdateFacet];
 
     constructor(
@@ -82,6 +92,24 @@ export class FacetDetailComponent
 
     ngOnInit() {
         this.init();
+        this.filteredValues$ = combineLatest([
+            this.values$,
+            this.filterControl.valueChanges.pipe(startWith('')),
+        ]).pipe(
+            map(([values, filterTerm]) => {
+                const filterString = filterTerm?.toLowerCase().trim();
+                return filterString
+                    ? values.filter(
+                          v =>
+                              v.name.toLowerCase().includes(filterString) ||
+                              v.code.toLowerCase().includes(filterString),
+                      )
+                    : values;
+            }),
+            tap(() => {
+                this.currentPage = 1;
+            }),
+        );
     }
 
     ngOnDestroy() {
@@ -97,9 +125,9 @@ export class FacetDetailComponent
         }
     }
 
-    updateValueCode(currentCode: string, nameValue: string, index: number) {
+    updateValueCode(currentCode: string, nameValue: string, valueId: string) {
         if (!currentCode) {
-            const codeControl = this.detailForm.get(['values', index, 'code']);
+            const codeControl = this.detailForm.get(['values', valueId, 'code']);
             if (codeControl && codeControl.pristine) {
                 codeControl.setValue(normalizeString(nameValue, '-'));
             }
@@ -110,20 +138,17 @@ export class FacetDetailComponent
         return !!this.detailForm.get(['values', index, 'customFields', name]);
     }
 
-    getValuesFormArray(): UntypedFormArray {
-        return this.detailForm.get('values') as UntypedFormArray;
-    }
-
     addFacetValue() {
-        const valuesFormArray = this.detailForm.get('values') as UntypedFormArray | null;
-        if (valuesFormArray) {
+        const valuesFormRecord = this.detailForm.get('values') as FormRecord;
+        if (valuesFormRecord) {
+            const id = this.createTempId();
             const valueGroup = this.formBuilder.group({
-                id: '',
+                id,
                 name: ['', Validators.required],
                 code: '',
                 customFields: this.formBuilder.group({}),
             });
-            const newValue: any = { name: '', code: '' };
+            const newValue: any = { id, name: '', code: '' };
             if (this.customValueFields.length) {
                 const customValueFieldsGroup = new UntypedFormGroup({});
                 newValue.customFields = {};
@@ -135,8 +160,11 @@ export class FacetDetailComponent
 
                 valueGroup.addControl('customFields', customValueFieldsGroup);
             }
-            valuesFormArray.insert(valuesFormArray.length, valueGroup);
-            this.values.push(newValue);
+            valuesFormRecord.addControl(id, valueGroup);
+            const values = this.values$.value;
+            const endOfPageIndex = this.currentPage * this.itemsPerPage - 1;
+            values.splice(endOfPageIndex, 0, newValue);
+            this.values$.next(values);
         }
     }
 
@@ -176,7 +204,9 @@ export class FacetDetailComponent
     }
 
     save() {
-        const valuesArray = this.detailForm.get('values') as (typeof this.detailForm)['controls']['values'];
+        const valuesFormRecord = this.detailForm.get(
+            'values',
+        ) as (typeof this.detailForm)['controls']['values'];
         combineLatest(this.entity$, this.languageCode$)
             .pipe(
                 take(1),
@@ -196,8 +226,12 @@ export class FacetDetailComponent
                             updateOperations.push(this.dataService.facet.updateFacet(newFacet));
                         }
                     }
-                    if (valuesArray && valuesArray.dirty) {
-                        const createdValues = this.getCreatedFacetValues(facet, valuesArray, languageCode);
+                    if (valuesFormRecord && valuesFormRecord.dirty) {
+                        const createdValues = this.getCreatedFacetValues(
+                            facet,
+                            valuesFormRecord,
+                            languageCode,
+                        );
                         if (createdValues.length) {
                             updateOperations.push(
                                 this.dataService.facet.createFacetValues(createdValues).pipe(
@@ -210,7 +244,11 @@ export class FacetDetailComponent
                                 ),
                             );
                         }
-                        const updatedValues = this.getUpdatedFacetValues(facet, valuesArray, languageCode);
+                        const updatedValues = this.getUpdatedFacetValues(
+                            facet,
+                            valuesFormRecord,
+                            languageCode,
+                        );
                         if (updatedValues.length) {
                             updateOperations.push(this.dataService.facet.updateFacetValues(updatedValues));
                         }
@@ -233,14 +271,15 @@ export class FacetDetailComponent
             );
     }
 
-    deleteFacetValue(facetValueId: string | undefined, index: number) {
-        if (!facetValueId) {
+    deleteFacetValue(facetValueId: string) {
+        if (this.isTempId(facetValueId)) {
             // deleting a newly-added (not persisted) FacetValue
-            const valuesFormArray = this.detailForm.get('values') as UntypedFormArray | null;
-            if (valuesFormArray) {
-                valuesFormArray.removeAt(index);
+            const valuesFormRecord = this.detailForm.get('values') as FormRecord;
+            if (valuesFormRecord) {
+                valuesFormRecord.removeControl(facetValueId);
             }
-            this.values.splice(index, 1);
+            const values = this.values$.value;
+            this.values$.next(values.filter(v => v.id !== facetValueId));
             return;
         }
         this.showModalAndDelete(facetValueId)
@@ -264,9 +303,9 @@ export class FacetDetailComponent
             )
             .subscribe(
                 () => {
-                    const valuesFormArray = this.detailForm.get('values') as UntypedFormArray | null;
-                    if (valuesFormArray) {
-                        valuesFormArray.removeAt(index);
+                    const valuesFormRecord = this.detailForm.get('values') as FormRecord;
+                    if (valuesFormRecord) {
+                        valuesFormRecord.removeControl(facetValueId);
                     }
                     this.notificationService.success(_('common.notify-delete-success'), {
                         entity: 'FacetValue',
@@ -313,7 +352,6 @@ export class FacetDetailComponent
         });
 
         if (this.customFields.length) {
-            const customFieldsGroup = this.detailForm.get(['facet', 'customFields']) as UntypedFormGroup;
             this.setCustomFieldFormValues(
                 this.customFields,
                 this.detailForm.get(['facet', 'customFields']),
@@ -322,8 +360,8 @@ export class FacetDetailComponent
             );
         }
 
-        const currentValuesFormArray = this.detailForm.get('values') as UntypedFormArray;
-        this.values = [...facet.values];
+        const currentValuesFormGroup = this.detailForm.get('values') as FormRecord;
+        this.values$.next([...facet.values]);
         facet.values.forEach(value => {
             const valueTranslation = findTranslation(value, languageCode);
             const group = {
@@ -331,16 +369,14 @@ export class FacetDetailComponent
                 code: value.code,
                 name: valueTranslation ? valueTranslation.name : '',
             };
-            let valueControl = currentValuesFormArray.controls.find(
-                control => control.value.id === value.id,
-            ) as UntypedFormGroup | undefined;
+            let valueControl = currentValuesFormGroup.get(value.id) as FormGroup;
             if (valueControl) {
                 valueControl.get('id')?.setValue(group.id);
                 valueControl.get('code')?.setValue(group.code);
                 valueControl.get('name')?.setValue(group.name);
             } else {
                 valueControl = this.formBuilder.group(group);
-                currentValuesFormArray.push(valueControl);
+                currentValuesFormGroup.addControl(value.id, valueControl);
             }
             if (this.customValueFields.length) {
                 let customValueFieldsGroup = valueControl.get(['customFields']) as
@@ -399,11 +435,11 @@ export class FacetDetailComponent
      */
     private getCreatedFacetValues(
         facet: FacetWithValuesFragment,
-        valuesFormArray: (typeof this.detailForm)['controls']['values'],
+        valuesFormRecord: (typeof this.detailForm)['controls']['values'],
         languageCode: LanguageCode,
     ): CreateFacetValueInput[] {
-        return valuesFormArray.controls
-            .filter(c => !c.value?.id)
+        return Object.values(valuesFormRecord.controls)
+            .filter(c => c.value.id && this.isTempId(c.value.id))
             .map(c => c.value)
             .map(value =>
                 createUpdatedTranslatable({
@@ -421,6 +457,7 @@ export class FacetDetailComponent
                 facetId: facet.id,
                 code: input.code ?? '',
                 ...input,
+                id: undefined,
             }));
     }
 
@@ -430,15 +467,15 @@ export class FacetDetailComponent
      */
     private getUpdatedFacetValues(
         facet: FacetWithValuesFragment,
-        valuesFormArray: UntypedFormArray,
+        valuesFormGroup: FormGroup,
         languageCode: LanguageCode,
     ): UpdateFacetValueInput[] {
-        const dirtyValues = facet.values.filter((v, i) => {
-            const formRow = valuesFormArray.get(i.toString());
+        const dirtyValues = facet.values.filter(v => {
+            const formRow = valuesFormGroup.get(v.id);
             return formRow && formRow.dirty && formRow.value.id;
         });
-        const dirtyValueValues = valuesFormArray.controls
-            .filter(c => c.dirty && c.value.id)
+        const dirtyValueValues = Object.values(valuesFormGroup.controls)
+            .filter(c => c.dirty && !this.isTempId(c.value.id))
             .map(c => c.value);
 
         if (dirtyValues.length !== dirtyValueValues.length) {
@@ -459,4 +496,12 @@ export class FacetDetailComponent
             )
             .filter(notNullOrUndefined);
     }
+
+    private createTempId() {
+        return `temp-${Math.random().toString(36).substr(2, 9)}`;
+    }
+
+    private isTempId(id: string) {
+        return id.startsWith('temp-');
+    }
 }