Smart Filter Encoding Fix (#2387)

This commit is contained in:
Joe Milazzo 2023-11-02 08:35:43 -05:00 committed by GitHub
parent b6d4938e22
commit 9894a2623c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
133 changed files with 677 additions and 471 deletions

View file

@ -1,6 +1,5 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, Params, Router} from '@angular/router';
import {Pagination} from 'src/app/_models/pagination';
import {SortField, SortOptions} from 'src/app/_models/metadata/series-filter';
import {MetadataService} from "../../_services/metadata.service";
import {SeriesFilterV2} from "../../_models/metadata/v2/series-filter-v2";
@ -8,245 +7,93 @@ import {FilterStatement} from "../../_models/metadata/v2/filter-statement";
import {FilterCombination} from "../../_models/metadata/v2/filter-combination";
import {FilterField} from "../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
import {HttpClient} from "@angular/common/http";
import {TextResonse} from "../../_types/text-response";
import {environment} from "../../../environments/environment";
import {map, tap} from "rxjs/operators";
import {of, switchMap} from "rxjs";
const sortOptionsKey = 'sortOptions=';
const statementsKey = 'stmts=';
const limitToKey = 'limitTo=';
const combinationKey = 'combination=';
@Injectable({
providedIn: 'root'
})
export class FilterUtilitiesService {
constructor(private metadataService: MetadataService, private router: Router) {}
private apiUrl = environment.apiUrl;
applyFilter(page: Array<any>, filter: FilterField, comparison: FilterComparison, value: string) {
const dto: SeriesFilterV2 = {
statements: [this.metadataService.createDefaultFilterStatement(filter, comparison, value + '')],
combination: FilterCombination.Or,
limitTo: 0
};
constructor(private metadataService: MetadataService, private router: Router, private http: HttpClient) {}
const url = this.urlFromFilterV2(page.join('/') + '?', dto);
return this.router.navigateByUrl(url);
}
applyFilterWithParams(page: Array<any>, filter: SeriesFilterV2, extraParams: Params) {
let url = this.urlFromFilterV2(page.join('/') + '?', filter);
url += Object.keys(extraParams).map(k => `&${k}=${extraParams[k]}`).join('');
return this.router.navigateByUrl(url, extraParams);
}
/**
* Updates the window location with a custom url based on filter and pagination objects
* @param pagination
* @param filter
*/
updateUrlFromFilterV2(pagination: Pagination, filter: SeriesFilterV2 | undefined) {
const params = '?page=' + pagination.currentPage + '&';
const url = this.urlFromFilterV2(window.location.href.split('?')[0] + params, filter);
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination));
}
private replacePaginationOnUrl(url: string, pagination: Pagination) {
return url.replace(/page=\d+/i, 'page=' + pagination.currentPage);
}
/**
* Will fetch current page from route if present
* @param snapshot to fetch page from. Must be from component else may get stale data
* @param itemsPerPage If you want pagination, pass non-zero number
* @returns A default pagination object
*/
pagination(snapshot: ActivatedRouteSnapshot, itemsPerPage: number = 0): Pagination {
return {currentPage: parseInt(snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage, totalItems: 0, totalPages: 1};
}
/**
* Returns the current url with query params for the filter
* @param currentUrl Full url, with ?page=1 as a minimum
* @param filter Filter to build url off
* @returns current url with query params added
*/
urlFromFilterV2(currentUrl: string, filter: SeriesFilterV2 | undefined) {
if (filter === undefined) return currentUrl;
return currentUrl + this.encodeSeriesFilter(filter);
}
encodeSeriesFilter(filter: SeriesFilterV2) {
const encodedStatements = this.encodeFilterStatements(filter.statements);
const encodedSortOptions = filter.sortOptions ? `${sortOptionsKey}${this.encodeSortOptions(filter.sortOptions)}` : '';
const encodedLimitTo = `${limitToKey}${filter.limitTo}`;
return `${this.encodeName(filter.name)}${encodedStatements}&${encodedSortOptions}&${encodedLimitTo}&${combinationKey}${filter.combination}`;
}
encodeName(name: string | undefined) {
if (name === undefined || name === '') return '';
return `name=${encodeURIComponent(name)}&`
}
encodeSortOptions(sortOptions: SortOptions) {
return `sortField=${sortOptions.sortField},isAscending=${sortOptions.isAscending}`;
}
encodeFilterStatements(statements: Array<FilterStatement>) {
if (statements.length === 0) return '';
return statementsKey + encodeURIComponent(statements.map(statement => {
const encodedComparison = `comparison=${statement.comparison}`;
const encodedField = `field=${statement.field}`;
const encodedValue = `value=${encodeURIComponent(statement.value)}`;
return `${encodedComparison}&${encodedField}&${encodedValue}`;
}).join(','));
}
decodeSeriesFilter(encodedFilter: string) {
const filter = this.metadataService.createDefaultFilterDto();
if (encodedFilter.includes('name=')) {
filter.name = decodeURIComponent(encodedFilter).split('name=')[1].split('&')[0];
}
const stmtsStartIndex = encodedFilter.indexOf(statementsKey);
let endIndex = encodedFilter.indexOf('&' + sortOptionsKey);
if (endIndex < 0) {
endIndex = encodedFilter.indexOf('&' + limitToKey);
}
if (stmtsStartIndex !== -1 || endIndex !== -1) {
// +1 is for the =
const stmtsEncoded = encodedFilter.substring(stmtsStartIndex + statementsKey.length, endIndex);
filter.statements = this.decodeFilterStatements(stmtsEncoded);
}
if (encodedFilter.includes(sortOptionsKey)) {
const optionsStartIndex = encodedFilter.indexOf('&' + sortOptionsKey);
const endIndex = encodedFilter.indexOf('&' + limitToKey);
const sortOptionsEncoded = encodedFilter.substring(optionsStartIndex + sortOptionsKey.length + 1, endIndex);
const sortOptions = this.decodeSortOptions(sortOptionsEncoded);
if (sortOptions) {
filter.sortOptions = sortOptions;
}
}
if (encodedFilter.includes(limitToKey)) {
const limitTo = decodeURIComponent(encodedFilter).split(limitToKey)[1].split('&')[0];
filter.limitTo = parseInt(limitTo, 10);
}
if (encodedFilter.includes(combinationKey)) {
const combo = decodeURIComponent(encodedFilter).split(combinationKey)[1].split('&')[0];;
filter.combination = parseInt(combo, 10) as FilterCombination;
}
return filter;
encodeFilter(filter: SeriesFilterV2 | undefined) {
return this.http.post<string>(this.apiUrl + 'filter/encode', filter, TextResonse);
}
decodeFilter(encodedFilter: string) {
return this.http.post<SeriesFilterV2>(this.apiUrl + 'filter/decode', {encodedFilter}).pipe(map(filter => {
if (filter == null) {
filter = this.metadataService.createDefaultFilterDto();
filter.statements.push(this.createSeriesV2DefaultStatement());
}
filterPresetsFromUrlV2(snapshot: ActivatedRouteSnapshot): SeriesFilterV2 {
const filter = this.metadataService.createDefaultFilterDto();
if (!window.location.href.includes('?')) return filter;
return filter;
}))
}
const queryParams = snapshot.queryParams;
updateUrlFromFilter(filter: SeriesFilterV2 | undefined) {
return this.encodeFilter(filter).pipe(tap(encodedFilter => {
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0]+ '?' + encodedFilter);
}));
}
if (queryParams.name) {
filter.name = queryParams.name;
}
filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot) {
const filter = this.metadataService.createDefaultFilterDto();
filter.statements.push(this.createSeriesV2DefaultStatement());
if (!window.location.href.includes('?')) return of(filter);
const fullUrl = window.location.href.split('?')[1];
const stmtsStartIndex = fullUrl.indexOf(statementsKey);
let endIndex = fullUrl.indexOf('&' + sortOptionsKey);
if (endIndex < 0) {
endIndex = fullUrl.indexOf('&' + limitToKey);
}
return this.decodeFilter(window.location.href.split('?')[1]);
}
if (stmtsStartIndex !== -1 || endIndex !== -1) {
// +1 is for the =
const stmtsEncoded = fullUrl.substring(stmtsStartIndex + statementsKey.length, endIndex);
filter.statements = this.decodeFilterStatements(stmtsEncoded);
}
/**
* Applies and redirects to the passed page with the filter encoded
* @param page
* @param filter
* @param comparison
* @param value
*/
applyFilter(page: Array<any>, filter: FilterField, comparison: FilterComparison, value: string) {
const dto = this.createSeriesV2Filter();
dto.statements.push(this.metadataService.createDefaultFilterStatement(filter, comparison, value + ''));
if (queryParams.sortOptions) {
const optionsStartIndex = fullUrl.indexOf('&' + sortOptionsKey);
const endIndex = fullUrl.indexOf('&' + limitToKey);
const sortOptionsEncoded = fullUrl.substring(optionsStartIndex + sortOptionsKey.length + 1, endIndex);
const sortOptions = this.decodeSortOptions(sortOptionsEncoded);
if (sortOptions) {
filter.sortOptions = sortOptions;
}
}
return this.encodeFilter(dto).pipe(switchMap(encodedFilter => {
return this.router.navigateByUrl(page.join('/') + '?' + encodedFilter);
}));
}
if (queryParams.limitTo) {
filter.limitTo = parseInt(queryParams.limitTo, 10);
}
applyFilterWithParams(page: Array<any>, filter: SeriesFilterV2, extraParams: Params) {
return this.encodeFilter(filter).pipe(switchMap(encodedFilter => {
let url = page.join('/') + '?' + encodedFilter;
url += Object.keys(extraParams).map(k => `&${k}=${extraParams[k]}`).join('');
if (queryParams.combination) {
filter.combination = parseInt(queryParams.combination, 10) as FilterCombination;
}
return this.router.navigateByUrl(url, extraParams);
}));
}
return filter;
}
decodeSortOptions(encodedSortOptions: string): SortOptions | null {
const parts = decodeURIComponent(encodedSortOptions).split(',');
const sortFieldPart = parts.find(part => part.startsWith('sortField='));
const isAscendingPart = parts.find(part => part.startsWith('isAscending='));
if (sortFieldPart && isAscendingPart) {
const sortField = parseInt(sortFieldPart.split('=')[1], 10) as SortField;
const isAscending = isAscendingPart.split('=')[1].toLowerCase() === 'true';
return {sortField, isAscending};
}
return null;
}
decodeFilterStatements(encodedStatements: string): FilterStatement[] {
const statementStrings = decodeURIComponent(encodedStatements).split(',').map(s => decodeURIComponent(s));
return statementStrings.map(statementString => {
const parts = statementString.split(',');
if (parts === null || parts.length < 3) return null;
const comparisonStartToken = parts.find(part => part.startsWith('comparison='));
if (!comparisonStartToken) return null;
const comparison = parseInt(comparisonStartToken.split('=')[1], 10) as FilterComparison;
const fieldStartToken = parts.find(part => part.startsWith('field='));
if (!fieldStartToken) return null;
const field = parseInt(fieldStartToken.split('=')[1], 10) as FilterField;
const valueStartToken = parts.find(part => part.startsWith('value='));
if (!valueStartToken) return null;
const value = decodeURIComponent(valueStartToken.split('=')[1]);
return {comparison, field, value};
}).filter(o => o != null) as FilterStatement[];
}
createSeriesV2Filter(): SeriesFilterV2 {
return {
combination: FilterCombination.And,
statements: [],
limitTo: 0,
sortOptions: {
isAscending: true,
sortField: SortField.SortName
},
};
}
createSeriesV2DefaultStatement(): FilterStatement {
return {
comparison: FilterComparison.Equal,
value: '',
field: FilterField.SeriesName
}
}
createSeriesV2Filter(): SeriesFilterV2 {
return {
combination: FilterCombination.And,
statements: [],
limitTo: 0,
sortOptions: {
isAscending: true,
sortField: SortField.SortName
},
};
}
createSeriesV2DefaultStatement(): FilterStatement {
return {
comparison: FilterComparison.Equal,
value: '',
field: FilterField.SeriesName
}
}
}