diff --git a/packages/admin-ui/src/lib/core/src/common/base-list.component.ts b/packages/admin-ui/src/lib/core/src/common/base-list.component.ts index 3934090bf8..995bbdcff1 100644 --- a/packages/admin-ui/src/lib/core/src/common/base-list.component.ts +++ b/packages/admin-ui/src/lib/core/src/common/base-list.component.ts @@ -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'; @@ -58,11 +61,17 @@ export class BaseListComponent; private mappingFn: MappingFn; private onPageChangeFn: OnPageChangeFn = (skip, take) => - ({ options: { skip, take } } as any); + ({ options: { skip, take } }) as any; protected refresh$ = new BehaviorSubject(undefined); private defaults: { take: number; skip: number } = { take: 10, skip: 0 }; + protected visibleCustomFieldColumnChange$ = new Subject< + Array> + >(); - constructor(protected router: Router, protected route: ActivatedRoute) {} + constructor( + protected router: Router, + protected route: ActivatedRoute, + ) {} /** * @description @@ -96,7 +105,7 @@ export class BaseListComponent { 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)); @@ -138,7 +147,7 @@ export class BaseListComponent>) { + this.visibleCustomFieldColumnChange$.next( + columns.filter( + (c): c is DataTableCustomFieldColumnComponent => + c instanceof DataTableCustomFieldColumnComponent, + ), + ); + } + /** * @description * Re-fetch the current page of results. @@ -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> = []; private collections: Array> = []; + constructor() { super(inject(Router), inject(ActivatedRoute)); @@ -229,8 +256,21 @@ export class TypedBaseListComponent< setVariables?: (skip: number, take: number) => VariablesOf; refreshListOnChanges?: Array>; }) { + 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), ); diff --git a/packages/admin-ui/src/lib/core/src/data/data.module.ts b/packages/admin-ui/src/lib/core/src/data/data.module.ts index 24d56e4f68..d77654c020 100644 --- a/packages/admin-ui/src/lib/core/src/data/data.module.ts +++ b/packages/admin-ui/src/lib/core/src/data/data.module.ts @@ -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'; @@ -46,6 +47,13 @@ export function createApollo( }, }, }, + Query: { + fields: { + products: { + merge: (existing, incoming) => incoming, + }, + }, + }, }, }); apolloCache.writeQuery({ @@ -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 = {}; diff --git a/packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts b/packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts index b9df060dde..93ba33c323 100644 --- a/packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts +++ b/packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts @@ -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 { @@ -33,14 +51,15 @@ export class BaseDataService { query: DocumentNode | TypedDocumentNode, variables?: V, fetchPolicy: WatchQueryFetchPolicy = 'cache-and-network', + options: ExtendedQueryOptions = {}, ): QueryResult { - const withCustomFields = addCustomFields(query, this.customFields); const queryRef = this.apollo.watchQuery({ - query: withCustomFields, + query: addCustomFields(query, this.customFields, options.includeCustomFields), variables, fetchPolicy, }); - const queryResult = new QueryResult(queryRef, this.apollo); + + const queryResult = new QueryResult(queryRef, this.apollo, this.customFields); return queryResult; } @@ -51,8 +70,9 @@ export class BaseDataService { mutation: DocumentNode | TypedDocumentNode, variables?: V, update?: MutationUpdaterFn, + options: ExtendedQueryOptions = {}, ): Observable { - const withCustomFields = addCustomFields(mutation, this.customFields); + const withCustomFields = addCustomFields(mutation, this.customFields, options.includeCustomFields); const withoutReadonlyFields = this.prepareCustomFields(mutation, variables); return this.apollo diff --git a/packages/admin-ui/src/lib/core/src/data/providers/data.service.ts b/packages/admin-ui/src/lib/core/src/data/providers/data.service.ts index a26e4815d2..cb1c965c35 100644 --- a/packages/admin-ui/src/lib/core/src/data/providers/data.service.ts +++ b/packages/admin-ui/src/lib/core/src/data/providers/data.service.ts @@ -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'; @@ -82,8 +82,9 @@ export class DataService { query: DocumentNode | TypedDocumentNode, variables?: V, fetchPolicy: WatchQueryFetchPolicy = 'cache-and-network', + options: ExtendedQueryOptions = {}, ): QueryResult { - return this.baseDataService.query(query, variables, fetchPolicy); + return this.baseDataService.query(query, variables, fetchPolicy, options); } /** @@ -107,7 +108,8 @@ export class DataService { mutation: DocumentNode | TypedDocumentNode, variables?: V, update?: MutationUpdaterFn, + options: ExtendedQueryOptions = {}, ): Observable { - return this.baseDataService.mutate(mutation, variables, update); + return this.baseDataService.mutate(mutation, variables, update, options); } } diff --git a/packages/admin-ui/src/lib/core/src/data/query-result.ts b/packages/admin-ui/src/lib/core/src/data/query-result.ts index b87cf1caa3..8edd3ec44e 100644 --- a/packages/admin-ui/src/lib/core/src/data/query-result.ts +++ b/packages/admin-ui/src/lib/core/src/data/query-result.ts @@ -1,12 +1,14 @@ import { ApolloQueryResult, NetworkStatus } from '@apollo/client/core'; import { notNullOrUndefined } from '@vendure/common/lib/shared-utils'; import { Apollo, QueryRef } from 'apollo-angular'; -import { merge, Observable, Subject } from 'rxjs'; -import { distinctUntilChanged, filter, finalize, map, skip, take, takeUntil, tap } from 'rxjs/operators'; +import { DocumentNode } from 'graphql/index'; +import { merge, Observable, Subject, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter, finalize, map, skip, take, takeUntil } from 'rxjs/operators'; -import { GetUserStatusQuery } from '../common/generated-types'; +import { CustomFieldConfig, GetUserStatusQuery } from '../common/generated-types'; import { GET_USER_STATUS } from './definitions/client-definitions'; +import { addCustomFields } from './utils/add-custom-fields'; /** * @description @@ -17,12 +19,42 @@ import { GET_USER_STATUS } from './definitions/client-definitions'; * @docsPage DataService */ export class QueryResult = Record> { - constructor(private queryRef: QueryRef, private apollo: Apollo) { - this.valueChanges = queryRef.valueChanges; + constructor( + private queryRef: QueryRef, + private apollo: Apollo, + private customFieldMap: Map, + ) { + this.lastQuery = queryRef.options.query; } - completed$ = new Subject(); - private valueChanges: Observable>; + /** + * Causes any subscriptions to the QueryRef to complete, via the use + * of the `takeUntil` operator. + */ + private completed$ = new Subject(); + /** + * The subscription to the current QueryRef.valueChanges Observable. + * This is stored so that it can be unsubscribed from when the QueryRef + * changes. + */ + private valueChangesSubscription: Subscription; + /** + * This Subject is used to emit new values from the QueryRef.valueChanges Observable. + * We use this rather than directly subscribing to the QueryRef.valueChanges Observable + * so that we are able to change the QueryRef and re-subscribe when necessary. + */ + private valueChangeSubject = new Subject>(); + /** + * We keep track of the QueryRefs which have been subscribed to so that we can avoid + * re-subscribing to the same QueryRef multiple times. + */ + private queryRefSubscribed = new WeakMap, boolean>(); + /** + * We store a reference to the last query so that we can compare it with the next query + * and avoid re-fetching the same query multiple times. This is applicable to the code + * paths that actually change the query, i.e. refetchOnCustomFieldsChange(). + */ + private lastQuery: DocumentNode; /** * @description @@ -47,17 +79,44 @@ export class QueryResult = Record> takeUntil(this.completed$), ); - this.valueChanges = merge(activeChannelId$, this.queryRef.valueChanges).pipe( - tap(val => { + merge(activeChannelId$, this.valueChangeSubject) + .pipe(takeUntil(loggedOut$), takeUntil(this.completed$)) + .subscribe(val => { if (typeof val === 'string') { new Promise(resolve => setTimeout(resolve, 50)).then(() => this.queryRef.refetch()); } - }), - filter(val => typeof val !== 'string'), - takeUntil(loggedOut$), - takeUntil(this.completed$), - ); - this.queryRef.valueChanges = this.valueChanges; + }); + return this; + } + + /** + * @description + * Re-fetch this query whenever the custom fields change, updating the query to include the + * specified custom fields. + * + * @since 3.0.4 + */ + refetchOnCustomFieldsChange(customFieldsToInclude$: Observable): QueryResult { + customFieldsToInclude$ + .pipe( + filter(customFields => { + const newQuery = addCustomFields(this.lastQuery, this.customFieldMap, customFields); + const hasChanged = JSON.stringify(newQuery) !== JSON.stringify(this.lastQuery); + return hasChanged; + }), + takeUntil(this.completed$), + ) + .subscribe(customFields => { + const newQuery = addCustomFields(this.lastQuery, this.customFieldMap, customFields); + this.lastQuery = newQuery; + const queryRef = this.apollo.watchQuery({ + query: newQuery, + variables: this.queryRef.variables, + fetchPolicy: this.queryRef.options.fetchPolicy, + }); + this.queryRef = queryRef; + this.subscribeToQueryRef(queryRef); + }); return this; } @@ -66,7 +125,7 @@ export class QueryResult = Record> * Returns an Observable which emits a single result and then completes. */ get single$(): Observable { - return this.valueChanges.pipe( + return this.currentQueryRefValueChanges.pipe( filter(result => result.networkStatus === NetworkStatus.ready), take(1), map(result => result.data), @@ -82,7 +141,7 @@ export class QueryResult = Record> * Returns an Observable which emits until unsubscribed. */ get stream$(): Observable { - return this.valueChanges.pipe( + return this.currentQueryRefValueChanges.pipe( filter(result => result.networkStatus === NetworkStatus.ready), map(result => result.data), finalize(() => { @@ -111,4 +170,48 @@ export class QueryResult = Record> mapStream(mapFn: (item: T) => R): Observable { return this.stream$.pipe(map(mapFn)); } + + /** + * @description + * Signals to the internal Observable subscriptions that they should complete. + */ + destroy() { + this.completed$.next(); + this.completed$.complete(); + } + + /** + * @description + * Returns an Observable which emits the current value of the QueryRef.valueChanges Observable. + * + * We wrap the valueChanges Observable in a new Observable so that we can have a lazy + * evaluation of the valueChanges Observable. That is, we only fire the HTTP request when + * the returned Observable is subscribed to. + */ + private get currentQueryRefValueChanges(): Observable> { + return new Observable(subscriber => { + if (!this.queryRefSubscribed.get(this.queryRef)) { + this.subscribeToQueryRef(this.queryRef); + this.queryRefSubscribed.set(this.queryRef, true); + } + this.valueChangeSubject.subscribe(subscriber); + return () => { + this.queryRefSubscribed.delete(this.queryRef); + }; + }); + } + + /** + * @description + * Subscribes to the valueChanges Observable of the given QueryRef, and stores the subscription + * so that it can be unsubscribed from when the QueryRef changes. + */ + private subscribeToQueryRef(queryRef: QueryRef) { + if (this.valueChangesSubscription) { + this.valueChangesSubscription.unsubscribe(); + } + this.valueChangesSubscription = queryRef.valueChanges + .pipe(takeUntil(this.completed$)) + .subscribe(this.valueChangeSubject); + } } diff --git a/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.spec.ts b/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.spec.ts index 675713c1d4..b9b8f7777e 100644 --- a/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.spec.ts +++ b/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.spec.ts @@ -186,6 +186,19 @@ describe('addCustomFields()', () => { expect((customFieldsDef.selectionSet!.selections[0] as FieldNode).name.value).toBe('custom'); } + it('Does not duplicate customFields selection set', () => { + const customFieldsConfig = new Map(); + customFieldsConfig.set('Product', [{ name: 'custom', type: 'boolean', list: false }]); + const result1 = addCustomFields(documentNode, customFieldsConfig); + const result2 = addCustomFields(result1, customFieldsConfig); + + const fragmentDef = result2.definitions[1] as FragmentDefinitionNode; + const customFieldSelections = fragmentDef.selectionSet.selections.filter( + s => s.kind === Kind.FIELD && s.name.value === 'customFields', + ); + expect(customFieldSelections.length).toBe(1); + }); + it('Adds customFields to ProductVariant fragment', () => { addsCustomFieldsToType('ProductVariant', 2); }); diff --git a/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.ts b/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.ts index 8f3a25ceb3..907369b9d4 100644 --- a/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.ts +++ b/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.ts @@ -21,8 +21,10 @@ import { export function addCustomFields( documentNode: DocumentNode, customFields: Map, + includeCustomFields?: string[], ): DocumentNode { - const fragmentDefs = documentNode.definitions.filter(isFragmentDefinition); + const clone = JSON.parse(JSON.stringify(documentNode)) as DocumentNode; + const fragmentDefs = clone.definitions.filter(isFragmentDefinition); for (const fragmentDef of fragmentDefs) { let entityType = fragmentDef.typeCondition.name.value as keyof Pick< @@ -43,41 +45,59 @@ export function addCustomFields( const customFieldsForType = customFields.get(entityType); if (customFieldsForType && customFieldsForType.length) { - (fragmentDef.selectionSet.selections as SelectionNode[]).push({ - name: { - kind: Kind.NAME, - value: 'customFields', - }, - kind: Kind.FIELD, - selectionSet: { + // Check if there is already a customFields field in the fragment + // to avoid duplication + const existingCustomFieldsField = fragmentDef.selectionSet.selections.find( + selection => isFieldNode(selection) && selection.name.value === 'customFields', + ) as FieldNode | undefined; + const selectionNodes: SelectionNode[] = customFieldsForType + .filter(field => !includeCustomFields || includeCustomFields.includes(field.name)) + .map( + customField => + ({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: customField.name, + }, + // For "relation" custom fields, we need to also select + // all the scalar fields of the related type + ...(customField.type === 'relation' + ? { + selectionSet: { + kind: Kind.SELECTION_SET, + selections: ( + customField as RelationCustomFieldFragment + ).scalarFields.map(f => ({ + kind: Kind.FIELD, + name: { kind: Kind.NAME, value: f }, + })), + }, + } + : {}), + }) as FieldNode, + ); + if (!existingCustomFieldsField) { + // If no customFields field exists, add one + (fragmentDef.selectionSet.selections as SelectionNode[]).push({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'customFields', + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: selectionNodes, + }, + }); + } else { + // If a customFields field already exists, add the custom fields + // to the existing selection set + (existingCustomFieldsField.selectionSet as any) = { kind: Kind.SELECTION_SET, - selections: customFieldsForType.map( - customField => - ({ - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: customField.name, - }, - // For "relation" custom fields, we need to also select - // all the scalar fields of the related type - ...(customField.type === 'relation' - ? { - selectionSet: { - kind: Kind.SELECTION_SET, - selections: ( - customField as RelationCustomFieldFragment - ).scalarFields.map(f => ({ - kind: Kind.FIELD, - name: { kind: Kind.NAME, value: f }, - })), - }, - } - : {}), - } as FieldNode), - ), - }, - }); + selections: selectionNodes, + }; + } const localizedFields = customFieldsForType.filter( field => field.type === 'localeString' || field.type === 'localeText', @@ -104,7 +124,7 @@ export function addCustomFields( kind: Kind.NAME, value: customField.name, }, - } as FieldNode), + }) as FieldNode, ), }, }); @@ -112,7 +132,7 @@ export function addCustomFields( } } - return documentNode; + return clone; } function isFragmentDefinition(value: DefinitionNode): value is FragmentDefinitionNode { diff --git a/packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.html b/packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.html index 3eb68823d8..f745e0fd5e 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.html +++ b/packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-field-column.component.html @@ -1,10 +1,10 @@ - - + implements AfterContentInit, OnChanges, OnDe route = inject(ActivatedRoute); filterPresetService = inject(FilterPresetService); dataTableCustomComponentService = inject(DataTableCustomComponentService); - dataTableConfigService = inject(DataTableConfigService); protected customComponents = new Map(); rowTemplate: TemplateRef; @@ -146,6 +145,7 @@ export class DataTable2Component implements AfterContentInit, OnChanges, OnDe constructor( protected changeDetectorRef: ChangeDetectorRef, + protected localStorageService: LocalStorageService, protected dataService: DataService, ) { this.uiLanguage$ = this.dataService.client @@ -167,8 +167,8 @@ export class DataTable2Component implements AfterContentInit, OnChanges, OnDe get sortedColumns() { const columns = this.allColumns; - const dataTableConfig = this.dataTableConfigService.getConfig(this.id); - for (const [id, index] of Object.entries(dataTableConfig.order)) { + const dataTableConfig = this.getDataTableConfig(); + for (const [id, index] of Object.entries(dataTableConfig[this.id].order)) { const column = columns.find(c => c.id === id); const currentIndex = columns.findIndex(c => c.id === id); if (currentIndex !== -1 && column) { @@ -212,21 +212,21 @@ export class DataTable2Component implements AfterContentInit, OnChanges, OnDe ngAfterContentInit(): void { this.rowTemplate = this.templateRefs.last; - const dataTableConfig = this.dataTableConfigService.getConfig(this.id); + const dataTableConfig = this.getDataTableConfig(); if (!this.id) { console.warn(`No id was assigned to the data table component`); } const updateColumnVisibility = () => { - dataTableConfig.visibility = this.allColumns + dataTableConfig[this.id].visibility = this.allColumns .filter(c => (c.visible && c.hiddenByDefault) || (!c.visible && !c.hiddenByDefault)) .map(c => c.id); - this.dataTableConfigService.setConfig(this.id, dataTableConfig); + this.localStorageService.set('dataTableConfig', dataTableConfig); this.visibleColumnsChange.emit(this.visibleSortedColumns); }; this.allColumns.forEach(column => { - if (dataTableConfig?.visibility.includes(column.id)) { + if (dataTableConfig?.[this.id]?.visibility.includes(column.id)) { column.setVisibility(column.hiddenByDefault); } column.onColumnChange(updateColumnVisibility); @@ -250,7 +250,8 @@ export class DataTable2Component implements AfterContentInit, OnChanges, OnDe this.selectionManager.setCurrentItems(this.items); } this.showSearchFilterRow = - !!this.filters?.activeFilters.length || (dataTableConfig?.showSearchFilterRow ?? false); + !!this.filters?.activeFilters.length || + (dataTableConfig?.[this.id]?.showSearchFilterRow ?? false); this.columns.changes.subscribe(() => { this.changeDetectorRef.markForCheck(); }); @@ -272,27 +273,27 @@ export class DataTable2Component implements AfterContentInit, OnChanges, OnDe onColumnReorder(event: { column: DataTable2ColumnComponent; newIndex: number }) { const naturalIndex = this.allColumns.findIndex(c => c.id === event.column.id); - const dataTableConfig = this.dataTableConfigService.getConfig(this.id); + const dataTableConfig = this.getDataTableConfig(); if (naturalIndex === event.newIndex) { - delete dataTableConfig.order[event.column.id]; + delete dataTableConfig[this.id].order[event.column.id]; } else { - dataTableConfig.order[event.column.id] = event.newIndex; + dataTableConfig[this.id].order[event.column.id] = event.newIndex; } - this.dataTableConfigService.setConfig(this.id, dataTableConfig); + this.localStorageService.set('dataTableConfig', dataTableConfig); } onColumnsReset() { - const dataTableConfig = this.dataTableConfigService.getConfig(this.id); - dataTableConfig.order = {}; - dataTableConfig.visibility = []; - this.dataTableConfigService.setConfig(this.id, dataTableConfig); + const dataTableConfig = this.getDataTableConfig(); + dataTableConfig[this.id].order = {}; + dataTableConfig[this.id].visibility = []; + this.localStorageService.set('dataTableConfig', dataTableConfig); } toggleSearchFilterRow() { this.showSearchFilterRow = !this.showSearchFilterRow; - const dataTableConfig = this.dataTableConfigService.getConfig(this.id); - dataTableConfig.showSearchFilterRow = this.showSearchFilterRow; - this.dataTableConfigService.setConfig(this.id, dataTableConfig); + const dataTableConfig = this.getDataTableConfig(); + dataTableConfig[this.id].showSearchFilterRow = this.showSearchFilterRow; + this.localStorageService.set('dataTableConfig', dataTableConfig); } trackByFn(index: number, item: any) { @@ -310,4 +311,17 @@ export class DataTable2Component implements AfterContentInit, OnChanges, OnDe onRowClick(item: T, event: MouseEvent) { this.selectionManager?.toggleSelection(item, event); } + + protected getDataTableConfig(): DataTableConfig { + const dataTableConfig = this.localStorageService.get('dataTableConfig') ?? {}; + if (!dataTableConfig[this.id]) { + dataTableConfig[this.id] = { + visibility: [], + order: {}, + showSearchFilterRow: false, + filterPresets: [], + }; + } + return dataTableConfig; + } }