From a43fb03bc47b020d9b77819d0f4d2c70b46c54ce Mon Sep 17 00:00:00 2001 From: Kyle Kemp Date: Thu, 25 Apr 2024 09:31:41 -0500 Subject: [PATCH] closes #3 --- interfaces/product.ts | 1 + search/operators/_helpers.ts | 28 ++++-------------- search/operators/bare.ts | 2 +- search/search.ts | 32 +++++++++++++-------- src/app/advanced/advanced.page.html | 44 ++++++++++++++++++++++++++++- src/app/advanced/advanced.page.ts | 39 +++++++++++++++++++++++-- src/app/cards.service.ts | 41 ++++++++++++++++++--------- src/app/meta.service.ts | 12 +++++++- 8 files changed, 147 insertions(+), 52 deletions(-) diff --git a/interfaces/product.ts b/interfaces/product.ts index 10245de..d639cd7 100644 --- a/interfaces/product.ts +++ b/interfaces/product.ts @@ -1,6 +1,7 @@ export interface IProductFilter { name: string; prop: string; + type: 'number'; } export interface IProductDefinition { diff --git a/search/operators/_helpers.ts b/search/operators/_helpers.ts index e0ea6fb..17c9dd9 100644 --- a/search/operators/_helpers.ts +++ b/search/operators/_helpers.ts @@ -1,10 +1,10 @@ -import { isArray } from 'lodash'; +import { get, isArray } from 'lodash'; import type * as parser from 'search-query-parser'; import { type ICard } from '../../interfaces'; function getValueFromCard(card: ICard, prop: keyof ICard): T { - return card[prop] as T; + return get(card, prop) as T; } function cardMatchesNumberCheck(value: number, numberCheck: string): boolean { @@ -38,11 +38,7 @@ function cardMatchesNumberCheck(value: number, numberCheck: string): boolean { // this operator works on number fields // it supports exact matching, as well as >, >=, <, <= export function numericalOperator(aliases: string[], key: keyof ICard) { - return ( - cards: ICard[], - results: parser.SearchParserResult, - extraData = {} - ) => { + return (cards: ICard[], results: parser.SearchParserResult) => { // if we have no cards, short-circuit because we can't filter it anymore if (cards.length === 0) { return []; @@ -95,11 +91,7 @@ export function numericalOperator(aliases: string[], key: keyof ICard) { // it also checks case-insensitively // it also supports "none" as a value for empty arrays export function arraySearchOperator(aliases: string[], key: keyof ICard) { - return ( - cards: ICard[], - results: parser.SearchParserResult, - extraData = {} - ) => { + return (cards: ICard[], results: parser.SearchParserResult) => { // if we have no cards, short-circuit because we can't filter it anymore if (cards.length === 0) { return []; @@ -175,11 +167,7 @@ export function partialWithOptionalExactTextOperator( aliases: string[], key: keyof ICard ) { - return ( - cards: ICard[], - results: parser.SearchParserResult, - extraData = {} - ) => { + return (cards: ICard[], results: parser.SearchParserResult) => { // if we have no cards, short-circuit because we can't filter it anymore if (cards.length === 0) { return []; @@ -252,11 +240,7 @@ export function partialWithOptionalExactTextOperator( // most properties can use this sufficiently // it still checks case-insensitively export function exactTextOperator(aliases: string[], key: keyof ICard) { - return ( - cards: ICard[], - results: parser.SearchParserResult, - extraData = {} - ) => { + return (cards: ICard[], results: parser.SearchParserResult) => { // if we have no cards, short-circuit because we can't filter it anymore if (cards.length === 0) { return []; diff --git a/search/operators/bare.ts b/search/operators/bare.ts index 0951b68..679bfff 100644 --- a/search/operators/bare.ts +++ b/search/operators/bare.ts @@ -1,6 +1,6 @@ import { type ICard } from '../../interfaces'; -export function bare(cards: ICard[], query: string, extraData = {}): ICard[] { +export function bare(cards: ICard[], query: string): ICard[] { const sQueries = query.toLowerCase().split(' '); const matches = (card: ICard, term: string) => { diff --git a/search/search.ts b/search/search.ts index ae1761a..4eae582 100644 --- a/search/search.ts +++ b/search/search.ts @@ -14,7 +14,12 @@ const allKeywords = [ ['tag'], // array search ]; -const operators = [card, name, product, subproduct, tag]; +export type ParserOperator = ( + cards: ICard[], + results: parser.SearchParserResult +) => ICard[]; + +const operators: ParserOperator[] = [card, name, product, subproduct, tag]; export function properOperatorsInsteadOfAliases( result: parser.SearchParserResult @@ -130,19 +135,24 @@ export function queryToText(query: string): string { export function parseQuery( cards: ICard[], query: string, - extraData = {} + extraOperators: Array<{ aliases: string[]; operator: ParserOperator }> = [] ): ICard[] { query = query.toLowerCase().trim(); + const validKeywords = [ + allKeywords, + ...extraOperators.map((o) => o.aliases), + ].flat(2); + const result = parser.parse(query, { - keywords: allKeywords.flat(), + keywords: validKeywords, offsets: false, }); // the parser returns a string if there's nothing interesting to do, for some reason // so we have a bare words parser if (isString(result)) { - return bare(cards, query, extraData); + return bare(cards, query); } const resultText = (result as parser.SearchParserResult).text as string; @@ -150,17 +160,15 @@ export function parseQuery( let returnCards = cards; if (resultText) { - returnCards = bare(returnCards, resultText, extraData); + returnCards = bare(returnCards, resultText); } // check all the operators - operators.forEach((operator) => { - returnCards = operator( - returnCards, - result as parser.SearchParserResult, - extraData - ); - }); + [operators, extraOperators.map((o) => o.operator)] + .flat() + .forEach((operator) => { + returnCards = operator(returnCards, result as parser.SearchParserResult); + }); return returnCards; } diff --git a/src/app/advanced/advanced.page.html b/src/app/advanced/advanced.page.html index 862e9da..eb23fb5 100644 --- a/src/app/advanced/advanced.page.html +++ b/src/app/advanced/advanced.page.html @@ -62,6 +62,16 @@ + @for(filter of visibleFilters; track index) { + + @switch(filter.type) { + @case ('number') { + + } + } + + } + @@ -76,4 +86,36 @@ - \ No newline at end of file + + + + + + {{ filter.name }} + + + + + + + + {{ operator.label }} + + + + + + + + + + + + + + If left unspecified, will not be included in search query. + + + + \ No newline at end of file diff --git a/src/app/advanced/advanced.page.ts b/src/app/advanced/advanced.page.ts index 0c1bad7..1d7480e 100644 --- a/src/app/advanced/advanced.page.ts +++ b/src/app/advanced/advanced.page.ts @@ -2,7 +2,8 @@ import { Component, inject, type OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { LocalStorage } from 'ngx-webstorage'; -import { sortBy, uniqBy } from 'lodash'; +import { isNumber, sortBy, uniqBy } from 'lodash'; +import type { IProductFilter } from '../../../interfaces'; import { CardsService } from '../cards.service'; import { MetaService } from '../meta.service'; @@ -11,6 +12,7 @@ const defaultQuery = () => ({ product: undefined, subproduct: [], tags: [], + meta: {}, }); interface DropdownForProductItem { @@ -27,7 +29,7 @@ interface DropdownForProductItem { export class AdvancedPage implements OnInit { private router = inject(Router); private cardsService = inject(CardsService); - private metaService = inject(MetaService); + public metaService = inject(MetaService); public allOperators = [ { value: '=', label: 'Equal To' }, @@ -45,6 +47,8 @@ export class AdvancedPage implements OnInit { public visibleSubproducts: DropdownForProductItem[] = []; public visibleTags: string[] = []; + public visibleFilters: IProductFilter[] = []; + @LocalStorage() // eslint-disable-next-line @typescript-eslint/no-explicit-any public searchQuery: any; @@ -84,6 +88,7 @@ export class AdvancedPage implements OnInit { this.visibleTags = this.allTags; this.setSubproductsBasedOnProduct(); + this.setBasicMeta(); } saveQuery() { @@ -94,8 +99,23 @@ export class AdvancedPage implements OnInit { changeProduct() { this.searchQuery.subproducts = []; this.searchQuery.tags = []; + this.searchQuery.meta = {}; this.setSubproductsBasedOnProduct(); + this.setBasicMeta(); + } + + setBasicMeta() { + this.visibleFilters.forEach((filter) => { + if (this.searchQuery.meta[filter.prop]) return; + + if (filter.type === 'number') { + this.searchQuery.meta[filter.prop] = { + operator: '=', + value: undefined, + }; + } + }); } setSubproductsBasedOnProduct() { @@ -105,6 +125,10 @@ export class AdvancedPage implements OnInit { this.visibleSubproducts = this.allSubproducts.filter( (sp) => sp.product === this.searchQuery.product.value ); + + this.visibleFilters = this.metaService.getFiltersByProductId( + this.searchQuery.product.value + ); } } @@ -130,6 +154,17 @@ export class AdvancedPage implements OnInit { queryAttributes.push(`tag:"${this.searchQuery.tags.join(',')}"`); } + this.visibleFilters.forEach((filter) => { + if (!this.searchQuery.meta[filter.prop]) return; + + if (filter.type === 'number') { + const { operator, value } = this.searchQuery.meta[filter.prop]; + if (isNumber(+value) && !isNaN(+value)) { + queryAttributes.push(`${filter.prop}:${operator}${value}`); + } + } + }); + const query = queryAttributes.join(' '); return query; diff --git a/src/app/cards.service.ts b/src/app/cards.service.ts index 0672a50..aeef0c4 100644 --- a/src/app/cards.service.ts +++ b/src/app/cards.service.ts @@ -1,11 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { decompress } from 'compress-json'; import { sortBy } from 'lodash'; -import { type ICard } from '../../interfaces'; -import { parseQuery } from '../../search/search'; +import { type ICard, type IProductFilter } from '../../interfaces'; +import { numericalOperator } from '../../search/operators/_helpers'; +import { parseQuery, type ParserOperator } from '../../search/search'; import { environment } from '../environments/environment'; +import { MetaService } from './meta.service'; @Injectable({ providedIn: 'root', @@ -13,13 +15,9 @@ import { environment } from '../environments/environment'; export class CardsService { private cards: ICard[] = []; private cardsByName: Record = {}; - private cardsByCode: Record = {}; + private cardsById: Record = {}; - private collection: Record = {}; - - public get cardCollection() { - return this.collection; - } + private metaService = inject(MetaService); public async init() { const cardData = await fetch(`${environment.baseUrl}/cards.min.json`); @@ -34,19 +32,36 @@ export class CardsService { this.cards.forEach((card) => { this.cardsByName[card.name] = card; - this.cardsByCode[card.id] = card; + this.cardsById[card.id] = card; }); } // card utilities - public getCardByCodeOrName(codeOrName: string): ICard | undefined { + public getCardByIdOrName(codeOrName: string): ICard | undefined { return ( - this.cardsByCode[codeOrName] ?? this.cardsByName[codeOrName] ?? undefined + this.cardsById[codeOrName] ?? this.cardsByName[codeOrName] ?? undefined ); } public searchCards(query: string): ICard[] { - return parseQuery(this.cards, query, { collection: this.cardCollection }); + const extraFilters = this.metaService.getAllFilters(); + const extraFilterOperators = extraFilters.map((filter) => { + const mapped: Record< + IProductFilter['type'], + (f: IProductFilter) => ParserOperator + > = { + number: (f: IProductFilter) => { + return numericalOperator( + [filter.prop], + `meta.${f.prop}` as keyof ICard + ); + }, + }; + + return { operator: mapped[filter.type](filter), aliases: [filter.prop] }; + }); + + return parseQuery(this.cards, query, extraFilterOperators); } public getCardById(id: string): ICard | undefined { diff --git a/src/app/meta.service.ts b/src/app/meta.service.ts index ad8f942..1ebbe3f 100644 --- a/src/app/meta.service.ts +++ b/src/app/meta.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import type { IProduct } from '../../interfaces'; +import type { IProduct, IProductFilter } from '../../interfaces'; import { environment } from '../environments/environment'; @Injectable({ @@ -10,6 +10,7 @@ export class MetaService { private allProducts: IProduct[] = []; private templatesByProductId: Record = {}; private rulesByProductId: Record = {}; + private filtersByProductId: Record = {}; public get products() { return this.allProducts; @@ -28,6 +29,7 @@ export class MetaService { this.allProducts.forEach((product) => { this.templatesByProductId[product.id] = product.cardTemplate; this.rulesByProductId[product.id] = product.external.rules; + this.filtersByProductId[product.id] = product.filters; }); } @@ -38,4 +40,12 @@ export class MetaService { public getRulesByProductId(productId: string): string { return this.rulesByProductId[productId]; } + + public getFiltersByProductId(productId: string): IProductFilter[] { + return this.filtersByProductId[productId] ?? []; + } + + public getAllFilters(): IProductFilter[] { + return Object.values(this.filtersByProductId).flat(); + } }