Skip to content

Commit

Permalink
fix(queryable): generic config
Browse files Browse the repository at this point in the history
  • Loading branch information
JBBianchi committed Jan 10, 2024
1 parent de78675 commit 7b431da
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Injectable, Injector, inject } from '@angular/core';
import { ODATA_DATA_SOURCE_ENDPOINT, ODataDataSource } from '@neuroglia/angular-data-source-odata';
import {
ColumnDefinition,
IQueryableTableStore,
QueryableTableState,
QueryableTableStore,
QueryableTableConfig,
} from '@neuroglia/angular-ngrx-component-store-queryable-table';
import { validateAuthorizations } from '@neuroglia/authorization-rule';
import { Observable, of } from 'rxjs';
import { filter, map, switchMap, takeUntil } from 'rxjs/operators';
import { ODataPrimitiveTypeEnum } from './models';
Expand All @@ -14,9 +15,13 @@ import { ODataMetadataService } from './odata-metadata.service';

@Injectable()
export class ODataTableStore<
TState extends QueryableTableState<TData> = QueryableTableState<any>,
TData = any,
> extends QueryableTableStore<TState, TData> {
TState extends QueryableTableState<TData> = QueryableTableState<any>,
TData = any,
TConfig extends QueryableTableConfig = QueryableTableConfig,
>
extends QueryableTableStore<TState, TData>
implements IQueryableTableStore<TState, TData>
{
/** Holds the datasource instance */
protected dataSource: ODataDataSource<TData> | null;

Expand All @@ -28,43 +33,40 @@ export class ODataTableStore<
}

/** @inheritdoc */
protected getServiceEndpoint(initialState: Partial<TState>): string {
protected getServiceDataEnpoint(config: TConfig): string {
return (
initialState.dataUrl ||
`${initialState.serviceUrl}${!initialState.serviceUrl!.endsWith('/') ? '/' : ''}${initialState.target}${
initialState.query || ''
}`
config.dataUrl ||
`${config.serviceUrl}${!config.serviceUrl!.endsWith('/') ? '/' : ''}${config.target}${config.query || ''}`
);
}

/** @inheritdoc */
protected getColumnDefinitions(initialState: Partial<TState>): Observable<ColumnDefinition[]> {
return !initialState.useMetadata
? of(initialState.columnDefinitions).pipe(
protected getColumnDefinitions(config: TConfig): Observable<ColumnDefinition[]> {
return !config.useMetadata
? of(config.columnDefinitions).pipe(
filter((definitions: ColumnDefinition[] | undefined) => !!definitions?.length),
map((definitions: ColumnDefinition[] | undefined) => definitions as ColumnDefinition[]),
)
: this.odataMetadataService.getMetadata(initialState.serviceUrl!).pipe(
: this.odataMetadataService.getMetadata(config.serviceUrl!).pipe(
takeUntil(this.destroy$),
switchMap((_: ODataMetadataSchema.Metadata) =>
!initialState.targetType
? this.odataMetadataService.getColumnDefinitions(initialState.target!)
: this.odataMetadataService.getColumnDefinitionsForQualifiedName(initialState.targetType),
!config.targetType
? this.odataMetadataService.getColumnDefinitions(config.target!)
: this.odataMetadataService.getColumnDefinitionsForQualifiedName(config.targetType),
),
map((definitions: ColumnDefinition[]) => {
const token = this.keycloak?.getKeycloakInstance()?.tokenParsed;
const stateDefinitionNames = (initialState.columnDefinitions || []).map((def) => def.name);
const stateDefinitionNames = (config.columnDefinitions || []).map((def) => def.name);
const columnDefinitions = [
...definitions.filter((def) => !stateDefinitionNames.includes(def.name)),
...(initialState.columnDefinitions || []).map((stateDef) => {
...(config.columnDefinitions || []).map((stateDef) => {
const def = definitions.find((def) => def.name === stateDef.name);
if (!def) {
return stateDef;
}
const columnDefinition = { ...def, ...stateDef };
return columnDefinition;
}),
].filter((def) => !def.authorizations || (token && validateAuthorizations(token, def.authorizations)));
];
return columnDefinitions as ColumnDefinition[];
}),
);
Expand All @@ -76,13 +78,13 @@ export class ODataTableStore<
}

/** @inheritdoc */
protected injectDataSource(): ODataDataSource<TData> {
protected injectDataSource(config: TConfig): Observable<ODataDataSource<TData>> {
const dataUrl = this.get((state) => state.dataUrl);
const dataSourceInjector = Injector.create({
name: 'DataSourceInjector',
parent: this.injector,
providers: [ODataDataSource, { provide: ODATA_DATA_SOURCE_ENDPOINT, useValue: dataUrl }],
});
return dataSourceInjector.get(ODataDataSource) as ODataDataSource<TData>;
return of(dataSourceInjector.get(ODataDataSource) as ODataDataSource<TData>);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { QueryableTableState } from './queryable-table.state';
import { ColumnDefinition } from './column-definition';
import { Paging, Sort } from '@neuroglia/angular-data-source-queryable';
import { Filters } from './filters';
import { QueryableTableConfig } from './queryable-table-config';

export interface IQueryableTableStore<
TState extends QueryableTableState<TData> = QueryableTableState<any>,
TData = any,
TConfig extends QueryableTableConfig = QueryableTableConfig,
> {
/** Selects the data */
data$: Observable<TData[]>;
Expand Down Expand Up @@ -58,7 +60,7 @@ export interface IQueryableTableStore<
* Initializes the state and its underlying datasource
* @param initialState A partial state with at least the service URL, the entity name and the column definitions
*/
init(initialState: Partial<TState>): Observable<TState>;
init(config: TConfig): Observable<TState>;
/**
* Clears persisted data
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Sort } from '@neuroglia/angular-data-source-queryable';
import { ColumnDefinition } from './column-definition';
import { Filters } from './filters';
import { SerializedFilter } from './serialized-filter';
import { QueryableTableConfig } from './queryable-table-config';

/**
* Represents the state of an queryable table
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import { ComponentStore } from '@ngrx/component-store';
import { KeycloakService } from 'keycloak-angular';
import { Filter as ODataQueryFilter } from 'odata-query';
import { Observable } from 'rxjs';
import { map, take, takeUntil, tap } from 'rxjs/operators';
import { map, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import {
ColumnDefinition,
Filter,
Filters,
IQueryableTableStore,
QueryableTableConfig,
QueryableTableState,
SerializedFilter,
expandRowColumnDefinition,
selectRowColumnDefinition,
} from './models';
import { persistenceKeys } from './persistence-keys';
import { QueryableTableTemplateProvider } from './queryable-table-template-provider.service';
import { validateAuthorizations } from '@neuroglia/authorization-rule';

/** Creates a default empty state */
export function createEmptyQueryableTableState<T>(): QueryableTableState<T> {
Expand Down Expand Up @@ -73,6 +75,7 @@ function asODataQueryFilter(filters: Filters): ODataQueryFilter {
export abstract class QueryableTableStore<
TState extends QueryableTableState<TData> = QueryableTableState<any>,
TData = any,
TConfig extends QueryableTableConfig = QueryableTableConfig,
>
extends ComponentStore<TState>
implements IQueryableTableStore<TState, TData>
Expand Down Expand Up @@ -153,16 +156,16 @@ export abstract class QueryableTableStore<
}

/**
* Builts the service endpoint URL based on the provided state
* Builds the data endpoint URL based on the provided state
* @param state
*/
protected abstract getServiceEndpoint(initialState: Partial<TState>): string;
protected abstract getServiceDataEnpoint(config: TConfig): string;

/**
* Gets the @see {@link ColumnDefinition}s for the provided state
* @param initialState
*/
protected abstract getColumnDefinitions(initialState: Partial<TState>): Observable<ColumnDefinition[]>;
protected abstract getColumnDefinitions(config: TConfig): Observable<ColumnDefinition[]>;

/**
* Returns a the string type for the current datasource, e.g. 'Edm.String' for OData
Expand All @@ -172,76 +175,90 @@ export abstract class QueryableTableStore<
/**
* Instanciates and returns the store's data source
*/
protected abstract injectDataSource(): QueryableDataSource<TData>;
protected abstract injectDataSource(config: TConfig): Observable<QueryableDataSource<TData>>;

/**
* Initializes the state and its underlying datasource
* @param initialState A partial state with at least the service URL, the entity name and the column definitions
* @param config A partial state with at least the service URL, the entity name and the column definitions
*/
init(initialState: Partial<TState>): Observable<TState> {
init(config: TConfig): Observable<TState> {
if (this.dataSource) {
throw 'The store has already been initialized for this component';
}
if (!initialState.dataSourceType) {
if (!config.dataSourceType) {
throw 'Unable to initialize, missing service data source type';
}
if (!initialState.serviceUrl) {
if (!config.serviceUrl) {
throw 'Unable to initialize, missing service URL';
}
if (!initialState.target) {
if (!config.target) {
throw 'Unable to initialize, missing entity name';
}
if (!initialState.useMetadata && !initialState.columnDefinitions) {
if (!config.useMetadata && !config.columnDefinitions) {
throw 'Unable to initialize, missing columns definitions';
}
const dataUrl = this.getServiceEndpoint(initialState);
const {
dataSourceType,
serviceUrl,
target,
useMetadata,
expand,
select,
enableSelection,
enableRowExpansion,
query,
} = config;
const dataUrl = this.getServiceDataEnpoint(config);
this.columnSettingsStorage = new StorageHandler<ColumnDefinition[]>(
persistenceKeys.columnSettings + '::' + dataUrl,
);
this.filtersStorage = new StorageHandler(persistenceKeys.filters + '::' + dataUrl, null, window.sessionStorage);
this.sortStorage = new StorageHandler(persistenceKeys.sort + '::' + dataUrl, null, window.sessionStorage);
this.pageSizeStorage = new StorageHandler(persistenceKeys.pageSize + '::' + dataUrl, null, window.sessionStorage);
this.pageIndexStorage = new StorageHandler(persistenceKeys.pageIndex + '::' + dataUrl, null, window.sessionStorage);
return this.getColumnDefinitions(initialState).pipe(
return this.getColumnDefinitions(config).pipe(
tap((definitions: ColumnDefinition[]) => {
const userPreferences = this.columnSettingsStorage!.getItem() || [];
if (initialState.enableSelection) {
if (config.enableSelection) {
definitions = [selectRowColumnDefinition, ...definitions];
}
if (initialState.enableRowExpansion) {
if (config.enableRowExpansion) {
definitions = [expandRowColumnDefinition, ...definitions];
}
const columnDefinitions = definitions.map((def, index) => {
const columnDefinition = { ...def };
const userPreference = userPreferences.find((columnDef) => columnDef.name === def.name);
columnDefinition.type = def.type || this.getStringType();
if (userPreference) {
if (userPreference.position != null) columnDefinition.position = userPreference.position;
if (userPreference.isVisible != null) columnDefinition.isVisible = userPreference.isVisible;
}
columnDefinition.sticky = userPreference?.sticky || columnDefinition.sticky || '';
if (def.position == null) columnDefinition.position = index + 1;
if (def.isVisible == null) columnDefinition.isVisible = true;
if (def.isSortable == null) columnDefinition.isSortable = false;
if (def.isFilterable == null) columnDefinition.isFilterable = false;
if (def.isEnum == null) columnDefinition.isEnum = false;
if (def.isCollection == null) columnDefinition.isCollection = false;
if (def.isNavigationProperty == null) columnDefinition.isNavigationProperty = false;
if (def.isNullable == null) columnDefinition.isNullable = false;
return columnDefinition;
});
const token = this.keycloak?.getKeycloakInstance()?.tokenParsed;
const columnDefinitions = definitions
.map((def, index) => {
const columnDefinition = { ...def };
const userPreference = userPreferences.find((columnDef) => columnDef.name === def.name);
columnDefinition.type = def.type || this.getStringType();
if (userPreference) {
if (userPreference.position != null) columnDefinition.position = userPreference.position;
if (userPreference.isVisible != null) columnDefinition.isVisible = userPreference.isVisible;
}
columnDefinition.sticky = userPreference?.sticky || columnDefinition.sticky || '';
if (def.position == null) columnDefinition.position = index + 1;
if (def.isVisible == null) columnDefinition.isVisible = true;
if (def.isSortable == null) columnDefinition.isSortable = false;
if (def.isFilterable == null) columnDefinition.isFilterable = false;
if (def.isEnum == null) columnDefinition.isEnum = false;
if (def.isCollection == null) columnDefinition.isCollection = false;
if (def.isNavigationProperty == null) columnDefinition.isNavigationProperty = false;
if (def.isNullable == null) columnDefinition.isNullable = false;
return columnDefinition;
})
.filter((def) => !def.authorizations || (token && validateAuthorizations(token, def.authorizations)));
let filters = Object.fromEntries(
(this.filtersStorage!.getItem() || (initialState.filters as SerializedFilter[]) || [])
(this.filtersStorage!.getItem() || (config.filters as SerializedFilter[]) || [])
.map((entry) => {
const columnDefinition = columnDefinitions.find((colDef) => colDef.name === entry.columnName);
if (!columnDefinition) {
return [null, null];
}
const filter = this.queryableTableTemplateProvider.getFilter(
columnDefinition,
initialState.dataSourceType!,
initialState.serviceUrl!,
initialState.target!,
config.dataSourceType!,
config.serviceUrl!,
config.target!,
);
if (!filter) {
return [null, null];
Expand All @@ -253,15 +270,21 @@ export abstract class QueryableTableStore<
if (!Object.keys(filters).length) {
filters = this.get((state) => state.filters as Filters);
}
const sort = this.sortStorage!.getItem() || initialState.sort || this.get((state) => state.sort);
const pageSize =
this.pageSizeStorage!.getItem() || initialState.pageSize || this.get((state) => state.pageSize);
const pageIndex =
this.pageIndexStorage!.getItem() || initialState.pageIndex || this.get((state) => state.pageIndex);
const sort = this.sortStorage!.getItem() || config.sort || this.get((state) => state.sort);
const pageSize = this.pageSizeStorage!.getItem() || this.get((state) => state.pageSize);
const pageIndex = this.pageIndexStorage!.getItem() || this.get((state) => state.pageIndex);
const enableColumnSettings =
initialState.enableColumnSettings === false ? false : this.get((state) => state.enableColumnSettings);
config.enableColumnSettings === false ? false : this.get((state) => state.enableColumnSettings);
this.patchState({
...initialState,
dataSourceType,
serviceUrl,
target,
useMetadata,
expand,
select,
enableSelection,
enableRowExpansion,
query,
dataUrl,
columnDefinitions,
filters,
Expand All @@ -270,8 +293,13 @@ export abstract class QueryableTableStore<
pageIndex,
enableColumnSettings,
} as TState);
}),
switchMap((_) => {
return this.injectDataSource(config);
}),
tap((dataSource) => {
this.dataSource = dataSource;
const state = this.get();
this.dataSource = this.injectDataSource();
if (state.select) {
this.dataSource.select(state.select);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
import { Injectable, Type, inject } from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { Injectable, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ODataTableStore } from '@neuroglia/angular-ngrx-component-store-odata-table';
import { isSet } from '@neuroglia/common';
import { QueryableTableConfig, QueryableTableState } from '@neuroglia/angular-ngrx-component-store-queryable-table';
import {
ColumnDefinition,
Filter,
Filters,
QueryableTableState,
SerializedFilter,
} from '@neuroglia/angular-ngrx-component-store-queryable-table';
import {
ColumnSettingsComponent,
ColumnSettingsDialogData,
FilterDialogData,
IMaterialQueryableTableStore,
isFilters,
showColumnSettingsDialog,
showFilterDialog,
} from '@neuroglia/angular-ui-material-queryable-table';

/** The state of an OData table specialized for AngularMaterial */
@Injectable()
export class MaterialODataTableStore<TState extends QueryableTableState<TData> = QueryableTableState<any>, TData = any>
export class MaterialODataTableStore<
TState extends QueryableTableState<TData> = QueryableTableState<any>,
TData = any,
TConfig extends QueryableTableConfig = QueryableTableConfig,
>
extends ODataTableStore<TState, TData>
implements IMaterialQueryableTableStore<TState, TData>
{
Expand Down
Loading

0 comments on commit 7b431da

Please sign in to comment.