Skip to content

Commit

Permalink
fix(admin-ui): Enable selective loading of custom fields
Browse files Browse the repository at this point in the history
This commit introduces some new low-level APIs to the data layer
of the Admin UI. It allows us to control which custom fields
get dynamically added to fragments when making queries & mutations.

It also exposes a new method on the QueryResult class which allows
us to update & refetch the underlying DocumentNode whenever
the selected custom fields changes.

Relates to #3097
  • Loading branch information
michaelbromley committed Oct 4, 2024
1 parent b9953e1 commit 9d7744b
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 90 deletions.
52 changes: 46 additions & 6 deletions packages/admin-ui/src/lib/core/src/common/base-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import { DestroyRef, Directive, inject, OnDestroy, OnInit } from '@angular/core'
import { FormControl } from '@angular/forms';
import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router';
import { ResultOf, TypedDocumentNode, VariablesOf } from '@graphql-typed-document-node/core';
import { BehaviorSubject, combineLatest, merge, Observable, Subject } from 'rxjs';
import { BehaviorSubject, combineLatest, merge, Observable, Subject, switchMap } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, map, shareReplay, takeUntil, tap } from 'rxjs/operators';
import { DataService } from '../data/providers/data.service';

import { QueryResult } from '../data/query-result';
import { ServerConfigService } from '../data/server-config';
import { DataTableConfigService } from '../providers/data-table/data-table-config.service';
import { DataTableFilterCollection } from '../providers/data-table/data-table-filter-collection';
import { DataTableSortCollection } from '../providers/data-table/data-table-sort-collection';
import { PermissionsService } from '../providers/permissions/permissions.service';
import { DataTable2ColumnComponent } from '../shared/components/data-table-2/data-table-column.component';
import { DataTableCustomFieldColumnComponent } from '../shared/components/data-table-2/data-table-custom-field-column.component';
import { CustomFieldConfig, CustomFields, LanguageCode } from './generated-types';
import { SelectionManager } from './utilities/selection-manager';

Expand Down Expand Up @@ -58,11 +61,17 @@ export class BaseListComponent<ResultType, ItemType, VariableType extends Record
private listQueryFn: ListQueryFn<ResultType>;
private mappingFn: MappingFn<ItemType, ResultType>;
private onPageChangeFn: OnPageChangeFn<VariableType> = (skip, take) =>
({ options: { skip, take } } as any);
({ options: { skip, take } }) as any;
protected refresh$ = new BehaviorSubject<undefined>(undefined);
private defaults: { take: number; skip: number } = { take: 10, skip: 0 };
protected visibleCustomFieldColumnChange$ = new Subject<
Array<DataTableCustomFieldColumnComponent<any>>
>();

constructor(protected router: Router, protected route: ActivatedRoute) {}
constructor(
protected router: Router,
protected route: ActivatedRoute,
) {}

/**
* @description
Expand Down Expand Up @@ -96,7 +105,7 @@ export class BaseListComponent<ResultType, ItemType, VariableType extends Record
const fetchPage = ([currentPage, itemsPerPage, _]: [number, number, undefined]) => {
const take = itemsPerPage;
const skip = (currentPage - 1) * itemsPerPage;
this.listQuery.ref.refetch(this.onPageChangeFn(skip, take));
this.listQuery.ref?.refetch(this.onPageChangeFn(skip, take));
};

this.result$ = this.listQuery.stream$.pipe(shareReplay(1));
Expand Down Expand Up @@ -138,7 +147,7 @@ export class BaseListComponent<ResultType, ItemType, VariableType extends Record
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.listQuery.completed$.next();
this.listQuery.destroy();
}

/**
Expand All @@ -157,6 +166,15 @@ export class BaseListComponent<ResultType, ItemType, VariableType extends Record
this.setQueryParam('perPage', perPage, { replaceUrl: true });
}

setVisibleColumns(columns: Array<DataTable2ColumnComponent<any>>) {
this.visibleCustomFieldColumnChange$.next(
columns.filter(
(c): c is DataTableCustomFieldColumnComponent<any> =>
c instanceof DataTableCustomFieldColumnComponent,
),
);
}

/**
* @description
* Re-fetch the current page of results.
Expand Down Expand Up @@ -212,8 +230,17 @@ export class TypedBaseListComponent<
protected router = inject(Router);
protected serverConfigService = inject(ServerConfigService);
protected permissionsService = inject(PermissionsService);
protected dataTableConfigService = inject(DataTableConfigService);
/**
* This was introduced to allow us to more easily manage the relation between the
* DataTableComponent and the BaseListComponent. It allows the base class to
* correctly look up the currently-visible custom field columns, which can then
* be passed to the `dataService.query()` method.
*/
protected dataTableListId: string | undefined;
private refreshStreams: Array<Observable<any>> = [];
private collections: Array<DataTableFilterCollection | DataTableSortCollection<any>> = [];

constructor() {
super(inject(Router), inject(ActivatedRoute));

Expand All @@ -229,8 +256,21 @@ export class TypedBaseListComponent<
setVariables?: (skip: number, take: number) => VariablesOf<T>;
refreshListOnChanges?: Array<Observable<any>>;
}) {
const customFieldsChange$ = this.visibleCustomFieldColumnChange$.pipe(
map(columns => columns.map(c => c.customField.name)),
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
);
const includeCustomFields = this.dataTableListId
? this.dataTableConfigService.getConfig(this.dataTableListId).visibility
: undefined;
super.setQueryFn(
(args: any) => this.dataService.query(config.document).refetchOnChannelChange(),
(args: any) =>
this.dataService
.query(config.document, {} as any, 'cache-and-network', {
includeCustomFields,
})
.refetchOnChannelChange()
.refetchOnCustomFieldsChange(customFieldsChange$),
data => config.getItems(data),
(skip, take) => config.setVariables?.(skip, take) ?? ({} as any),
);
Expand Down
9 changes: 9 additions & 0 deletions packages/admin-ui/src/lib/core/src/data/data.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import createUploadLink from 'apollo-upload-client/createUploadLink.mjs';
import { getAppConfig } from '../app.config';
import { introspectionResult } from '../common/introspection-result-wrapper';
import { LocalStorageService } from '../providers/local-storage/local-storage.service';
import { AddCustomFieldsLink } from './add-custom-fields-link';

import { CheckJobsLink } from './check-jobs-link';
import { getClientDefaults } from './client-state/client-defaults';
Expand Down Expand Up @@ -46,6 +47,13 @@ export function createApollo(
},
},
},
Query: {
fields: {
products: {
merge: (existing, incoming) => incoming,
},
},
},
},
});
apolloCache.writeQuery({
Expand All @@ -61,6 +69,7 @@ export function createApollo(
return {
link: ApolloLink.from([
new OmitTypenameLink(),
// new AddCustomFieldsLink(injector.get(ServerConfigService)),
new CheckJobsLink(injector),
setContext(() => {
const headers: Record<string, string> = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,27 @@ import { CustomFieldConfig } from '../../common/generated-types';
import { QueryResult } from '../query-result';
import { ServerConfigService } from '../server-config';
import { addCustomFields } from '../utils/add-custom-fields';
import { isEntityCreateOrUpdateMutation } from '../utils/is-entity-create-or-update-mutation';
import { removeReadonlyCustomFields } from '../utils/remove-readonly-custom-fields';
import { transformRelationCustomFieldInputs } from '../utils/transform-relation-custom-field-inputs';
import { isEntityCreateOrUpdateMutation } from '../utils/is-entity-create-or-update-mutation';

/**
* @description
* Additional options that can be passed to the `query` and `mutate` methods.
*
* @since 3.0.4
*/
export interface ExtendedQueryOptions {
/**
* @description
* An array of custom field names which should be included in the query or mutation
* return data. The automatic inclusion of custom fields is only supported for
* entities which are defined as Fragments in the DocumentNode.
*
* @since 3.0.4
*/
includeCustomFields?: string[];
}

@Injectable()
export class BaseDataService {
Expand All @@ -33,14 +51,15 @@ export class BaseDataService {
query: DocumentNode | TypedDocumentNode<T, V>,
variables?: V,
fetchPolicy: WatchQueryFetchPolicy = 'cache-and-network',
options: ExtendedQueryOptions = {},
): QueryResult<T, V> {
const withCustomFields = addCustomFields(query, this.customFields);
const queryRef = this.apollo.watchQuery<T, V>({
query: withCustomFields,
query: addCustomFields(query, this.customFields, options.includeCustomFields),
variables,
fetchPolicy,
});
const queryResult = new QueryResult<T, any>(queryRef, this.apollo);

const queryResult = new QueryResult<T, V>(queryRef, this.apollo, this.customFields);
return queryResult;
}

Expand All @@ -51,8 +70,9 @@ export class BaseDataService {
mutation: DocumentNode | TypedDocumentNode<T, V>,
variables?: V,
update?: MutationUpdaterFn<T>,
options: ExtendedQueryOptions = {},
): Observable<T> {
const withCustomFields = addCustomFields(mutation, this.customFields);
const withCustomFields = addCustomFields(mutation, this.customFields, options.includeCustomFields);
const withoutReadonlyFields = this.prepareCustomFields(mutation, variables);

return this.apollo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { QueryResult } from '../query-result';

import { AdministratorDataService } from './administrator-data.service';
import { AuthDataService } from './auth-data.service';
import { BaseDataService } from './base-data.service';
import { BaseDataService, ExtendedQueryOptions } from './base-data.service';
import { ClientDataService } from './client-data.service';
import { CollectionDataService } from './collection-data.service';
import { CustomerDataService } from './customer-data.service';
Expand Down Expand Up @@ -82,8 +82,9 @@ export class DataService {
query: DocumentNode | TypedDocumentNode<T, V>,
variables?: V,
fetchPolicy: WatchQueryFetchPolicy = 'cache-and-network',
options: ExtendedQueryOptions = {},
): QueryResult<T, V> {
return this.baseDataService.query(query, variables, fetchPolicy);
return this.baseDataService.query(query, variables, fetchPolicy, options);
}

/**
Expand All @@ -107,7 +108,8 @@ export class DataService {
mutation: DocumentNode | TypedDocumentNode<T, V>,
variables?: V,
update?: MutationUpdaterFn<T>,
options: ExtendedQueryOptions = {},
): Observable<T> {
return this.baseDataService.mutate(mutation, variables, update);
return this.baseDataService.mutate(mutation, variables, update, options);
}
}
Loading

0 comments on commit 9d7744b

Please sign in to comment.