diff --git a/backend/package.json b/backend/package.json index e240efe17dca..662e0d95ca04 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,7 +21,7 @@ "gen-docs": "tsx scripts/openapi.ts dist/static/api/openapi.json && redocly build-docs -o dist/static/api/internal.html internal@v2 && redocly bundle -o dist/static/api/public.json public-filter && redocly build-docs -o dist/static/api/public.html public@v2" }, "engines": { - "node": "24.11.0" + "node": "24.11.0 || 22.21.0" }, "dependencies": { "@date-fns/utc": "1.2.0", @@ -40,7 +40,7 @@ "date-fns": "3.6.0", "dotenv": "16.4.5", "etag": "1.8.1", - "express": "5.1.0", + "express": "5.2.0", "express-rate-limit": "7.5.1", "firebase-admin": "12.0.0", "helmet": "4.6.0", @@ -49,7 +49,7 @@ "mjml": "4.15.0", "mongodb": "6.3.0", "mustache": "4.2.0", - "nodemailer": "7.0.7", + "nodemailer": "7.0.11", "object-hash": "3.0.0", "prom-client": "15.1.3", "rate-limiter-flexible": "5.0.3", diff --git a/frontend/package.json b/frontend/package.json index f1ab6e2691b8..aed5df9013bd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,7 @@ "docker": "docker compose -f docker/compose.dev.yml up" }, "engines": { - "node": "24.11.0" + "node": "24.11.0 || 22.21.0" }, "browserslist": [ "defaults", diff --git a/frontend/src/ts/modals/quote-filter.ts b/frontend/src/ts/modals/quote-filter.ts new file mode 100644 index 000000000000..d83fccce564f --- /dev/null +++ b/frontend/src/ts/modals/quote-filter.ts @@ -0,0 +1,51 @@ +import { SimpleModal } from "../utils/simple-modal"; + +export let minFilterLength: number = 0; +export let maxFilterLength: number = 0; +export let removeCustom: boolean = false; + +export function setRemoveCustom(value: boolean): void { + removeCustom = value; +} + +function refresh(): void { + const refreshEvent = new CustomEvent("refresh"); + document.dispatchEvent(refreshEvent); +} + +export const quoteFilterModal = new SimpleModal({ + id: "quoteFilter", + title: "Enter minimum and maximum values", + inputs: [ + { + placeholder: "1", + type: "number", + }, + { + placeholder: "100", + type: "number", + }, + ], + buttonText: "save", + execFn: async (_thisPopup, min, max) => { + const minNum = parseInt(min, 10); + const maxNum = parseInt(max, 10); + if (isNaN(minNum) || isNaN(maxNum)) { + return { + status: 0, + message: "Invalid min/max values", + }; + } + + minFilterLength = minNum; + maxFilterLength = maxNum; + refresh(); + + let message: string = "saved custom filter"; + return { status: 1, message }; + }, + afterClickAway: () => { + setRemoveCustom(true); + refresh(); + }, +}); diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index 12d02d3ee16a..33f371384681 100644 --- a/frontend/src/ts/modals/quote-search.ts +++ b/frontend/src/ts/modals/quote-search.ts @@ -4,6 +4,7 @@ import * as ManualRestart from "../test/manual-restart-tracker"; import * as Notifications from "../elements/notifications"; import * as QuoteSubmitPopup from "./quote-submit"; import * as QuoteApprovePopup from "./quote-approve"; +import * as QuoteFilterPopup from "./quote-filter"; import * as QuoteReportModal from "./quote-report"; import { buildSearchService, @@ -26,6 +27,7 @@ const searchServiceCache: Record> = {}; const pageSize = 100; let currentPageNumber = 1; +let usingCustomLength = true; function getSearchService( language: string, @@ -45,16 +47,65 @@ function getSearchService( function applyQuoteLengthFilter(quotes: Quote[]): Quote[] { if (!modal.isOpen()) return []; - const quoteLengthFilterValue = $( - "#quoteSearchModal .quoteLengthFilter", - ).val() as string[]; + const quoteLengthDropdown = $("#quoteSearchModal .quoteLengthFilter"); + const quoteLengthFilterValue = quoteLengthDropdown.val() as string[]; + if (quoteLengthFilterValue.length === 0) { + usingCustomLength = true; return quotes; } const quoteLengthFilter = new Set( quoteLengthFilterValue.map((filterValue) => parseInt(filterValue, 10)), ); + + const customFilterIndex = quoteLengthFilterValue.indexOf("4"); + + if (customFilterIndex !== -1) { + if (QuoteFilterPopup.removeCustom) { + QuoteFilterPopup.setRemoveCustom(false); + const selectElement = quoteLengthDropdown.get(0) as + | HTMLSelectElement + | null + | undefined; + + if (!selectElement) { + return quotes; + } + + //@ts-expect-error SlimSelect adds slim to the element + const ss = selectElement.slim as SlimSelect | undefined; + + if (ss !== undefined) { + const currentSelected = ss.getSelected(); + + // remove custom selection + const customIndex = currentSelected.indexOf("4"); + if (customIndex > -1) { + currentSelected.splice(customIndex, 1); + } + + ss.setSelected(currentSelected); + } + } else { + if (usingCustomLength) { + QuoteFilterPopup.quoteFilterModal.show(undefined, {}); + usingCustomLength = false; + } else { + const filteredQuotes = quotes.filter( + (quote) => + (quote.length >= QuoteFilterPopup.minFilterLength && + quote.length <= QuoteFilterPopup.maxFilterLength) || + quoteLengthFilter.has(quote.group), + ); + + return filteredQuotes; + } + } + } else { + usingCustomLength = true; + } + const filteredQuotes = quotes.filter((quote) => quoteLengthFilter.has(quote.group), ); @@ -281,6 +332,10 @@ export async function show(showOptions?: ShowOptions): Promise { text: "thicc", value: "3", }, + { + text: "custom", + value: "4", + }, ], }); }, @@ -437,6 +492,13 @@ async function setup(modalEl: HTMLElement): Promise { currentPageNumber--; void updateResults(searchText); }); + + document?.addEventListener("refresh", () => { + const searchText = ( + document.getElementById("searchBox") as HTMLInputElement + ).value; + void updateResults(searchText); + }); } async function cleanup(): Promise { diff --git a/frontend/src/ts/utils/simple-modal.ts b/frontend/src/ts/utils/simple-modal.ts index b1452dadf3e2..63364467637a 100644 --- a/frontend/src/ts/utils/simple-modal.ts +++ b/frontend/src/ts/utils/simple-modal.ts @@ -111,6 +111,7 @@ type SimpleModalOptions = { onlineOnly?: boolean; hideCallsExec?: boolean; showLabels?: boolean; + afterClickAway?: () => void; }; export class SimpleModal { @@ -131,6 +132,7 @@ export class SimpleModal { onlineOnly: boolean; hideCallsExec: boolean; showLabels: boolean; + afterClickAway: (() => void) | undefined; constructor(options: SimpleModalOptions) { this.parameters = []; this.id = options.id; @@ -149,6 +151,7 @@ export class SimpleModal { this.onlineOnly = options.onlineOnly ?? false; this.hideCallsExec = options.hideCallsExec ?? false; this.showLabels = options.showLabels ?? false; + this.afterClickAway = options.afterClickAway; } reset(): void { this.element.innerHTML = ` @@ -480,6 +483,7 @@ const modal = new AnimatedModal({ hide(); }, customWrapperClickHandler: (e): void => { + activePopup?.afterClickAway?.(); hide(); }, }); diff --git a/package.json b/package.json index e074584a310a..b96f7771b8f5 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "knip": "knip" }, "engines": { - "node": "24.11.0" + "node": "24.11.0 || 22.21.0" }, "devDependencies": { "@commitlint/cli": "17.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31219991556c..e895a63bf7cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,7 +73,7 @@ importers: version: 3.52.1(@types/node@24.9.1)(zod@3.23.8) '@ts-rest/express': specifier: 3.52.1 - version: 3.52.1(@ts-rest/core@3.52.1(@types/node@24.9.1)(zod@3.23.8))(express@5.1.0)(zod@3.23.8) + version: 3.52.1(@ts-rest/core@3.52.1(@types/node@24.9.1)(zod@3.23.8))(express@5.2.0)(zod@3.23.8) '@ts-rest/open-api': specifier: 3.52.1 version: 3.52.1(@ts-rest/core@3.52.1(@types/node@24.9.1)(zod@3.23.8))(zod@3.23.8) @@ -102,11 +102,11 @@ importers: specifier: 1.8.1 version: 1.8.1 express: - specifier: 5.1.0 - version: 5.1.0 + specifier: 5.2.0 + version: 5.2.0 express-rate-limit: specifier: 7.5.1 - version: 7.5.1(express@5.1.0) + version: 7.5.1(express@5.2.0) firebase-admin: specifier: 12.0.0 version: 12.0.0(encoding@0.1.13) @@ -129,8 +129,8 @@ importers: specifier: 4.2.0 version: 4.2.0 nodemailer: - specifier: 7.0.7 - version: 7.0.7 + specifier: 7.0.11 + version: 7.0.11 object-hash: specifier: 3.0.0 version: 3.0.0 @@ -3883,8 +3883,8 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} engines: {node: '>=18'} boolbase@1.0.0: @@ -5187,8 +5187,8 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + express@5.2.0: + resolution: {integrity: sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==} engines: {node: '>= 18'} extend@3.0.2: @@ -6837,10 +6837,6 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.53.0: - resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} - engines: {node: '>= 0.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} @@ -7293,8 +7289,8 @@ packages: resolution: {integrity: sha512-1uiY543L+N7Og4yswvlm5NCKgPKDEXd9AUR9Jh3gen6oOeBsesr6LqhXom1er3eRzSUcVRWXzhv8tSNrIfGHKw==} engines: {node: '>=18'} - nodemailer@7.0.7: - resolution: {integrity: sha512-jGOaRznodf62TVzdyhKt/f1Q/c3kYynk8629sgJHpRzGZj01ezbgMMWJSAjHADcwTKxco3B68/R+KHJY2T5BaA==} + nodemailer@7.0.11: + resolution: {integrity: sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==} engines: {node: '>=6.0.0'} nodemon@3.1.4: @@ -11672,7 +11668,7 @@ snapshots: '@hapi/mimos@7.0.1': dependencies: '@hapi/hoek': 11.0.4 - mime-db: 1.53.0 + mime-db: 1.54.0 '@hapi/nigel@5.0.1': dependencies: @@ -11835,7 +11831,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.3.6(supports-color@5.5.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -12548,10 +12544,10 @@ snapshots: '@types/node': 24.9.1 zod: 3.23.8 - '@ts-rest/express@3.52.1(@ts-rest/core@3.52.1(@types/node@24.9.1)(zod@3.23.8))(express@5.1.0)(zod@3.23.8)': + '@ts-rest/express@3.52.1(@ts-rest/core@3.52.1(@types/node@24.9.1)(zod@3.23.8))(express@5.2.0)(zod@3.23.8)': dependencies: '@ts-rest/core': 3.52.1(@types/node@24.9.1)(zod@3.23.8) - express: 5.1.0 + express: 5.2.0 optionalDependencies: zod: 3.23.8 @@ -13508,13 +13504,13 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.2.0: + body-parser@2.2.1: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 http-errors: 2.0.0 - iconv-lite: 0.6.3 + iconv-lite: 0.7.0 on-finished: 2.4.1 qs: 6.14.0 raw-body: 3.0.1 @@ -15099,9 +15095,9 @@ snapshots: exponential-backoff@3.1.1: optional: true - express-rate-limit@7.5.1(express@5.1.0): + express-rate-limit@7.5.1(express@5.2.0): dependencies: - express: 5.1.0 + express: 5.2.0 express@4.21.2: dependencies: @@ -15139,15 +15135,16 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.1.0: + express@5.2.0: dependencies: accepts: 2.0.0 - body-parser: 2.2.0 + body-parser: 2.2.1 content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 debug: 4.4.3 + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -16096,6 +16093,7 @@ snapshots: iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 + optional: true iconv-lite@0.7.0: dependencies: @@ -17104,8 +17102,6 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.53.0: {} - mime-db@1.54.0: {} mime-types@2.1.35: @@ -17710,7 +17706,7 @@ snapshots: dependencies: '@babel/parser': 7.28.5 - nodemailer@7.0.7: {} + nodemailer@7.0.11: {} nodemon@3.1.4: dependencies: