From cf23d7adac7aa86b249303488c5ad574c012f2af Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Jul 2026 13:51:21 -0400 Subject: [PATCH 01/10] Favorite names can now be edited inline new file: drizzle/0003_favorites_add_name.sql modified: package-lock.json modified: package.json modified: src/__test_utils__/seed.ts modified: src/backend/routes/favorites.test.ts modified: src/backend/routes/favorites.ts modified: src/frontend/cards/card-components.tsx modified: src/frontend/favorites/favorite-button.tsx modified: src/frontend/favorites/favorite-card.tsx modified: src/frontend/favorites/favorite-menu.tsx modified: src/shared/api-models.ts modified: src/shared/schema.ts --- coverage/clover.xml | 6 + coverage/coverage-final.json | 1 + coverage/lcov-report/base.css | 362 +++++++ coverage/lcov-report/block-navigation.js | 87 ++ coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes coverage/lcov-report/index.html | 101 ++ coverage/lcov-report/prettify.css | 101 ++ coverage/lcov-report/prettify.js | 1048 ++++++++++++++++++++ coverage/lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/lcov-report/sorter.js | 210 ++++ coverage/lcov.info | 0 drizzle/0003_favorites_add_name.sql | 1 + package-lock.json | 90 +- package.json | 11 +- src/__test_utils__/seed.ts | 4 +- src/backend/routes/favorites.test.ts | 6 +- src/backend/routes/favorites.ts | 39 +- src/frontend/cards/card-components.tsx | 24 +- src/frontend/favorites/favorite-button.tsx | 3 +- src/frontend/favorites/favorite-card.tsx | 95 +- src/frontend/favorites/favorite-menu.tsx | 8 +- src/shared/api-models.ts | 1 + src/shared/schema.ts | 2 + 23 files changed, 2129 insertions(+), 71 deletions(-) create mode 100644 coverage/clover.xml create mode 100644 coverage/coverage-final.json create mode 100644 coverage/lcov-report/base.css create mode 100644 coverage/lcov-report/block-navigation.js create mode 100644 coverage/lcov-report/favicon.png create mode 100644 coverage/lcov-report/index.html create mode 100644 coverage/lcov-report/prettify.css create mode 100644 coverage/lcov-report/prettify.js create mode 100644 coverage/lcov-report/sort-arrow-sprite.png create mode 100644 coverage/lcov-report/sorter.js create mode 100644 coverage/lcov.info create mode 100644 drizzle/0003_favorites_add_name.sql diff --git a/coverage/clover.xml b/coverage/clover.xml new file mode 100644 index 000000000..c410f9466 --- /dev/null +++ b/coverage/clover.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/coverage/coverage-final.json @@ -0,0 +1 @@ +{} diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 000000000..85891cb88 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,362 @@ +body, +html { + margin: 0; + padding: 0; + height: 100%; +} +body { + font-family: + Helvetica Neue, + Helvetica, + Arial; + font-size: 14px; + color: #333; +} +.small { + font-size: 12px; +} +*, +*:after, +*:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +h1 { + font-size: 20px; + margin: 0; +} +h2 { + font-size: 14px; +} +pre { + font: + 12px/1.4 Consolas, + "Liberation Mono", + Menlo, + Courier, + monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { + color: #0074d9; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +.strong { + font-weight: bold; +} +.space-top1 { + padding: 10px 0 0 0; +} +.pad2y { + padding: 20px 0; +} +.pad1y { + padding: 10px 0; +} +.pad2x { + padding: 0 20px; +} +.pad2 { + padding: 20px; +} +.pad1 { + padding: 10px; +} +.space-left2 { + padding-left: 55px; +} +.space-right2 { + padding-right: 20px; +} +.center { + text-align: center; +} +.clearfix { + display: block; +} +.clearfix:after { + content: ""; + display: block; + height: 0; + clear: both; + visibility: hidden; +} +.fl { + float: left; +} +@media only screen and (max-width: 640px) { + .col3 { + width: 100%; + max-width: 100%; + } + .hide-mobile { + display: none !important; + } +} + +.quiet { + color: #7f7f7f; + color: rgba(0, 0, 0, 0.5); +} +.quiet a { + opacity: 0.7; +} + +.fraction { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #e8e8e8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, +div.path a:visited { + color: #333; +} +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width: 20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, +.skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { + border-bottom: 1px solid #bbb; +} +.keyline-all { + border: 1px solid #ddd; +} +.coverage-summary td, +.coverage-summary th { + padding: 10px; +} +.coverage-summary tbody { + border: 1px solid #bbb; +} +.coverage-summary td { + border-right: 1px solid #bbb; +} +.coverage-summary td:last-child { + border-right: none; +} +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { + border-right: none !important; +} +.coverage-summary th.pct { +} +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { + text-align: right; +} +.coverage-summary td.file { + white-space: nowrap; +} +.coverage-summary td.pic { + min-width: 120px !important; +} +.coverage-summary tfoot td { +} + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { + height: 10px; +} +/* yellow */ +.cbranch-no { + background: yellow !important; + color: #111; +} +/* dark red */ +.red.solid, +.status-line.low, +.low .cover-fill { + background: #c21f39; +} +.low .chart { + border: 1px solid #c21f39; +} +.highlighted, +.highlighted .cstat-no, +.highlighted .fstat-no, +.highlighted .cbranch-no { + background: #c21f39 !important; +} +/* medium red */ +.cstat-no, +.fstat-no, +.cbranch-no, +.cbranch-no { + background: #f6c6ce; +} +/* light red */ +.low, +.cline-no { + background: #fce1e5; +} +/* light green */ +.high, +.cline-yes { + background: rgb(230, 245, 208); +} +/* medium green */ +.cstat-yes { + background: rgb(161, 215, 106); +} +/* dark green */ +.status-line.high, +.high .cover-fill { + background: rgb(77, 146, 33); +} +.high .chart { + border: 1px solid rgb(77, 146, 33); +} +/* dark yellow (gold) */ +.status-line.medium, +.medium .cover-fill { + background: #f9cd0b; +} +.medium .chart { + border: 1px solid #f9cd0b; +} +/* light yellow */ +.medium { + background: #fff4c2; +} + +.cstat-skip { + background: #ddd; + color: #111; +} +.fstat-skip { + background: #ddd; + color: #111 !important; +} +.cbranch-skip { + background: #ddd !important; + color: #111; +} + +span.cline-neutral { + background: #eaeaea; +} + +.coverage-summary td.empty { + opacity: 0.5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, +.cover-empty { + display: inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { + color: #999 !important; +} +.ignore-none { + color: #999; + font-weight: normal; +} + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, +.push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 000000000..1d4778881 --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = [".cbranch-no", ".cstat-no", ".fstat-no"]; + + // Elements to highlight in the file listing view + var fileListingElements = ["td.pct.low"]; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ":not(" + missingCoverageClasses.join("):not(") + ") > "; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(", ") + + ", " + + notSelector + + missingCoverageClasses.join(", " + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove("highlighted"); + missingCoverageElements.item(index).classList.add("highlighted"); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center" + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== "number" || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === "number" && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById("fileSearch") === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener("keydown", jumpToCode); diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ Unknown% + Statements + 0/0 +
+ + +
+ Unknown% + Branches + 0/0 +
+ + +
+ Unknown% + Functions + 0/0 +
+ + +
+ Unknown% + Lines + 0/0 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 000000000..babc093e8 --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1,101 @@ +.pln { + color: #000; +} +@media screen { + .str { + color: #080; + } + .kwd { + color: #008; + } + .com { + color: #800; + } + .typ { + color: #606; + } + .lit { + color: #066; + } + .pun, + .opn, + .clo { + color: #660; + } + .tag { + color: #008; + } + .atn { + color: #606; + } + .atv { + color: #080; + } + .dec, + .var { + color: #606; + } + .fun { + color: red; + } +} +@media print, projection { + .str { + color: #060; + } + .kwd { + color: #006; + font-weight: bold; + } + .com { + color: #600; + font-style: italic; + } + .typ { + color: #404; + font-weight: bold; + } + .lit { + color: #044; + } + .pun, + .opn, + .clo { + color: #440; + } + .tag { + color: #006; + font-weight: bold; + } + .atn { + color: #404; + } + .atv { + color: #060; + } +} +pre.prettyprint { + padding: 2px; + border: 1px solid #888; +} +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} +li.L0, +li.L1, +li.L2, +li.L3, +li.L5, +li.L6, +li.L7, +li.L8 { + list-style-type: none; +} +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + background: #eee; +} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 000000000..6894dbba3 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1,1048 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION = true; +(function () { + var h = ["break,continue,do,else,for,if,return,while"]; + var u = [ + h, + "auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile" + ]; + var p = [ + u, + "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof" + ]; + var l = [ + p, + "alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where" + ]; + var x = [ + p, + "abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient" + ]; + var R = [ + x, + "as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var" + ]; + var r = + "all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes"; + var w = [ + p, + "debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN" + ]; + var s = + "caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"; + var I = [ + h, + "and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None" + ]; + var f = [ + h, + "alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END" + ]; + var H = [h, "case,done,elif,esac,eval,fi,function,in,local,set,then,until"]; + var A = [l, R, w, s + I, f, H]; + var e = + /^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/; + var C = "str"; + var z = "kwd"; + var j = "com"; + var O = "typ"; + var G = "lit"; + var L = "pun"; + var F = "pln"; + var m = "tag"; + var E = "dec"; + var J = "src"; + var P = "atn"; + var n = "atv"; + var N = "nocode"; + var M = + "(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*"; + function k(Z) { + var ad = 0; + var S = false; + var ac = false; + for (var V = 0, U = Z.length; V < U; ++V) { + var ae = Z[V]; + if (ae.ignoreCase) { + ac = true; + } else { + if ( + /[a-z]/i.test( + ae.source.replace( + /\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, + "" + ) + ) + ) { + S = true; + ac = false; + break; + } + } + } + var Y = { b: 8, t: 9, n: 10, v: 11, f: 12, r: 13 }; + function ab(ah) { + var ag = ah.charCodeAt(0); + if (ag !== 92) { + return ag; + } + var af = ah.charAt(1); + ag = Y[af]; + if (ag) { + return ag; + } else { + if ("0" <= af && af <= "7") { + return parseInt(ah.substring(1), 8); + } else { + if (af === "u" || af === "x") { + return parseInt(ah.substring(2), 16); + } else { + return ah.charCodeAt(1); + } + } + } + } + function T(af) { + if (af < 32) { + return (af < 16 ? "\\x0" : "\\x") + af.toString(16); + } + var ag = String.fromCharCode(af); + if (ag === "\\" || ag === "-" || ag === "[" || ag === "]") { + ag = "\\" + ag; + } + return ag; + } + function X(am) { + var aq = am + .substring(1, am.length - 1) + .match( + new RegExp( + "\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]", + "g" + ) + ); + var ak = []; + var af = []; + var ao = aq[0] === "^"; + for (var ar = ao ? 1 : 0, aj = aq.length; ar < aj; ++ar) { + var ah = aq[ar]; + if (/\\[bdsw]/i.test(ah)) { + ak.push(ah); + } else { + var ag = ab(ah); + var al; + if (ar + 2 < aj && "-" === aq[ar + 1]) { + al = ab(aq[ar + 2]); + ar += 2; + } else { + al = ag; + } + af.push([ag, al]); + if (!(al < 65 || ag > 122)) { + if (!(al < 65 || ag > 90)) { + af.push([ + Math.max(65, ag) | 32, + Math.min(al, 90) | 32 + ]); + } + if (!(al < 97 || ag > 122)) { + af.push([ + Math.max(97, ag) & ~32, + Math.min(al, 122) & ~32 + ]); + } + } + } + } + af.sort(function (av, au) { + return av[0] - au[0] || au[1] - av[1]; + }); + var ai = []; + var ap = [NaN, NaN]; + for (var ar = 0; ar < af.length; ++ar) { + var at = af[ar]; + if (at[0] <= ap[1] + 1) { + ap[1] = Math.max(ap[1], at[1]); + } else { + ai.push((ap = at)); + } + } + var an = ["["]; + if (ao) { + an.push("^"); + } + an.push.apply(an, ak); + for (var ar = 0; ar < ai.length; ++ar) { + var at = ai[ar]; + an.push(T(at[0])); + if (at[1] > at[0]) { + if (at[1] + 1 > at[0]) { + an.push("-"); + } + an.push(T(at[1])); + } + } + an.push("]"); + return an.join(""); + } + function W(al) { + var aj = al.source.match( + new RegExp( + "(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)", + "g" + ) + ); + var ah = aj.length; + var an = []; + for (var ak = 0, am = 0; ak < ah; ++ak) { + var ag = aj[ak]; + if (ag === "(") { + ++am; + } else { + if ("\\" === ag.charAt(0)) { + var af = +ag.substring(1); + if (af && af <= am) { + an[af] = -1; + } + } + } + } + for (var ak = 1; ak < an.length; ++ak) { + if (-1 === an[ak]) { + an[ak] = ++ad; + } + } + for (var ak = 0, am = 0; ak < ah; ++ak) { + var ag = aj[ak]; + if (ag === "(") { + ++am; + if (an[am] === undefined) { + aj[ak] = "(?:"; + } + } else { + if ("\\" === ag.charAt(0)) { + var af = +ag.substring(1); + if (af && af <= am) { + aj[ak] = "\\" + an[am]; + } + } + } + } + for (var ak = 0, am = 0; ak < ah; ++ak) { + if ("^" === aj[ak] && "^" !== aj[ak + 1]) { + aj[ak] = ""; + } + } + if (al.ignoreCase && S) { + for (var ak = 0; ak < ah; ++ak) { + var ag = aj[ak]; + var ai = ag.charAt(0); + if (ag.length >= 2 && ai === "[") { + aj[ak] = X(ag); + } else { + if (ai !== "\\") { + aj[ak] = ag.replace(/[a-zA-Z]/g, function (ao) { + var ap = ao.charCodeAt(0); + return ( + "[" + + String.fromCharCode(ap & ~32, ap | 32) + + "]" + ); + }); + } + } + } + } + return aj.join(""); + } + var aa = []; + for (var V = 0, U = Z.length; V < U; ++V) { + var ae = Z[V]; + if (ae.global || ae.multiline) { + throw new Error("" + ae); + } + aa.push("(?:" + W(ae) + ")"); + } + return new RegExp(aa.join("|"), ac ? "gi" : "g"); + } + function a(V) { + var U = /(?:^|\s)nocode(?:\s|$)/; + var X = []; + var T = 0; + var Z = []; + var W = 0; + var S; + if (V.currentStyle) { + S = V.currentStyle.whiteSpace; + } else { + if (window.getComputedStyle) { + S = document.defaultView + .getComputedStyle(V, null) + .getPropertyValue("white-space"); + } + } + var Y = S && "pre" === S.substring(0, 3); + function aa(ab) { + switch (ab.nodeType) { + case 1: + if (U.test(ab.className)) { + return; + } + for (var ae = ab.firstChild; ae; ae = ae.nextSibling) { + aa(ae); + } + var ad = ab.nodeName; + if ("BR" === ad || "LI" === ad) { + X[W] = "\n"; + Z[W << 1] = T++; + Z[(W++ << 1) | 1] = ab; + } + break; + case 3: + case 4: + var ac = ab.nodeValue; + if (ac.length) { + if (!Y) { + ac = ac.replace(/[ \t\r\n]+/g, " "); + } else { + ac = ac.replace(/\r\n?/g, "\n"); + } + X[W] = ac; + Z[W << 1] = T; + T += ac.length; + Z[(W++ << 1) | 1] = ab; + } + break; + } + } + aa(V); + return { sourceCode: X.join("").replace(/\n$/, ""), spans: Z }; + } + function B(S, U, W, T) { + if (!U) { + return; + } + var V = { sourceCode: U, basePos: S }; + W(V); + T.push.apply(T, V.decorations); + } + var v = /\S/; + function o(S) { + var V = undefined; + for (var U = S.firstChild; U; U = U.nextSibling) { + var T = U.nodeType; + V = + T === 1 + ? V + ? S + : U + : T === 3 + ? v.test(U.nodeValue) + ? S + : V + : V; + } + return V === S ? undefined : V; + } + function g(U, T) { + var S = {}; + var V; + (function () { + var ad = U.concat(T); + var ah = []; + var ag = {}; + for (var ab = 0, Z = ad.length; ab < Z; ++ab) { + var Y = ad[ab]; + var ac = Y[3]; + if (ac) { + for (var ae = ac.length; --ae >= 0; ) { + S[ac.charAt(ae)] = Y; + } + } + var af = Y[1]; + var aa = "" + af; + if (!ag.hasOwnProperty(aa)) { + ah.push(af); + ag[aa] = null; + } + } + ah.push(/[\0-\uffff]/); + V = k(ah); + })(); + var X = T.length; + var W = function (ah) { + var Z = ah.sourceCode, + Y = ah.basePos; + var ad = [Y, F]; + var af = 0; + var an = Z.match(V) || []; + var aj = {}; + for (var ae = 0, aq = an.length; ae < aq; ++ae) { + var ag = an[ae]; + var ap = aj[ag]; + var ai = void 0; + var am; + if (typeof ap === "string") { + am = false; + } else { + var aa = S[ag.charAt(0)]; + if (aa) { + ai = ag.match(aa[1]); + ap = aa[0]; + } else { + for (var ao = 0; ao < X; ++ao) { + aa = T[ao]; + ai = ag.match(aa[1]); + if (ai) { + ap = aa[0]; + break; + } + } + if (!ai) { + ap = F; + } + } + am = ap.length >= 5 && "lang-" === ap.substring(0, 5); + if (am && !(ai && typeof ai[1] === "string")) { + am = false; + ap = J; + } + if (!am) { + aj[ag] = ap; + } + } + var ab = af; + af += ag.length; + if (!am) { + ad.push(Y + ab, ap); + } else { + var al = ai[1]; + var ak = ag.indexOf(al); + var ac = ak + al.length; + if (ai[2]) { + ac = ag.length - ai[2].length; + ak = ac - al.length; + } + var ar = ap.substring(5); + B(Y + ab, ag.substring(0, ak), W, ad); + B(Y + ab + ak, al, q(ar, al), ad); + B(Y + ab + ac, ag.substring(ac), W, ad); + } + } + ah.decorations = ad; + }; + return W; + } + function i(T) { + var W = [], + S = []; + if (T.tripleQuotedStrings) { + W.push([ + C, + /^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/, + null, + "'\"" + ]); + } else { + if (T.multiLineStrings) { + W.push([ + C, + /^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/, + null, + "'\"`" + ]); + } else { + W.push([ + C, + /^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/, + null, + "\"'" + ]); + } + } + if (T.verbatimStrings) { + S.push([C, /^@\"(?:[^\"]|\"\")*(?:\"|$)/, null]); + } + var Y = T.hashComments; + if (Y) { + if (T.cStyleComments) { + if (Y > 1) { + W.push([ + j, + /^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/, + null, + "#" + ]); + } else { + W.push([ + j, + /^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/, + null, + "#" + ]); + } + S.push([ + C, + /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/, + null + ]); + } else { + W.push([j, /^#[^\r\n]*/, null, "#"]); + } + } + if (T.cStyleComments) { + S.push([j, /^\/\/[^\r\n]*/, null]); + S.push([j, /^\/\*[\s\S]*?(?:\*\/|$)/, null]); + } + if (T.regexLiterals) { + var X = + "/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/"; + S.push(["lang-regex", new RegExp("^" + M + "(" + X + ")")]); + } + var V = T.types; + if (V) { + S.push([O, V]); + } + var U = ("" + T.keywords).replace(/^ | $/g, ""); + if (U.length) { + S.push([ + z, + new RegExp("^(?:" + U.replace(/[\s,]+/g, "|") + ")\\b"), + null + ]); + } + W.push([F, /^\s+/, null, " \r\n\t\xA0"]); + S.push( + [G, /^@[a-z_$][a-z_$@0-9]*/i, null], + [O, /^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/, null], + [F, /^[a-z_$][a-z_$@0-9]*/i, null], + [ + G, + new RegExp( + "^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*", + "i" + ), + null, + "0123456789" + ], + [F, /^\\[\s\S]?/, null], + [L, /^.[^\s\w\.$@\'\"\`\/\#\\]*/, null] + ); + return g(W, S); + } + var K = i({ + keywords: A, + hashComments: true, + cStyleComments: true, + multiLineStrings: true, + regexLiterals: true + }); + function Q(V, ag) { + var U = /(?:^|\s)nocode(?:\s|$)/; + var ab = /\r\n?|\n/; + var ac = V.ownerDocument; + var S; + if (V.currentStyle) { + S = V.currentStyle.whiteSpace; + } else { + if (window.getComputedStyle) { + S = ac.defaultView + .getComputedStyle(V, null) + .getPropertyValue("white-space"); + } + } + var Z = S && "pre" === S.substring(0, 3); + var af = ac.createElement("LI"); + while (V.firstChild) { + af.appendChild(V.firstChild); + } + var W = [af]; + function ae(al) { + switch (al.nodeType) { + case 1: + if (U.test(al.className)) { + break; + } + if ("BR" === al.nodeName) { + ad(al); + if (al.parentNode) { + al.parentNode.removeChild(al); + } + } else { + for (var an = al.firstChild; an; an = an.nextSibling) { + ae(an); + } + } + break; + case 3: + case 4: + if (Z) { + var am = al.nodeValue; + var aj = am.match(ab); + if (aj) { + var ai = am.substring(0, aj.index); + al.nodeValue = ai; + var ah = am.substring(aj.index + aj[0].length); + if (ah) { + var ak = al.parentNode; + ak.insertBefore( + ac.createTextNode(ah), + al.nextSibling + ); + } + ad(al); + if (!ai) { + al.parentNode.removeChild(al); + } + } + } + break; + } + } + function ad(ak) { + while (!ak.nextSibling) { + ak = ak.parentNode; + if (!ak) { + return; + } + } + function ai(al, ar) { + var aq = ar ? al.cloneNode(false) : al; + var ao = al.parentNode; + if (ao) { + var ap = ai(ao, 1); + var an = al.nextSibling; + ap.appendChild(aq); + for (var am = an; am; am = an) { + an = am.nextSibling; + ap.appendChild(am); + } + } + return aq; + } + var ah = ai(ak.nextSibling, 0); + for (var aj; (aj = ah.parentNode) && aj.nodeType === 1; ) { + ah = aj; + } + W.push(ah); + } + for (var Y = 0; Y < W.length; ++Y) { + ae(W[Y]); + } + if (ag === (ag | 0)) { + W[0].setAttribute("value", ag); + } + var aa = ac.createElement("OL"); + aa.className = "linenums"; + var X = Math.max(0, (ag - 1) | 0) || 0; + for (var Y = 0, T = W.length; Y < T; ++Y) { + af = W[Y]; + af.className = "L" + ((Y + X) % 10); + if (!af.firstChild) { + af.appendChild(ac.createTextNode("\xA0")); + } + aa.appendChild(af); + } + V.appendChild(aa); + } + function D(ac) { + var aj = /\bMSIE\b/.test(navigator.userAgent); + var am = /\n/g; + var al = ac.sourceCode; + var an = al.length; + var V = 0; + var aa = ac.spans; + var T = aa.length; + var ah = 0; + var X = ac.decorations; + var Y = X.length; + var Z = 0; + X[Y] = an; + var ar, aq; + for (aq = ar = 0; aq < Y; ) { + if (X[aq] !== X[aq + 2]) { + X[ar++] = X[aq++]; + X[ar++] = X[aq++]; + } else { + aq += 2; + } + } + Y = ar; + for (aq = ar = 0; aq < Y; ) { + var at = X[aq]; + var ab = X[aq + 1]; + var W = aq + 2; + while (W + 2 <= Y && X[W + 1] === ab) { + W += 2; + } + X[ar++] = at; + X[ar++] = ab; + aq = W; + } + Y = X.length = ar; + var ae = null; + while (ah < T) { + var af = aa[ah]; + var S = aa[ah + 2] || an; + var ag = X[Z]; + var ap = X[Z + 2] || an; + var W = Math.min(S, ap); + var ak = aa[ah + 1]; + var U; + if (ak.nodeType !== 1 && (U = al.substring(V, W))) { + if (aj) { + U = U.replace(am, "\r"); + } + ak.nodeValue = U; + var ai = ak.ownerDocument; + var ao = ai.createElement("SPAN"); + ao.className = X[Z + 1]; + var ad = ak.parentNode; + ad.replaceChild(ao, ak); + ao.appendChild(ak); + if (V < S) { + aa[ah + 1] = ak = ai.createTextNode(al.substring(W, S)); + ad.insertBefore(ak, ao.nextSibling); + } + } + V = W; + if (V >= S) { + ah += 2; + } + if (V >= ap) { + Z += 2; + } + } + } + var t = {}; + function c(U, V) { + for (var S = V.length; --S >= 0; ) { + var T = V[S]; + if (!t.hasOwnProperty(T)) { + t[T] = U; + } else { + if (window.console) { + console.warn("cannot override language handler %s", T); + } + } + } + } + function q(T, S) { + if (!(T && t.hasOwnProperty(T))) { + T = /^\s*]*(?:>|$)/], + [j, /^<\!--[\s\S]*?(?:-\->|$)/], + ["lang-", /^<\?([\s\S]+?)(?:\?>|$)/], + ["lang-", /^<%([\s\S]+?)(?:%>|$)/], + [L, /^(?:<[%?]|[%?]>)/], + ["lang-", /^]*>([\s\S]+?)<\/xmp\b[^>]*>/i], + ["lang-js", /^]*>([\s\S]*?)(<\/script\b[^>]*>)/i], + ["lang-css", /^]*>([\s\S]*?)(<\/style\b[^>]*>)/i], + ["lang-in.tag", /^(<\/?[a-z][^<>]*>)/i] + ] + ), + ["default-markup", "htm", "html", "mxml", "xhtml", "xml", "xsl"] + ); + c( + g( + [ + [F, /^[\s]+/, null, " \t\r\n"], + [n, /^(?:\"[^\"]*\"?|\'[^\']*\'?)/, null, "\"'"] + ], + [ + [m, /^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i], + [P, /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i], + ["lang-uq.val", /^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/], + [L, /^[=<>\/]+/], + ["lang-js", /^on\w+\s*=\s*\"([^\"]+)\"/i], + ["lang-js", /^on\w+\s*=\s*\'([^\']+)\'/i], + ["lang-js", /^on\w+\s*=\s*([^\"\'>\s]+)/i], + ["lang-css", /^style\s*=\s*\"([^\"]+)\"/i], + ["lang-css", /^style\s*=\s*\'([^\']+)\'/i], + ["lang-css", /^style\s*=\s*([^\"\'>\s]+)/i] + ] + ), + ["in.tag"] + ); + c(g([], [[n, /^[\s\S]+/]]), ["uq.val"]); + c(i({ keywords: l, hashComments: true, cStyleComments: true, types: e }), [ + "c", + "cc", + "cpp", + "cxx", + "cyc", + "m" + ]); + c(i({ keywords: "null,true,false" }), ["json"]); + c( + i({ + keywords: R, + hashComments: true, + cStyleComments: true, + verbatimStrings: true, + types: e + }), + ["cs"] + ); + c(i({ keywords: x, cStyleComments: true }), ["java"]); + c(i({ keywords: H, hashComments: true, multiLineStrings: true }), [ + "bsh", + "csh", + "sh" + ]); + c( + i({ + keywords: I, + hashComments: true, + multiLineStrings: true, + tripleQuotedStrings: true + }), + ["cv", "py"] + ); + c( + i({ + keywords: s, + hashComments: true, + multiLineStrings: true, + regexLiterals: true + }), + ["perl", "pl", "pm"] + ); + c( + i({ + keywords: f, + hashComments: true, + multiLineStrings: true, + regexLiterals: true + }), + ["rb"] + ); + c(i({ keywords: w, cStyleComments: true, regexLiterals: true }), ["js"]); + c( + i({ + keywords: r, + hashComments: 3, + cStyleComments: true, + multilineStrings: true, + tripleQuotedStrings: true, + regexLiterals: true + }), + ["coffee"] + ); + c(g([], [[C, /^[\s\S]+/]]), ["regex"]); + function d(V) { + var U = V.langExtension; + try { + var S = a(V.sourceNode); + var T = S.sourceCode; + V.sourceCode = T; + V.spans = S.spans; + V.basePos = 0; + q(U, T)(V); + D(V); + } catch (W) { + if ("console" in window) { + console.log(W && W.stack ? W.stack : W); + } + } + } + function y(W, V, U) { + var S = document.createElement("PRE"); + S.innerHTML = W; + if (U) { + Q(S, U); + } + var T = { langExtension: V, numberLines: U, sourceNode: S }; + d(T); + return S.innerHTML; + } + function b(ad) { + function Y(af) { + return document.getElementsByTagName(af); + } + var ac = [Y("pre"), Y("code"), Y("xmp")]; + var T = []; + for (var aa = 0; aa < ac.length; ++aa) { + for (var Z = 0, V = ac[aa].length; Z < V; ++Z) { + T.push(ac[aa][Z]); + } + } + ac = null; + var W = Date; + if (!W.now) { + W = { + now: function () { + return +new Date(); + } + }; + } + var X = 0; + var S; + var ab = /\blang(?:uage)?-([\w.]+)(?!\S)/; + var ae = /\bprettyprint\b/; + function U() { + var ag = window.PR_SHOULD_USE_CONTINUATION + ? W.now() + 250 + : Infinity; + for (; X < T.length && W.now() < ag; X++) { + var aj = T[X]; + var ai = aj.className; + if (ai.indexOf("prettyprint") >= 0) { + var ah = ai.match(ab); + var am; + if (!ah && (am = o(aj)) && "CODE" === am.tagName) { + ah = am.className.match(ab); + } + if (ah) { + ah = ah[1]; + } + var al = false; + for (var ak = aj.parentNode; ak; ak = ak.parentNode) { + if ( + (ak.tagName === "pre" || + ak.tagName === "code" || + ak.tagName === "xmp") && + ak.className && + ak.className.indexOf("prettyprint") >= 0 + ) { + al = true; + break; + } + } + if (!al) { + var af = aj.className.match(/\blinenums\b(?::(\d+))?/); + af = af + ? af[1] && af[1].length + ? +af[1] + : true + : false; + if (af) { + Q(aj, af); + } + S = { + langExtension: ah, + sourceNode: aj, + numberLines: af + }; + d(S); + } + } + } + if (X < T.length) { + setTimeout(U, 250); + } else { + if (ad) { + ad(); + } + } + } + U(); + } + window.prettyPrintOne = y; + window.prettyPrint = b; + window.PR = { + createSimpleLexer: g, + registerLangHandler: c, + sourceDecorator: i, + PR_ATTRIB_NAME: P, + PR_ATTRIB_VALUE: n, + PR_COMMENT: j, + PR_DECLARATION: E, + PR_KEYWORD: z, + PR_LITERAL: G, + PR_NOCODE: N, + PR_PLAIN: F, + PR_PUNCTUATION: L, + PR_SOURCE: J, + PR_STRING: C, + PR_TAG: m, + PR_TYPE: O + }; +})(); +PR.registerLangHandler( + PR.createSimpleLexer( + [], + [ + [PR.PR_DECLARATION, /^]*(?:>|$)/], + [PR.PR_COMMENT, /^<\!--[\s\S]*?(?:-\->|$)/], + [PR.PR_PUNCTUATION, /^(?:<[%?]|[%?]>)/], + ["lang-", /^<\?([\s\S]+?)(?:\?>|$)/], + ["lang-", /^<%([\s\S]+?)(?:%>|$)/], + ["lang-", /^]*>([\s\S]+?)<\/xmp\b[^>]*>/i], + [ + "lang-handlebars", + /^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i + ], + ["lang-js", /^]*>([\s\S]*?)(<\/script\b[^>]*>)/i], + ["lang-css", /^]*>([\s\S]*?)(<\/style\b[^>]*>)/i], + ["lang-in.tag", /^(<\/?[a-z][^<>]*>)/i], + [PR.PR_DECLARATION, /^{{[#^>/]?\s*[\w.][^}]*}}/], + [PR.PR_DECLARATION, /^{{&?\s*[\w.][^}]*}}/], + [PR.PR_DECLARATION, /^{{{>?\s*[\w.][^}]*}}}/], + [PR.PR_COMMENT, /^{{![^}]*}}/] + ] + ), + ["handlebars", "hbs"] +); +PR.registerLangHandler( + PR.createSimpleLexer( + [[PR.PR_PLAIN, /^[ \t\r\n\f]+/, null, " \t\r\n\f"]], + [ + [ + PR.PR_STRING, + /^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/, + null + ], + [ + PR.PR_STRING, + /^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/, + null + ], + ["lang-css-str", /^url\(([^\)\"\']*)\)/i], + [ + PR.PR_KEYWORD, + /^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i, + null + ], + [ + "lang-css-kw", + /^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i + ], + [PR.PR_COMMENT, /^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//], + [PR.PR_COMMENT, /^(?:)/], + [PR.PR_LITERAL, /^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i], + [PR.PR_LITERAL, /^#(?:[0-9a-f]{3}){1,2}/i], + [ + PR.PR_PLAIN, + /^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i + ], + [PR.PR_PUNCTUATION, /^[^\s\w\'\"]+/] + ] + ), + ["css"] +); +PR.registerLangHandler( + PR.createSimpleLexer( + [], + [ + [ + PR.PR_KEYWORD, + /^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i + ] + ] + ), + ["css-kw"] +); +PR.registerLangHandler( + PR.createSimpleLexer([], [[PR.PR_STRING, /^[^\)\"\']+/]]), + ["css-str"] +); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js new file mode 100644 index 000000000..aa007d25f --- /dev/null +++ b/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function () { + "use strict"; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector(".coverage-summary"); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector("thead tr"); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector("tbody"); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll("th")[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById("fileSearch").value; + const rows = document.getElementsByTagName("tbody")[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, "i"); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? "" : "none"; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById("filterTemplate"); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById("fileSearch").oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll("th"), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute("data-col"), + sortable: !colNode.getAttribute("data-nosort"), + type: colNode.getAttribute("data-type") || "string" + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === "number"; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll("td"), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute("data-value"); + if (col.type === "number") { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll("tr"), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function (a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector(".coverage-summary tbody"), + rowNodes = tableBody.querySelectorAll("tr"), + rows = [], + i; + + if (desc) { + finalSorter = function (a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, "").replace(/ sorted-desc$/, ""); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? " sorted-desc" + : " sorted"; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function () { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector(".sorter").parentElement; + if (el.addEventListener) { + el.addEventListener("click", ithSorter(i)); + } else { + el.attachEvent("onclick", ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function () { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener("load", addSorting); diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 000000000..e69de29bb diff --git a/drizzle/0003_favorites_add_name.sql b/drizzle/0003_favorites_add_name.sql new file mode 100644 index 000000000..b7915df13 --- /dev/null +++ b/drizzle/0003_favorites_add_name.sql @@ -0,0 +1 @@ +ALTER TABLE `favorites` ADD `name` text NOT NULL DEFAULT ''; diff --git a/package-lock.json b/package-lock.json index 8fc6abacd..6ae0ed262 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", "vite": "^8.0.10", - "vitest": "^4.1.9", + "vitest": "^4.1.10", "wrangler": "^4.93.0" } }, @@ -4239,16 +4239,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -4257,13 +4257,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -4284,9 +4284,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4297,13 +4297,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.10", "pathe": "^2.0.3" }, "funding": { @@ -4311,14 +4311,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -4327,9 +4327,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -4337,13 +4337,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", + "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -8001,19 +8001,19 @@ } }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -8041,12 +8041,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index df35fb423..6c7240320 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,16 @@ "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", "vite": "^8.0.10", - "vitest": "^4.1.9", + "vitest": "^4.1.10", "wrangler": "^4.93.0" + }, + "allowScripts": { + "esbuild@0.28.1": true, + "esbuild@0.25.12": true, + "esbuild@0.27.3": true, + "esbuild@0.18.20": true, + "sharp@0.34.5": true, + "workerd@1.20260616.1": true, + "@parcel/watcher@2.5.6": true } } diff --git a/src/__test_utils__/seed.ts b/src/__test_utils__/seed.ts index d024acfeb..75855b15f 100644 --- a/src/__test_utils__/seed.ts +++ b/src/__test_utils__/seed.ts @@ -150,7 +150,8 @@ export async function seedFavorite( db: Db, insertableId: string, userId: string = TEST_USER_ID, - sortOrder = 0 + sortOrder = 0, + name = "test-favorite" ): Promise { const id = crypto.randomUUID(); await seedUser(db, userId); @@ -161,6 +162,7 @@ export async function seedFavorite( userId, libraryId: TEST_LIBRARY_ID, insertableId, + name, sortOrder }) .onConflictDoNothing(); diff --git a/src/backend/routes/favorites.test.ts b/src/backend/routes/favorites.test.ts index 01c398ef0..a3ca4ba5d 100644 --- a/src/backend/routes/favorites.test.ts +++ b/src/backend/routes/favorites.test.ts @@ -74,7 +74,7 @@ describe("favorites routes", () => { const app = createTestApp(); const res = await app.request( - `${favoritesUrl}?insertableId=${TEST_ASSEMBLY_ID}&id=fav-new`, + `${favoritesUrl}?insertableId=${TEST_ASSEMBLY_ID}&id=fav-new&name=fav-new`, jsonRequest("POST"), env ); @@ -196,7 +196,7 @@ describe("favorites routes", () => { }); }); - describe("POST /default-configuration/:favoriteId", () => { + describe("POST /favorite/:favoriteId", () => { it("persists the default configuration", async () => { await seedPartStudio(db); const favoriteId = await seedFavorite(db, TEST_PART_STUDIO_ID); @@ -204,7 +204,7 @@ describe("favorites routes", () => { const defaultConfiguration = { "param-id": "value" }; const res = await app.request( - `/api/default-configuration/${favoriteId}`, + `/api/favorites/${favoriteId}`, jsonRequest("POST", { defaultConfiguration }), env ); diff --git a/src/backend/routes/favorites.ts b/src/backend/routes/favorites.ts index 95e9f9045..47f3ef772 100644 --- a/src/backend/routes/favorites.ts +++ b/src/backend/routes/favorites.ts @@ -32,7 +32,8 @@ async function getFavorites( id: row.id, insertableId: row.insertableId, libraryId, - defaultConfiguration: row.defaultConfiguration ?? undefined + defaultConfiguration: row.defaultConfiguration ?? undefined, + name: row.name || undefined }; favoritesOut[row.id] = fav; favoriteOrder.push(row.id); @@ -58,9 +59,10 @@ favoriteRoutes.post("/favorites" + libraryRoute(), async (c) => { const userId = await c.var.getUserId(); const insertableId = c.req.query("insertableId"); const favoriteId = c.req.query("id"); + const name = c.req.query("name"); if (!insertableId) return c.json({ error: "insertableId required" }, 400); if (!favoriteId) return c.json({ error: "id required" }, 400); - + if (!name) return c.json({ error: "name required" }, 400); const db = getDb(c.env.DB); await db.insert(users).values({ id: userId }).onConflictDoNothing(); @@ -83,6 +85,7 @@ favoriteRoutes.post("/favorites" + libraryRoute(), async (c) => { userId, libraryId, insertableId, + name, sortOrder: existingCount.length }) .onConflictDoNothing(); @@ -126,17 +129,41 @@ favoriteRoutes.post("/favorite-order" + libraryRoute(), async (c) => { return c.json({ success: true }); }); -/** POST /api/default-configuration/:favoriteId */ -favoriteRoutes.post("/default-configuration/:favoriteId", async (c) => { +/** POST /api/favorite/:favoriteId */ +favoriteRoutes.post("/favorite/:favoriteId", async (c) => { const favoriteId = c.req.param("favoriteId"); const body = await c.req.json<{ - defaultConfiguration: Configuration; + defaultConfiguration?: Configuration; + name?: string; }>(); + if (!favoriteId) { + return c.json({ error: "favoriteId is required" }, 400); + } + + const updateValues: { + defaultConfiguration?: Configuration | null; + name?: string; + } = {}; + + if (body.defaultConfiguration !== undefined) { + updateValues.defaultConfiguration = body.defaultConfiguration; + } + if (body.name !== undefined) { + updateValues.name = body.name; + } + + if (Object.keys(updateValues).length === 0) { + return c.json( + { error: "defaultConfiguration or name is required" }, + 400 + ); + } + const db = getDb(c.env.DB); await db .update(favorites) - .set({ defaultConfiguration: body.defaultConfiguration }) + .set(updateValues) .where(eq(favorites.id, favoriteId)); return c.json({ success: true }); diff --git a/src/frontend/cards/card-components.tsx b/src/frontend/cards/card-components.tsx index a773b4842..f022902c4 100644 --- a/src/frontend/cards/card-components.tsx +++ b/src/frontend/cards/card-components.tsx @@ -130,28 +130,40 @@ interface CardTitleProps { title: string; searchHit?: SearchHit; thumbnailUrls: ThumbnailUrls; + /** Optional title override rendered instead of the default title text. */ + titleComponent?: ReactNode; /** Optional build-status badge rendered after the title. */ buildStatusBadge?: ReactNode; } export function CardTitle(props: CardTitleProps) { - const { searchHit, title, thumbnailUrls, buildStatusBadge } = props; + const { + searchHit, + title, + titleComponent, + thumbnailUrls, + buildStatusBadge + } = props; const disabled = props.disabled ?? false; const isHidden = props.showHiddenTag ?? false; let cardTitle: ReactNode; - if (searchHit) { + if (titleComponent) { + cardTitle = titleComponent; + } else if (searchHit) { cardTitle = ; } else { - cardTitle = title; + cardTitle = ( + + {title} + + ); } return ( - - {cardTitle} - + {cardTitle} {isHidden && ( { + const trimmedName = favoriteName.trim(); + if (trimmedName.length === 0) { + setFavoriteName(defaultTitle); + return; + } + if (trimmedName === defaultTitle) return; + + setFavoriteName(trimmedName); + updateFavoriteNameMutation.mutate({ + favoriteId: favorite.id, + name: trimmedName + }); + }; + return ( { @@ -68,7 +91,32 @@ export function FavoriteCard(props: FavoriteCardProps): ReactNode { left={ + setFavoriteName(event.currentTarget.value) + } + onBlur={saveFavoriteName} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.currentTarget.blur(); + } + }} + onClick={(event) => event.stopPropagation()} + onFocus={(event) => event.stopPropagation()} + styles={{ input: { minWidth: 0 } }} + style={{ + width: "50%", + maxWidth: "100%", + minWidth: 0 + }} + /> + } thumbnailUrls={insertable.thumbnailUrls} searchHit={searchHit} /> @@ -117,7 +165,8 @@ function FavoriteMenuItems(props: FavoriteMenuItemsProps): ReactNode { openFavoriteMenu({ favoriteId: favorite.id, insertableName: insertable.name, - defaultConfiguration: favorite.defaultConfiguration + defaultConfiguration: favorite.defaultConfiguration, + favorite: favorite }); }} > @@ -179,3 +228,39 @@ function useSetFavoriteOrderMutation() { } }); } + +function useUpdateFavoriteNameMutation() { + const libraryId = useLibraryId(); + const router = useRouter(); + + const queryKey = favoritesQueryKey(libraryId); + + return useMutation({ + mutationKey: ["update-favorite-name"], + mutationFn: async (args: { favoriteId: string; name: string }) => { + return apiPost("/favorite/" + args.favoriteId, { + body: { name: args.name } + }); + }, + onMutate: async (args) => { + await queryClient.cancelQueries({ queryKey }); + queryClient.setQueryData( + queryKey, + produce((data?: FavoritesData) => { + if (!data) return undefined; + const fav = data.favorites[args.favoriteId]; + if (fav) { + fav.name = args.name; + } + return data; + }) + ); + void router.invalidate(); + }, + onError: getAppErrorHandler("Unexpectedly failed to rename favorite."), + onSettled: async () => { + await queryClient.invalidateQueries({ queryKey }); + void router.invalidate(); + } + }); +} diff --git a/src/frontend/favorites/favorite-menu.tsx b/src/frontend/favorites/favorite-menu.tsx index 2a021f5cc..3b17c2b89 100644 --- a/src/frontend/favorites/favorite-menu.tsx +++ b/src/frontend/favorites/favorite-menu.tsx @@ -9,7 +9,7 @@ import { apiPost } from "../api-utils/api"; import { showErrorToast, showSuccessToast } from "../common/notifications"; import { PreviewImageCard } from "../insert/thumbnail"; import { ConfigurationWrapper } from "../insert/configurations"; -import { type FavoritesData } from "../../shared/api-models"; +import { Favorite, type FavoritesData } from "../../shared/api-models"; import { HeartIcon } from "./favorite-button"; import { queryClient } from "../query-client"; import { Configuration } from "../../shared/configuration-models"; @@ -26,15 +26,17 @@ interface OpenFavoriteMenuProps { favoriteId: string; insertableName: string; defaultConfiguration?: Configuration; + favorite: Favorite; } export function openFavoriteMenu(props: OpenFavoriteMenuProps) { - const { favoriteId, insertableName, defaultConfiguration } = props; + const { favoriteId, insertableName, favorite, defaultConfiguration } = + props; modals.open({ title: ( - {insertableName} + {favorite.name ?? insertableName} ), size: 500, diff --git a/src/shared/api-models.ts b/src/shared/api-models.ts index 2235854dc..77285f32d 100644 --- a/src/shared/api-models.ts +++ b/src/shared/api-models.ts @@ -68,6 +68,7 @@ export interface Favorite { insertableId: string; libraryId: LibraryId; defaultConfiguration?: Configuration; + name?: string; } export interface FavoritesData { diff --git a/src/shared/schema.ts b/src/shared/schema.ts index 02afb7338..097bb46b0 100644 --- a/src/shared/schema.ts +++ b/src/shared/schema.ts @@ -143,9 +143,11 @@ export const favorites = sqliteTable( insertableId: text("insertable_id") .notNull() .references(() => insertables.id, { onDelete: "cascade" }), + defaultConfiguration: text("default_configuration", { mode: "json" }).$type(), + name: text("name").notNull().default(""), sortOrder: integer("sort_order").notNull().default(0) }, (t) => [unique().on(t.userId, t.libraryId, t.insertableId)] From d63b3d908b72d136abf440360ca864355a64aed5 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Jul 2026 16:06:05 -0400 Subject: [PATCH 02/10] modified: src/frontend/favorites/favorite-card.tsx new file: src/frontend/favorites/favorite-search.test.ts new file: src/frontend/favorites/favorite-search.ts modified: src/frontend/favorites/favorites-list.tsx modified: src/shared/search.ts --- src/frontend/favorites/favorite-card.tsx | 59 +++++----- .../favorites/favorite-search.test.ts | 53 +++++++++ src/frontend/favorites/favorite-search.ts | 109 ++++++++++++++++++ src/frontend/favorites/favorites-list.tsx | 87 +++++++------- 4 files changed, 237 insertions(+), 71 deletions(-) create mode 100644 src/frontend/favorites/favorite-search.test.ts create mode 100644 src/frontend/favorites/favorite-search.ts diff --git a/src/frontend/favorites/favorite-card.tsx b/src/frontend/favorites/favorite-card.tsx index de5ec9961..9d4ef6f12 100644 --- a/src/frontend/favorites/favorite-card.tsx +++ b/src/frontend/favorites/favorite-card.tsx @@ -33,6 +33,7 @@ import { getAppErrorHandler } from "../api-utils/errors"; import { favoritesQueryKey, useFavoritesQuery } from "../queries"; import { produce } from "immer"; import { SearchHit } from "../search/search"; +import { SearchHitTitle } from "../search/search-results"; import { toLibraryPath, useLibraryId } from "../api-utils/library"; interface FavoriteCardProps { @@ -48,6 +49,8 @@ interface FavoriteCardProps { export function FavoriteCard(props: FavoriteCardProps): ReactNode { const { insertable, favorite, searchHit } = props; + const uiState = useUiState()[0]; + const isHidden = useIsInsertableHidden(insertable); const isAssemblyInPartStudio = useIsAssemblyInPartStudio( insertable.elementType @@ -76,6 +79,34 @@ export function FavoriteCard(props: FavoriteCardProps): ReactNode { }); }; + const isSearchActive = Boolean(uiState.searchQuery && searchHit); + const titleComponent = + isSearchActive && searchHit ? ( + + ) : ( + setFavoriteName(event.currentTarget.value)} + onBlur={saveFavoriteName} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.currentTarget.blur(); + } + }} + onClick={(event) => event.stopPropagation()} + onFocus={(event) => event.stopPropagation()} + styles={{ input: { minWidth: 0 } }} + style={{ + width: "50%", + maxWidth: "100%", + minWidth: 0 + }} + /> + ); + return ( { @@ -91,32 +122,8 @@ export function FavoriteCard(props: FavoriteCardProps): ReactNode { left={ - setFavoriteName(event.currentTarget.value) - } - onBlur={saveFavoriteName} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.currentTarget.blur(); - } - }} - onClick={(event) => event.stopPropagation()} - onFocus={(event) => event.stopPropagation()} - styles={{ input: { minWidth: 0 } }} - style={{ - width: "50%", - maxWidth: "100%", - minWidth: 0 - }} - /> - } + title={favoriteName} + titleComponent={titleComponent} thumbnailUrls={insertable.thumbnailUrls} searchHit={searchHit} /> diff --git a/src/frontend/favorites/favorite-search.test.ts b/src/frontend/favorites/favorite-search.test.ts new file mode 100644 index 000000000..406e261b2 --- /dev/null +++ b/src/frontend/favorites/favorite-search.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { LibraryId } from "../../shared/types"; +import type { Favorite } from "../../shared/api-models"; +import { filterFavoritesForSearch } from "./favorite-search"; + +describe("filterFavoritesForSearch", () => { + it("matches favorites by their own name and id instead of the insertable name or id", () => { + const favorites: Favorite[] = [ + { + id: "fav-1", + insertableId: "insertable-1", + libraryId: LibraryId.FRC_DESIGN_LIB, + name: "My Favorite Wheel" + }, + { + id: "custom-favorite-id", + insertableId: "insertable-2", + libraryId: LibraryId.FRC_DESIGN_LIB, + name: "Unrelated" + } + ]; + + expect( + filterFavoritesForSearch(favorites, "wheel").map( + (result) => result.favorite.id + ) + ).toEqual(["fav-1"]); + + expect( + filterFavoritesForSearch( + [ + { + id: "fav-2", + insertableId: "insertable-3", + libraryId: LibraryId.FRC_DESIGN_LIB, + name: "Linear (REV)" + } + ], + "linear rev" + ).map((result) => result.favorite.id) + ).toEqual(["fav-2"]); + + expect( + filterFavoritesForSearch(favorites, "custom").map( + (result) => result.favorite.id + ) + ).toEqual(["custom-favorite-id"]); + + expect( + filterFavoritesForSearch(favorites, "different-insertable") + ).toEqual([]); + }); +}); diff --git a/src/frontend/favorites/favorite-search.ts b/src/frontend/favorites/favorite-search.ts new file mode 100644 index 000000000..fae25a2f3 --- /dev/null +++ b/src/frontend/favorites/favorite-search.ts @@ -0,0 +1,109 @@ +import MiniSearch, { + Options, + SearchResult as MiniSearchResult +} from "minisearch"; +import { Favorite } from "../../shared/api-models"; +import { processTerm, tokenize } from "../../shared/search"; +import { Position, SearchHit } from "../search/search"; + +interface FavoriteSearchDocument { + id: string; + name: string; + favoriteId: string; +} + +const FAVORITE_SEARCH_OPTIONS: Options = { + fields: ["name", "favoriteId"], + storeFields: ["id", "name"], + searchOptions: { + prefix: true + }, + tokenize, + processTerm +}; + +function buildFavoriteSearchDb( + favorites: Favorite[] +): MiniSearch { + const searchDb = new MiniSearch( + FAVORITE_SEARCH_OPTIONS + ); + + searchDb.addAll( + favorites.map((favorite) => ({ + id: favorite.id, + name: favorite.name ?? "", + favoriteId: favorite.id + })) + ); + + return searchDb; +} + +function generateHighlightPositions( + result: MiniSearchResult, + document: FavoriteSearchDocument +): Position[] { + const name = document.name.toLowerCase(); + const positions: Position[] = []; + + for (const [term, matchedFields] of Object.entries(result.match)) { + if (!matchedFields.includes("name")) { + continue; + } + const matchedLocations = name.matchAll(new RegExp(`(${term})`, "gi")); + for (const match of matchedLocations) { + positions.push({ + start: match.index ?? 0, + length: term.length + }); + } + } + + return positions; +} + +export interface FavoriteSearchResult { + favorite: Favorite; + searchHit: SearchHit | undefined; +} + +export function filterFavoritesForSearch( + favorites: Favorite[], + query?: string +): FavoriteSearchResult[] { + if (!query || query.trim() === "") { + return favorites.map((favorite) => ({ + favorite, + searchHit: undefined + })); + } + + const searchDb = buildFavoriteSearchDb(favorites); + const miniSearchResults = searchDb.search(query); + + const results: FavoriteSearchResult[] = []; + + for (const miniSearchResult of miniSearchResults) { + const document = searchDb.getStoredFields( + miniSearchResult.id + ) as unknown as FavoriteSearchDocument; + const favorite = favorites.find((item) => item.id === document.id); + if (!favorite) { + continue; + } + + results.push({ + favorite, + searchHit: { + id: favorite.id, + positions: generateHighlightPositions( + miniSearchResult, + document + ) + } + }); + } + + return results; +} diff --git a/src/frontend/favorites/favorites-list.tsx b/src/frontend/favorites/favorites-list.tsx index f6553e012..57cc9b041 100644 --- a/src/frontend/favorites/favorites-list.tsx +++ b/src/frontend/favorites/favorites-list.tsx @@ -1,7 +1,6 @@ import { IconHeartBroken } from "@tabler/icons-react"; import { HeartIconColor, IconSize } from "../common/style-constants"; import { ReactNode } from "react"; -import { useLoaderData } from "@tanstack/react-router"; import { filterInsertables } from "../search/filter"; import { getFavoriteForInsertable, @@ -12,13 +11,12 @@ import { SectionError, SectionLoading } from "../app-common/app-zero-state"; import { NoSearchResultError, SearchCallout } from "../search/search-errors"; import { FavoriteCard } from "./favorite-card"; import { ItemTable } from "../cards/card-components"; +import { useFavoritesQuery, useLibraryQuery } from "../queries"; +import { FilterResult, SearchHit } from "../search/search"; import { - useFavoritesQuery, - useLibraryQuery, - useSearchDbQuery -} from "../queries"; -import { doSearch, FilterResult, SearchHit } from "../search/search"; -import { hasEditorAccess } from "../../shared/types"; + FavoriteSearchResult, + filterFavoritesForSearch +} from "./favorite-search"; /** * A list of current favorite cards. @@ -26,23 +24,13 @@ import { hasEditorAccess } from "../../shared/types"; */ export function FavoritesList(): ReactNode { const uiState = useUiState()[0]; - const loaderData = useLoaderData({ from: "/app" }); const favoritesQuery = useFavoritesQuery(); const libraryQuery = useLibraryQuery(); - const searchDbQuery = useSearchDbQuery(); - if ( - libraryQuery.isPending || - searchDbQuery.isPending || - favoritesQuery.isPending - ) { + if (libraryQuery.isPending || favoritesQuery.isPending) { return ; - } else if ( - libraryQuery.isError || - searchDbQuery.isError || - favoritesQuery.isError - ) { + } else if (libraryQuery.isError || favoritesQuery.isError) { return ( = {}; if (uiState.searchQuery) { - if (!searchDbQuery.data) { - return ; - } - const favoriteInsertableIds = new Set( - Object.values(favoritesQuery.data.favorites).map( - (f) => f.insertableId - ) + const matchedFavorites = filterFavoritesForSearch( + orderedFavorites, + uiState.searchQuery ); - const searchResults = doSearch( - searchDbQuery.data, - uiState.searchQuery, + + const matchedInsertables = matchedFavorites + .map((result: FavoriteSearchResult) => { + const insertable = insertables[result.favorite.insertableId]; + return insertable ? { insertable, result } : undefined; + }) + .filter( + ( + item + ): item is { + insertable: InsertableOut; + result: FavoriteSearchResult; + } => !!item + ); + + const filterResult2 = filterInsertables( + matchedInsertables.map((item) => item.insertable), { vendors: uiState.vendorFilters, - isFavorite: true - }, - favoriteInsertableIds, - hasEditorAccess(loaderData.accessData.currentAccessLevel) + isVisible: true + } ); - filteredInsertables = searchResults.hits - .map((hit) => insertables[hit.id]) - .filter((insertable) => !!insertable); - - filterResult = searchResults.filtered; - - searchHits = searchResults.hits.reduce( - (acc, hit) => { - acc[hit.id] = hit; - return acc; - }, - {} as Record + filteredInsertables = filterResult2.insertables; + filterResult = filterResult2.filtered; + searchHits = Object.fromEntries( + matchedInsertables + .filter((item) => + filteredInsertables.some( + (insertable) => insertable.id === item.insertable.id + ) + ) + .flatMap((item) => { + const searchHit = item.result.searchHit; + return searchHit ? [[item.insertable.id, searchHit]] : []; + }) ); } else { const filterResult2 = filterInsertables(favoriteInsertables, { From 0623a96a6d75a1724f13276c6a9ea75e02e57f36 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 9 Jul 2026 13:23:19 -0400 Subject: [PATCH 03/10] fixed a small route error modified: src/frontend/favorites/favorite-menu.tsx --- src/frontend/favorites/favorite-menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/favorites/favorite-menu.tsx b/src/frontend/favorites/favorite-menu.tsx index 3b17c2b89..9b6f476a0 100644 --- a/src/frontend/favorites/favorite-menu.tsx +++ b/src/frontend/favorites/favorite-menu.tsx @@ -70,7 +70,7 @@ function FavoriteMenuContent(props: FavoriteMenuContentProps): ReactNode { const setDefaultConfigurationMutation = useMutation({ mutationKey: ["set-default-configuration"], mutationFn: async () => { - return apiPost("/default-configuration/" + favoriteId, { + return apiPost("/favorite/" + favoriteId, { body: { defaultConfiguration: configuration } }); }, From e62f8e5c7b90c7ff3cb02f43a082ab7ade1b4fda Mon Sep 17 00:00:00 2001 From: James Date: Fri, 10 Jul 2026 10:53:45 -0400 Subject: [PATCH 04/10] Fixed a error causing a test to fail modified: src/backend/routes/favorites.test.ts modified: src/backend/routes/favorites.ts --- src/backend/routes/favorites.test.ts | 2 +- src/backend/routes/favorites.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/routes/favorites.test.ts b/src/backend/routes/favorites.test.ts index a3ca4ba5d..2bd241ac5 100644 --- a/src/backend/routes/favorites.test.ts +++ b/src/backend/routes/favorites.test.ts @@ -110,7 +110,7 @@ describe("favorites routes", () => { it("is idempotent on a conflicting id", async () => { await seedPartStudio(db); const app = createTestApp(); - const url = `${favoritesUrl}?insertableId=${TEST_PART_STUDIO_ID}&id=dup`; + const url = `${favoritesUrl}?insertableId=${TEST_PART_STUDIO_ID}&id=dup&name=dup`; await app.request(url, jsonRequest("POST"), env); await app.request(url, jsonRequest("POST"), env); diff --git a/src/backend/routes/favorites.ts b/src/backend/routes/favorites.ts index 47f3ef772..f73521b54 100644 --- a/src/backend/routes/favorites.ts +++ b/src/backend/routes/favorites.ts @@ -129,8 +129,8 @@ favoriteRoutes.post("/favorite-order" + libraryRoute(), async (c) => { return c.json({ success: true }); }); -/** POST /api/favorite/:favoriteId */ -favoriteRoutes.post("/favorite/:favoriteId", async (c) => { +/** POST /api/favorites/:favoriteId */ +favoriteRoutes.post("/favorites/:favoriteId", async (c) => { const favoriteId = c.req.param("favoriteId"); const body = await c.req.json<{ defaultConfiguration?: Configuration; From 2c92c16b3c0dc2eeaa435378e3243038bf8b88ff Mon Sep 17 00:00:00 2001 From: Alex Kempen Date: Sun, 12 Jul 2026 19:08:45 -0500 Subject: [PATCH 05/10] Remove favorite logic from doSearch --- src/frontend/search/search-results.tsx | 1 - src/frontend/search/search.ts | 8 -------- 2 files changed, 9 deletions(-) diff --git a/src/frontend/search/search-results.tsx b/src/frontend/search/search-results.tsx index a1fcc7e42..162469f63 100644 --- a/src/frontend/search/search-results.tsx +++ b/src/frontend/search/search-results.tsx @@ -37,7 +37,6 @@ export function SearchResults(props: SearchResultsProps): ReactNode { searchDbQuery.data, query, filters, - undefined, hasEditorAccess(loaderData.accessData.currentAccessLevel) ); diff --git a/src/frontend/search/search.ts b/src/frontend/search/search.ts index 0d9ed5797..b674b80fd 100644 --- a/src/frontend/search/search.ts +++ b/src/frontend/search/search.ts @@ -17,7 +17,6 @@ export function plural(objectLabel: ObjectLabel): string { export interface SearchFilters { groupId?: string; vendors?: Vendor[]; - isFavorite?: boolean; } // Range is already defined by TypeScript @@ -52,7 +51,6 @@ export function doSearch( searchDb: MiniSearch, query?: string, filters?: SearchFilters, - favoritedInsertableIds?: Set, showHidden?: boolean ): SearchResult { const filtered: FilterResult = { byVendor: 0, byGroup: 0 }; @@ -67,12 +65,6 @@ export function doSearch( return false; } - if (filters?.isFavorite) { - if (!favoritedInsertableIds?.has(searchResult.id)) { - return false; - } - } - let filteredByGroup = false; let filteredByVendor = false; if (filters?.groupId && searchResult.groupId !== filters.groupId) { From ab47ee65a7d03da6dff48274a829ed60a5916f07 Mon Sep 17 00:00:00 2001 From: Alex Kempen Date: Sun, 12 Jul 2026 19:55:44 -0500 Subject: [PATCH 06/10] Refactor how search results are arranged --- src/backend/library-data.ts | 2 +- src/frontend/cards/card-components.tsx | 2 +- src/frontend/cards/insertable-card.tsx | 2 +- src/frontend/favorites/favorite-card.tsx | 2 +- src/frontend/favorites/favorite-search.ts | 44 ++++++++-------- src/frontend/favorites/favorites-list.tsx | 8 +-- src/frontend/queries.ts | 2 +- src/frontend/search/filter.ts | 2 +- .../{search.ts => insertable-search.ts} | 51 ++++++------------- src/frontend/search/search-errors.tsx | 14 ++++- src/frontend/search/search-results.tsx | 7 ++- .../{search.ts => build-insertable-search.ts} | 32 +----------- .../search-utils.test.ts} | 3 +- src/shared/search-utils.ts | 42 +++++++++++++++ 14 files changed, 110 insertions(+), 103 deletions(-) rename src/frontend/search/{search.ts => insertable-search.ts} (72%) rename src/shared/{search.ts => build-insertable-search.ts} (62%) rename src/{frontend/search/search.test.ts => shared/search-utils.test.ts} (91%) create mode 100644 src/shared/search-utils.ts diff --git a/src/backend/library-data.ts b/src/backend/library-data.ts index 1d1982728..3d7e7fe1a 100644 --- a/src/backend/library-data.ts +++ b/src/backend/library-data.ts @@ -13,7 +13,7 @@ import { Insertables, Groups } from "../shared/api-models"; -import { buildSearchDb } from "../shared/search"; +import { buildSearchDb } from "../shared/build-insertable-search"; /** * Assembles the full `LibraryOut` (groups + insertables, in sort order) for a diff --git a/src/frontend/cards/card-components.tsx b/src/frontend/cards/card-components.tsx index f022902c4..b6a4360d1 100644 --- a/src/frontend/cards/card-components.tsx +++ b/src/frontend/cards/card-components.tsx @@ -11,7 +11,7 @@ import { IconColor, IconSize } from "../common/style-constants"; import { copyUrlToClipboard, makeUrl, openUrlInNewTab } from "../common/url"; import { PropsWithChildren, ReactNode, useCallback } from "react"; import { AppContextMenu, MenuButton } from "../app-common/app-menu"; -import { SearchHit } from "../search/search"; +import { SearchHit } from "../search/insertable-search"; import { SearchHitTitle } from "../search/search-results"; import { CardThumbnail } from "../insert/thumbnail"; import { ConfigurablePath, InstancePath } from "../../shared/onshape-path"; diff --git a/src/frontend/cards/insertable-card.tsx b/src/frontend/cards/insertable-card.tsx index c05bde4dd..d072f2e52 100644 --- a/src/frontend/cards/insertable-card.tsx +++ b/src/frontend/cards/insertable-card.tsx @@ -16,7 +16,7 @@ import { } from "../../shared/api-models"; import { Configuration } from "../../shared/configuration-models"; import { ElementType } from "../../shared/types"; -import { SearchHit } from "../search/search"; +import { SearchHit } from "../search/insertable-search"; import { FavoriteButton, FavoriteInsertableItem diff --git a/src/frontend/favorites/favorite-card.tsx b/src/frontend/favorites/favorite-card.tsx index 9d4ef6f12..65a6fe46d 100644 --- a/src/frontend/favorites/favorite-card.tsx +++ b/src/frontend/favorites/favorite-card.tsx @@ -32,7 +32,7 @@ import { import { getAppErrorHandler } from "../api-utils/errors"; import { favoritesQueryKey, useFavoritesQuery } from "../queries"; import { produce } from "immer"; -import { SearchHit } from "../search/search"; +import { SearchHit } from "../search/insertable-search"; import { SearchHitTitle } from "../search/search-results"; import { toLibraryPath, useLibraryId } from "../api-utils/library"; diff --git a/src/frontend/favorites/favorite-search.ts b/src/frontend/favorites/favorite-search.ts index fae25a2f3..c680b0b57 100644 --- a/src/frontend/favorites/favorite-search.ts +++ b/src/frontend/favorites/favorite-search.ts @@ -1,10 +1,8 @@ -import MiniSearch, { - Options, - SearchResult as MiniSearchResult -} from "minisearch"; +import MiniSearch, { Options } from "minisearch"; import { Favorite } from "../../shared/api-models"; -import { processTerm, tokenize } from "../../shared/search"; -import { Position, SearchHit } from "../search/search"; +import { tokenize, TypedMiniSearchResult } from "../../shared/search-utils"; +import { processTerm } from "../../shared/search-utils"; +import { Position, SearchHit } from "../search/insertable-search"; interface FavoriteSearchDocument { id: string; @@ -12,6 +10,8 @@ interface FavoriteSearchDocument { favoriteId: string; } +type FavoriteSearchResult = TypedMiniSearchResult; + const FAVORITE_SEARCH_OPTIONS: Options = { fields: ["name", "favoriteId"], storeFields: ["id", "name"], @@ -40,11 +40,8 @@ function buildFavoriteSearchDb( return searchDb; } -function generateHighlightPositions( - result: MiniSearchResult, - document: FavoriteSearchDocument -): Position[] { - const name = document.name.toLowerCase(); +function generateHighlightPositions(result: FavoriteSearchResult): Position[] { + const name = result.name.toLowerCase(); const positions: Position[] = []; for (const [term, matchedFields] of Object.entries(result.match)) { @@ -63,7 +60,7 @@ function generateHighlightPositions( return positions; } -export interface FavoriteSearchResult { +export interface FilteredFavoritesResult { favorite: Favorite; searchHit: SearchHit | undefined; } @@ -71,7 +68,7 @@ export interface FavoriteSearchResult { export function filterFavoritesForSearch( favorites: Favorite[], query?: string -): FavoriteSearchResult[] { +): FilteredFavoritesResult[] { if (!query || query.trim() === "") { return favorites.map((favorite) => ({ favorite, @@ -80,15 +77,19 @@ export function filterFavoritesForSearch( } const searchDb = buildFavoriteSearchDb(favorites); - const miniSearchResults = searchDb.search(query); + const miniSearchResults = searchDb.search(query, { + filter: () => { + // const searchResult = result as FavoriteSearchResult; + return true; + } + }) as FavoriteSearchResult[]; - const results: FavoriteSearchResult[] = []; + const results: FilteredFavoritesResult[] = []; for (const miniSearchResult of miniSearchResults) { - const document = searchDb.getStoredFields( - miniSearchResult.id - ) as unknown as FavoriteSearchDocument; - const favorite = favorites.find((item) => item.id === document.id); + const favorite = favorites.find( + (item) => item.id === miniSearchResult.id + ); if (!favorite) { continue; } @@ -97,10 +98,7 @@ export function filterFavoritesForSearch( favorite, searchHit: { id: favorite.id, - positions: generateHighlightPositions( - miniSearchResult, - document - ) + positions: generateHighlightPositions(miniSearchResult) } }); } diff --git a/src/frontend/favorites/favorites-list.tsx b/src/frontend/favorites/favorites-list.tsx index 57cc9b041..e34c15b31 100644 --- a/src/frontend/favorites/favorites-list.tsx +++ b/src/frontend/favorites/favorites-list.tsx @@ -12,9 +12,9 @@ import { NoSearchResultError, SearchCallout } from "../search/search-errors"; import { FavoriteCard } from "./favorite-card"; import { ItemTable } from "../cards/card-components"; import { useFavoritesQuery, useLibraryQuery } from "../queries"; -import { FilterResult, SearchHit } from "../search/search"; +import { FilterResult, SearchHit } from "../search/insertable-search"; import { - FavoriteSearchResult, + FilteredFavoritesResult, filterFavoritesForSearch } from "./favorite-search"; @@ -64,7 +64,7 @@ export function FavoritesList(): ReactNode { ); const matchedInsertables = matchedFavorites - .map((result: FavoriteSearchResult) => { + .map((result: FilteredFavoritesResult) => { const insertable = insertables[result.favorite.insertableId]; return insertable ? { insertable, result } : undefined; }) @@ -73,7 +73,7 @@ export function FavoritesList(): ReactNode { item ): item is { insertable: InsertableOut; - result: FavoriteSearchResult; + result: FilteredFavoritesResult; } => !!item ); diff --git a/src/frontend/queries.ts b/src/frontend/queries.ts index 5178151f3..062a40bda 100644 --- a/src/frontend/queries.ts +++ b/src/frontend/queries.ts @@ -14,7 +14,7 @@ import { ContextData } from "../shared/types"; import { useLibraryId } from "./api-utils/library"; import { type UnitInfo } from "../shared/configuration-models"; import MiniSearch from "minisearch"; -import { SEARCH_OPTIONS } from "../shared/search"; +import { SEARCH_OPTIONS } from "../shared/build-insertable-search"; import { InstancePath } from "../shared/onshape-path"; export function getConfigurationMatchKey() { diff --git a/src/frontend/search/filter.ts b/src/frontend/search/filter.ts index 01a658d31..72b8626b4 100644 --- a/src/frontend/search/filter.ts +++ b/src/frontend/search/filter.ts @@ -1,6 +1,6 @@ import { InsertableOut } from "../../shared/api-models"; import { Vendor } from "../../shared/types"; -import { FilterResult } from "./search"; +import { FilterResult } from "./insertable-search"; export interface FilterArgs { /** diff --git a/src/frontend/search/search.ts b/src/frontend/search/insertable-search.ts similarity index 72% rename from src/frontend/search/search.ts rename to src/frontend/search/insertable-search.ts index b674b80fd..1a5555431 100644 --- a/src/frontend/search/search.ts +++ b/src/frontend/search/insertable-search.ts @@ -1,18 +1,9 @@ -import MiniSearch, { SearchResult as MiniSearchResult } from "minisearch"; +import MiniSearch from "minisearch"; import { Vendor } from "../../shared/types"; -import { SearchDocument } from "../../shared/search"; +import { SearchDocument } from "../../shared/build-insertable-search"; +import { TypedMiniSearchResult } from "../../shared/search-utils"; -/** - * A user facing name to use for elements currently being filtered/searched on. - */ -export type ObjectLabel = "element" | "favorite" | "search result"; - -/** - * Returns the plural form of an object label. - */ -export function plural(objectLabel: ObjectLabel): string { - return objectLabel + "s"; -} +type SearchDocumentResult = TypedMiniSearchResult; export interface SearchFilters { groupId?: string; @@ -59,8 +50,10 @@ export function doSearch( return { hits: [], filtered }; } - const miniSearchResults: MiniSearchResult[] = searchDb.search(query, { - filter: (searchResult) => { + const miniSearchResults = searchDb.search(query, { + filter: (result) => { + // Stored fields are merged onto each result, so it doubles as the document + const searchResult = result as SearchDocumentResult; if (!showHidden && !searchResult.isVisible) { return false; } @@ -93,25 +86,14 @@ export function doSearch( return true; } - }); + }) as SearchDocumentResult[]; // Add highlighting const hits: SearchHit[] = miniSearchResults - .map((miniSearchResult) => { - // Stored fields should be the same as SearchDocument - const document = searchDb.getStoredFields( - miniSearchResult.id - ) as unknown as SearchDocument; - const positions = generateHighlightPositions( - miniSearchResult, - document - ); - - return { - id: document.id, - positions - }; - }) + .map((miniSearchResult) => ({ + id: miniSearchResult.id, + positions: generateHighlightPositions(miniSearchResult) + })) .slice(0, 50); // Limit to 50 results return { hits, filtered }; @@ -121,14 +103,11 @@ export function doSearch( * Generate highlight positions for matched terms in the document. * Based on approach from https://github.com/lucaong/minisearch/issues/37 */ -function generateHighlightPositions( - result: MiniSearchResult, - document: SearchDocument -): Position[] { +function generateHighlightPositions(result: SearchDocumentResult): Position[] { // Terms is an array of values in name (or spacedName) which matched // e.g., if search is "mot w", then terms could be ["motor", "WCP"] - const name = document.name.toLowerCase(); + const name = result.name.toLowerCase(); const positions: Position[] = []; diff --git a/src/frontend/search/search-errors.tsx b/src/frontend/search/search-errors.tsx index 66d87a07f..c3fbf22f8 100644 --- a/src/frontend/search/search-errors.tsx +++ b/src/frontend/search/search-errors.tsx @@ -3,10 +3,22 @@ import { IconHeartBroken, IconSearch } from "@tabler/icons-react"; import { HeartIconColor, IconColor, IconSize } from "../common/style-constants"; import { ReactNode } from "react"; import { ClearFiltersButton } from "../settings/vendor-filters"; -import { FilterResult, ObjectLabel, plural } from "./search"; +import { FilterResult } from "./insertable-search"; import { useNavigate } from "@tanstack/react-router"; import { SectionError } from "../app-common/app-zero-state"; +/** + * A user facing name to use for elements currently being filtered/searched on. + */ +type ObjectLabel = "element" | "favorite" | "search result"; + +/** + * Returns the plural form of an object label. + */ +function plural(objectLabel: ObjectLabel): string { + return objectLabel + "s"; +} + function getGroupString(filtered: FilterResult, objectLabel: ObjectLabel) { if (filtered.byGroup > 1) { return `${filtered.byGroup} ${plural( diff --git a/src/frontend/search/search-results.tsx b/src/frontend/search/search-results.tsx index 162469f63..00ca6c4cd 100644 --- a/src/frontend/search/search-results.tsx +++ b/src/frontend/search/search-results.tsx @@ -1,6 +1,11 @@ import { ReactNode } from "react"; import { useLoaderData } from "@tanstack/react-router"; -import { Position, SearchFilters, SearchHit, doSearch } from "./search"; +import { + Position, + SearchFilters, + SearchHit, + doSearch +} from "./insertable-search"; import { InsertableCard } from "../cards/insertable-card"; import { ItemTable } from "../cards/card-components"; import { SectionError, SectionLoading } from "../app-common/app-zero-state"; diff --git a/src/shared/search.ts b/src/shared/build-insertable-search.ts similarity index 62% rename from src/shared/search.ts rename to src/shared/build-insertable-search.ts index 538cdf38e..fba52644d 100644 --- a/src/shared/search.ts +++ b/src/shared/build-insertable-search.ts @@ -6,37 +6,7 @@ import MiniSearch, { Options } from "minisearch"; import { LibraryOut } from "./api-models"; import { Vendor } from "./types"; - -const deliminator = "^"; - -/** - * Adds spaces to a given string so prefix matching is more efficient. - */ -export function processTerm(term: string): string[] { - // Split between lowercase-to-uppercase (camelCase -> camel case) - const camelSplit = term - .replace(/([a-z])([A-Z])/g, `$1${deliminator}$2`) - .split(deliminator); - - // Insert spaces to handle MAXTube->MAX Tube, VEXpro->VEX pro - const pascalSplit = term - .replace(/([A-Z])([A-Z][a-z])/g, `$1${deliminator}$2`) - .split(deliminator); - - const base = term.toLowerCase(); - - const terms = [...camelSplit, ...pascalSplit, base].map((t) => - t.toLowerCase() - ); - // Deduplicate - return Array.from(new Set(terms)); -} - -export function tokenize(text: string): string[] { - // Don't lowercase so we can use casing for term splitting - // Remove -, (, ), ", ', #, &, /, and whitespace - return text.split(/[-()"'#&\s^/]+/).filter(Boolean); -} +import { processTerm, tokenize } from "./search-utils"; export interface SearchDocument { id: string; diff --git a/src/frontend/search/search.test.ts b/src/shared/search-utils.test.ts similarity index 91% rename from src/frontend/search/search.test.ts rename to src/shared/search-utils.test.ts index 47307928c..0d7d11178 100644 --- a/src/frontend/search/search.test.ts +++ b/src/shared/search-utils.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; -import { processTerm, tokenize } from "../../shared/search"; +import { tokenize } from "./search-utils"; +import { processTerm } from "./search-utils"; describe("processTerm", () => { it("should process camelCase", () => { diff --git a/src/shared/search-utils.ts b/src/shared/search-utils.ts new file mode 100644 index 000000000..bb853e05c --- /dev/null +++ b/src/shared/search-utils.ts @@ -0,0 +1,42 @@ +import { SearchResult as MiniSearchResult } from "minisearch"; + +/** + * Global deliminator character used when splitting terms. + * + * Intended to be a unique character that doesn't otherwise get used. + */ +export const deliminator = "^"; + +/** + * Adds spaces to a given string so prefix matching is more efficient. + */ +export function processTerm(term: string): string[] { + // Split between lowercase-to-uppercase (camelCase -> camel case) + const camelSplit = term + .replace(/([a-z])([A-Z])/g, `$1${deliminator}$2`) + .split(deliminator); + + // Insert spaces to handle MAXTube->MAX Tube, VEXpro->VEX pro + const pascalSplit = term + .replace(/([A-Z])([A-Z][a-z])/g, `$1${deliminator}$2`) + .split(deliminator); + + const base = term.toLowerCase(); + + const terms = [...camelSplit, ...pascalSplit, base].map((t) => + t.toLowerCase() + ); + // Deduplicate + return Array.from(new Set(terms)); +} +export function tokenize(text: string): string[] { + // Don't lowercase so we can use casing for term splitting + // Remove -, (, ), ", ', #, &, /, and whitespace + return text.split(/[-()"'#&\s^/]+/).filter(Boolean); +} + +/** + * A MiniSearch result with its stored fields typed as `Document`. + */ +export type TypedMiniSearchResult = SearchDocument & + Pick; From e52c60686be88f1bcaf8a756fa987aef8a6d1b43 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 12 Jul 2026 18:34:09 -0400 Subject: [PATCH 07/10] deleted: coverage/clover.xml deleted: coverage/coverage-final.json deleted: coverage/lcov-report/base.css deleted: coverage/lcov-report/block-navigation.js deleted: coverage/lcov-report/favicon.png deleted: coverage/lcov-report/index.html deleted: coverage/lcov-report/prettify.css deleted: coverage/lcov-report/prettify.js deleted: coverage/lcov-report/sort-arrow-sprite.png deleted: coverage/lcov-report/sorter.js deleted: coverage/lcov.info --- coverage/clover.xml | 6 - coverage/coverage-final.json | 1 - coverage/lcov-report/base.css | 362 ------- coverage/lcov-report/block-navigation.js | 87 -- coverage/lcov-report/favicon.png | Bin 445 -> 0 bytes coverage/lcov-report/index.html | 101 -- coverage/lcov-report/prettify.css | 101 -- coverage/lcov-report/prettify.js | 1048 -------------------- coverage/lcov-report/sort-arrow-sprite.png | Bin 138 -> 0 bytes coverage/lcov-report/sorter.js | 210 ---- coverage/lcov.info | 0 11 files changed, 1916 deletions(-) delete mode 100644 coverage/clover.xml delete mode 100644 coverage/coverage-final.json delete mode 100644 coverage/lcov-report/base.css delete mode 100644 coverage/lcov-report/block-navigation.js delete mode 100644 coverage/lcov-report/favicon.png delete mode 100644 coverage/lcov-report/index.html delete mode 100644 coverage/lcov-report/prettify.css delete mode 100644 coverage/lcov-report/prettify.js delete mode 100644 coverage/lcov-report/sort-arrow-sprite.png delete mode 100644 coverage/lcov-report/sorter.js delete mode 100644 coverage/lcov.info diff --git a/coverage/clover.xml b/coverage/clover.xml deleted file mode 100644 index c410f9466..000000000 --- a/coverage/clover.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json deleted file mode 100644 index 0967ef424..000000000 --- a/coverage/coverage-final.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css deleted file mode 100644 index 85891cb88..000000000 --- a/coverage/lcov-report/base.css +++ /dev/null @@ -1,362 +0,0 @@ -body, -html { - margin: 0; - padding: 0; - height: 100%; -} -body { - font-family: - Helvetica Neue, - Helvetica, - Arial; - font-size: 14px; - color: #333; -} -.small { - font-size: 12px; -} -*, -*:after, -*:before { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -h1 { - font-size: 20px; - margin: 0; -} -h2 { - font-size: 14px; -} -pre { - font: - 12px/1.4 Consolas, - "Liberation Mono", - Menlo, - Courier, - monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { - color: #0074d9; - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -.strong { - font-weight: bold; -} -.space-top1 { - padding: 10px 0 0 0; -} -.pad2y { - padding: 20px 0; -} -.pad1y { - padding: 10px 0; -} -.pad2x { - padding: 0 20px; -} -.pad2 { - padding: 20px; -} -.pad1 { - padding: 10px; -} -.space-left2 { - padding-left: 55px; -} -.space-right2 { - padding-right: 20px; -} -.center { - text-align: center; -} -.clearfix { - display: block; -} -.clearfix:after { - content: ""; - display: block; - height: 0; - clear: both; - visibility: hidden; -} -.fl { - float: left; -} -@media only screen and (max-width: 640px) { - .col3 { - width: 100%; - max-width: 100%; - } - .hide-mobile { - display: none !important; - } -} - -.quiet { - color: #7f7f7f; - color: rgba(0, 0, 0, 0.5); -} -.quiet a { - opacity: 0.7; -} - -.fraction { - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #e8e8e8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, -div.path a:visited { - color: #333; -} -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width: 20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, -.skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { - border-bottom: 1px solid #bbb; -} -.keyline-all { - border: 1px solid #ddd; -} -.coverage-summary td, -.coverage-summary th { - padding: 10px; -} -.coverage-summary tbody { - border: 1px solid #bbb; -} -.coverage-summary td { - border-right: 1px solid #bbb; -} -.coverage-summary td:last-child { - border-right: none; -} -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { - border-right: none !important; -} -.coverage-summary th.pct { -} -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { - text-align: right; -} -.coverage-summary td.file { - white-space: nowrap; -} -.coverage-summary td.pic { - min-width: 120px !important; -} -.coverage-summary tfoot td { -} - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { - height: 10px; -} -/* yellow */ -.cbranch-no { - background: yellow !important; - color: #111; -} -/* dark red */ -.red.solid, -.status-line.low, -.low .cover-fill { - background: #c21f39; -} -.low .chart { - border: 1px solid #c21f39; -} -.highlighted, -.highlighted .cstat-no, -.highlighted .fstat-no, -.highlighted .cbranch-no { - background: #c21f39 !important; -} -/* medium red */ -.cstat-no, -.fstat-no, -.cbranch-no, -.cbranch-no { - background: #f6c6ce; -} -/* light red */ -.low, -.cline-no { - background: #fce1e5; -} -/* light green */ -.high, -.cline-yes { - background: rgb(230, 245, 208); -} -/* medium green */ -.cstat-yes { - background: rgb(161, 215, 106); -} -/* dark green */ -.status-line.high, -.high .cover-fill { - background: rgb(77, 146, 33); -} -.high .chart { - border: 1px solid rgb(77, 146, 33); -} -/* dark yellow (gold) */ -.status-line.medium, -.medium .cover-fill { - background: #f9cd0b; -} -.medium .chart { - border: 1px solid #f9cd0b; -} -/* light yellow */ -.medium { - background: #fff4c2; -} - -.cstat-skip { - background: #ddd; - color: #111; -} -.fstat-skip { - background: #ddd; - color: #111 !important; -} -.cbranch-skip { - background: #ddd !important; - color: #111; -} - -span.cline-neutral { - background: #eaeaea; -} - -.coverage-summary td.empty { - opacity: 0.5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, -.cover-empty { - display: inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { - color: #999 !important; -} -.ignore-none { - color: #999; - font-weight: normal; -} - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, -.push { - height: 48px; -} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js deleted file mode 100644 index 1d4778881..000000000 --- a/coverage/lcov-report/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = [".cbranch-no", ".cstat-no", ".fstat-no"]; - - // Elements to highlight in the file listing view - var fileListingElements = ["td.pct.low"]; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ":not(" + missingCoverageClasses.join("):not(") + ") > "; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(", ") + - ", " + - notSelector + - missingCoverageClasses.join(", " + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove("highlighted"); - missingCoverageElements.item(index).classList.add("highlighted"); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: "smooth", - block: "center", - inline: "center" - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== "number" || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === "number" && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById("fileSearch") === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener("keydown", jumpToCode); diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png deleted file mode 100644 index c1525b811a167671e9de1fa78aab9f5c0b61cef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- Unknown% - Statements - 0/0 -
- - -
- Unknown% - Branches - 0/0 -
- - -
- Unknown% - Functions - 0/0 -
- - -
- Unknown% - Lines - 0/0 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css deleted file mode 100644 index babc093e8..000000000 --- a/coverage/lcov-report/prettify.css +++ /dev/null @@ -1,101 +0,0 @@ -.pln { - color: #000; -} -@media screen { - .str { - color: #080; - } - .kwd { - color: #008; - } - .com { - color: #800; - } - .typ { - color: #606; - } - .lit { - color: #066; - } - .pun, - .opn, - .clo { - color: #660; - } - .tag { - color: #008; - } - .atn { - color: #606; - } - .atv { - color: #080; - } - .dec, - .var { - color: #606; - } - .fun { - color: red; - } -} -@media print, projection { - .str { - color: #060; - } - .kwd { - color: #006; - font-weight: bold; - } - .com { - color: #600; - font-style: italic; - } - .typ { - color: #404; - font-weight: bold; - } - .lit { - color: #044; - } - .pun, - .opn, - .clo { - color: #440; - } - .tag { - color: #006; - font-weight: bold; - } - .atn { - color: #404; - } - .atv { - color: #060; - } -} -pre.prettyprint { - padding: 2px; - border: 1px solid #888; -} -ol.linenums { - margin-top: 0; - margin-bottom: 0; -} -li.L0, -li.L1, -li.L2, -li.L3, -li.L5, -li.L6, -li.L7, -li.L8 { - list-style-type: none; -} -li.L1, -li.L3, -li.L5, -li.L7, -li.L9 { - background: #eee; -} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js deleted file mode 100644 index 6894dbba3..000000000 --- a/coverage/lcov-report/prettify.js +++ /dev/null @@ -1,1048 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION = true; -(function () { - var h = ["break,continue,do,else,for,if,return,while"]; - var u = [ - h, - "auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile" - ]; - var p = [ - u, - "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof" - ]; - var l = [ - p, - "alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where" - ]; - var x = [ - p, - "abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient" - ]; - var R = [ - x, - "as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var" - ]; - var r = - "all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes"; - var w = [ - p, - "debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN" - ]; - var s = - "caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"; - var I = [ - h, - "and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None" - ]; - var f = [ - h, - "alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END" - ]; - var H = [h, "case,done,elif,esac,eval,fi,function,in,local,set,then,until"]; - var A = [l, R, w, s + I, f, H]; - var e = - /^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/; - var C = "str"; - var z = "kwd"; - var j = "com"; - var O = "typ"; - var G = "lit"; - var L = "pun"; - var F = "pln"; - var m = "tag"; - var E = "dec"; - var J = "src"; - var P = "atn"; - var n = "atv"; - var N = "nocode"; - var M = - "(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*"; - function k(Z) { - var ad = 0; - var S = false; - var ac = false; - for (var V = 0, U = Z.length; V < U; ++V) { - var ae = Z[V]; - if (ae.ignoreCase) { - ac = true; - } else { - if ( - /[a-z]/i.test( - ae.source.replace( - /\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, - "" - ) - ) - ) { - S = true; - ac = false; - break; - } - } - } - var Y = { b: 8, t: 9, n: 10, v: 11, f: 12, r: 13 }; - function ab(ah) { - var ag = ah.charCodeAt(0); - if (ag !== 92) { - return ag; - } - var af = ah.charAt(1); - ag = Y[af]; - if (ag) { - return ag; - } else { - if ("0" <= af && af <= "7") { - return parseInt(ah.substring(1), 8); - } else { - if (af === "u" || af === "x") { - return parseInt(ah.substring(2), 16); - } else { - return ah.charCodeAt(1); - } - } - } - } - function T(af) { - if (af < 32) { - return (af < 16 ? "\\x0" : "\\x") + af.toString(16); - } - var ag = String.fromCharCode(af); - if (ag === "\\" || ag === "-" || ag === "[" || ag === "]") { - ag = "\\" + ag; - } - return ag; - } - function X(am) { - var aq = am - .substring(1, am.length - 1) - .match( - new RegExp( - "\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]", - "g" - ) - ); - var ak = []; - var af = []; - var ao = aq[0] === "^"; - for (var ar = ao ? 1 : 0, aj = aq.length; ar < aj; ++ar) { - var ah = aq[ar]; - if (/\\[bdsw]/i.test(ah)) { - ak.push(ah); - } else { - var ag = ab(ah); - var al; - if (ar + 2 < aj && "-" === aq[ar + 1]) { - al = ab(aq[ar + 2]); - ar += 2; - } else { - al = ag; - } - af.push([ag, al]); - if (!(al < 65 || ag > 122)) { - if (!(al < 65 || ag > 90)) { - af.push([ - Math.max(65, ag) | 32, - Math.min(al, 90) | 32 - ]); - } - if (!(al < 97 || ag > 122)) { - af.push([ - Math.max(97, ag) & ~32, - Math.min(al, 122) & ~32 - ]); - } - } - } - } - af.sort(function (av, au) { - return av[0] - au[0] || au[1] - av[1]; - }); - var ai = []; - var ap = [NaN, NaN]; - for (var ar = 0; ar < af.length; ++ar) { - var at = af[ar]; - if (at[0] <= ap[1] + 1) { - ap[1] = Math.max(ap[1], at[1]); - } else { - ai.push((ap = at)); - } - } - var an = ["["]; - if (ao) { - an.push("^"); - } - an.push.apply(an, ak); - for (var ar = 0; ar < ai.length; ++ar) { - var at = ai[ar]; - an.push(T(at[0])); - if (at[1] > at[0]) { - if (at[1] + 1 > at[0]) { - an.push("-"); - } - an.push(T(at[1])); - } - } - an.push("]"); - return an.join(""); - } - function W(al) { - var aj = al.source.match( - new RegExp( - "(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)", - "g" - ) - ); - var ah = aj.length; - var an = []; - for (var ak = 0, am = 0; ak < ah; ++ak) { - var ag = aj[ak]; - if (ag === "(") { - ++am; - } else { - if ("\\" === ag.charAt(0)) { - var af = +ag.substring(1); - if (af && af <= am) { - an[af] = -1; - } - } - } - } - for (var ak = 1; ak < an.length; ++ak) { - if (-1 === an[ak]) { - an[ak] = ++ad; - } - } - for (var ak = 0, am = 0; ak < ah; ++ak) { - var ag = aj[ak]; - if (ag === "(") { - ++am; - if (an[am] === undefined) { - aj[ak] = "(?:"; - } - } else { - if ("\\" === ag.charAt(0)) { - var af = +ag.substring(1); - if (af && af <= am) { - aj[ak] = "\\" + an[am]; - } - } - } - } - for (var ak = 0, am = 0; ak < ah; ++ak) { - if ("^" === aj[ak] && "^" !== aj[ak + 1]) { - aj[ak] = ""; - } - } - if (al.ignoreCase && S) { - for (var ak = 0; ak < ah; ++ak) { - var ag = aj[ak]; - var ai = ag.charAt(0); - if (ag.length >= 2 && ai === "[") { - aj[ak] = X(ag); - } else { - if (ai !== "\\") { - aj[ak] = ag.replace(/[a-zA-Z]/g, function (ao) { - var ap = ao.charCodeAt(0); - return ( - "[" + - String.fromCharCode(ap & ~32, ap | 32) + - "]" - ); - }); - } - } - } - } - return aj.join(""); - } - var aa = []; - for (var V = 0, U = Z.length; V < U; ++V) { - var ae = Z[V]; - if (ae.global || ae.multiline) { - throw new Error("" + ae); - } - aa.push("(?:" + W(ae) + ")"); - } - return new RegExp(aa.join("|"), ac ? "gi" : "g"); - } - function a(V) { - var U = /(?:^|\s)nocode(?:\s|$)/; - var X = []; - var T = 0; - var Z = []; - var W = 0; - var S; - if (V.currentStyle) { - S = V.currentStyle.whiteSpace; - } else { - if (window.getComputedStyle) { - S = document.defaultView - .getComputedStyle(V, null) - .getPropertyValue("white-space"); - } - } - var Y = S && "pre" === S.substring(0, 3); - function aa(ab) { - switch (ab.nodeType) { - case 1: - if (U.test(ab.className)) { - return; - } - for (var ae = ab.firstChild; ae; ae = ae.nextSibling) { - aa(ae); - } - var ad = ab.nodeName; - if ("BR" === ad || "LI" === ad) { - X[W] = "\n"; - Z[W << 1] = T++; - Z[(W++ << 1) | 1] = ab; - } - break; - case 3: - case 4: - var ac = ab.nodeValue; - if (ac.length) { - if (!Y) { - ac = ac.replace(/[ \t\r\n]+/g, " "); - } else { - ac = ac.replace(/\r\n?/g, "\n"); - } - X[W] = ac; - Z[W << 1] = T; - T += ac.length; - Z[(W++ << 1) | 1] = ab; - } - break; - } - } - aa(V); - return { sourceCode: X.join("").replace(/\n$/, ""), spans: Z }; - } - function B(S, U, W, T) { - if (!U) { - return; - } - var V = { sourceCode: U, basePos: S }; - W(V); - T.push.apply(T, V.decorations); - } - var v = /\S/; - function o(S) { - var V = undefined; - for (var U = S.firstChild; U; U = U.nextSibling) { - var T = U.nodeType; - V = - T === 1 - ? V - ? S - : U - : T === 3 - ? v.test(U.nodeValue) - ? S - : V - : V; - } - return V === S ? undefined : V; - } - function g(U, T) { - var S = {}; - var V; - (function () { - var ad = U.concat(T); - var ah = []; - var ag = {}; - for (var ab = 0, Z = ad.length; ab < Z; ++ab) { - var Y = ad[ab]; - var ac = Y[3]; - if (ac) { - for (var ae = ac.length; --ae >= 0; ) { - S[ac.charAt(ae)] = Y; - } - } - var af = Y[1]; - var aa = "" + af; - if (!ag.hasOwnProperty(aa)) { - ah.push(af); - ag[aa] = null; - } - } - ah.push(/[\0-\uffff]/); - V = k(ah); - })(); - var X = T.length; - var W = function (ah) { - var Z = ah.sourceCode, - Y = ah.basePos; - var ad = [Y, F]; - var af = 0; - var an = Z.match(V) || []; - var aj = {}; - for (var ae = 0, aq = an.length; ae < aq; ++ae) { - var ag = an[ae]; - var ap = aj[ag]; - var ai = void 0; - var am; - if (typeof ap === "string") { - am = false; - } else { - var aa = S[ag.charAt(0)]; - if (aa) { - ai = ag.match(aa[1]); - ap = aa[0]; - } else { - for (var ao = 0; ao < X; ++ao) { - aa = T[ao]; - ai = ag.match(aa[1]); - if (ai) { - ap = aa[0]; - break; - } - } - if (!ai) { - ap = F; - } - } - am = ap.length >= 5 && "lang-" === ap.substring(0, 5); - if (am && !(ai && typeof ai[1] === "string")) { - am = false; - ap = J; - } - if (!am) { - aj[ag] = ap; - } - } - var ab = af; - af += ag.length; - if (!am) { - ad.push(Y + ab, ap); - } else { - var al = ai[1]; - var ak = ag.indexOf(al); - var ac = ak + al.length; - if (ai[2]) { - ac = ag.length - ai[2].length; - ak = ac - al.length; - } - var ar = ap.substring(5); - B(Y + ab, ag.substring(0, ak), W, ad); - B(Y + ab + ak, al, q(ar, al), ad); - B(Y + ab + ac, ag.substring(ac), W, ad); - } - } - ah.decorations = ad; - }; - return W; - } - function i(T) { - var W = [], - S = []; - if (T.tripleQuotedStrings) { - W.push([ - C, - /^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/, - null, - "'\"" - ]); - } else { - if (T.multiLineStrings) { - W.push([ - C, - /^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/, - null, - "'\"`" - ]); - } else { - W.push([ - C, - /^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/, - null, - "\"'" - ]); - } - } - if (T.verbatimStrings) { - S.push([C, /^@\"(?:[^\"]|\"\")*(?:\"|$)/, null]); - } - var Y = T.hashComments; - if (Y) { - if (T.cStyleComments) { - if (Y > 1) { - W.push([ - j, - /^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/, - null, - "#" - ]); - } else { - W.push([ - j, - /^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/, - null, - "#" - ]); - } - S.push([ - C, - /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/, - null - ]); - } else { - W.push([j, /^#[^\r\n]*/, null, "#"]); - } - } - if (T.cStyleComments) { - S.push([j, /^\/\/[^\r\n]*/, null]); - S.push([j, /^\/\*[\s\S]*?(?:\*\/|$)/, null]); - } - if (T.regexLiterals) { - var X = - "/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/"; - S.push(["lang-regex", new RegExp("^" + M + "(" + X + ")")]); - } - var V = T.types; - if (V) { - S.push([O, V]); - } - var U = ("" + T.keywords).replace(/^ | $/g, ""); - if (U.length) { - S.push([ - z, - new RegExp("^(?:" + U.replace(/[\s,]+/g, "|") + ")\\b"), - null - ]); - } - W.push([F, /^\s+/, null, " \r\n\t\xA0"]); - S.push( - [G, /^@[a-z_$][a-z_$@0-9]*/i, null], - [O, /^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/, null], - [F, /^[a-z_$][a-z_$@0-9]*/i, null], - [ - G, - new RegExp( - "^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*", - "i" - ), - null, - "0123456789" - ], - [F, /^\\[\s\S]?/, null], - [L, /^.[^\s\w\.$@\'\"\`\/\#\\]*/, null] - ); - return g(W, S); - } - var K = i({ - keywords: A, - hashComments: true, - cStyleComments: true, - multiLineStrings: true, - regexLiterals: true - }); - function Q(V, ag) { - var U = /(?:^|\s)nocode(?:\s|$)/; - var ab = /\r\n?|\n/; - var ac = V.ownerDocument; - var S; - if (V.currentStyle) { - S = V.currentStyle.whiteSpace; - } else { - if (window.getComputedStyle) { - S = ac.defaultView - .getComputedStyle(V, null) - .getPropertyValue("white-space"); - } - } - var Z = S && "pre" === S.substring(0, 3); - var af = ac.createElement("LI"); - while (V.firstChild) { - af.appendChild(V.firstChild); - } - var W = [af]; - function ae(al) { - switch (al.nodeType) { - case 1: - if (U.test(al.className)) { - break; - } - if ("BR" === al.nodeName) { - ad(al); - if (al.parentNode) { - al.parentNode.removeChild(al); - } - } else { - for (var an = al.firstChild; an; an = an.nextSibling) { - ae(an); - } - } - break; - case 3: - case 4: - if (Z) { - var am = al.nodeValue; - var aj = am.match(ab); - if (aj) { - var ai = am.substring(0, aj.index); - al.nodeValue = ai; - var ah = am.substring(aj.index + aj[0].length); - if (ah) { - var ak = al.parentNode; - ak.insertBefore( - ac.createTextNode(ah), - al.nextSibling - ); - } - ad(al); - if (!ai) { - al.parentNode.removeChild(al); - } - } - } - break; - } - } - function ad(ak) { - while (!ak.nextSibling) { - ak = ak.parentNode; - if (!ak) { - return; - } - } - function ai(al, ar) { - var aq = ar ? al.cloneNode(false) : al; - var ao = al.parentNode; - if (ao) { - var ap = ai(ao, 1); - var an = al.nextSibling; - ap.appendChild(aq); - for (var am = an; am; am = an) { - an = am.nextSibling; - ap.appendChild(am); - } - } - return aq; - } - var ah = ai(ak.nextSibling, 0); - for (var aj; (aj = ah.parentNode) && aj.nodeType === 1; ) { - ah = aj; - } - W.push(ah); - } - for (var Y = 0; Y < W.length; ++Y) { - ae(W[Y]); - } - if (ag === (ag | 0)) { - W[0].setAttribute("value", ag); - } - var aa = ac.createElement("OL"); - aa.className = "linenums"; - var X = Math.max(0, (ag - 1) | 0) || 0; - for (var Y = 0, T = W.length; Y < T; ++Y) { - af = W[Y]; - af.className = "L" + ((Y + X) % 10); - if (!af.firstChild) { - af.appendChild(ac.createTextNode("\xA0")); - } - aa.appendChild(af); - } - V.appendChild(aa); - } - function D(ac) { - var aj = /\bMSIE\b/.test(navigator.userAgent); - var am = /\n/g; - var al = ac.sourceCode; - var an = al.length; - var V = 0; - var aa = ac.spans; - var T = aa.length; - var ah = 0; - var X = ac.decorations; - var Y = X.length; - var Z = 0; - X[Y] = an; - var ar, aq; - for (aq = ar = 0; aq < Y; ) { - if (X[aq] !== X[aq + 2]) { - X[ar++] = X[aq++]; - X[ar++] = X[aq++]; - } else { - aq += 2; - } - } - Y = ar; - for (aq = ar = 0; aq < Y; ) { - var at = X[aq]; - var ab = X[aq + 1]; - var W = aq + 2; - while (W + 2 <= Y && X[W + 1] === ab) { - W += 2; - } - X[ar++] = at; - X[ar++] = ab; - aq = W; - } - Y = X.length = ar; - var ae = null; - while (ah < T) { - var af = aa[ah]; - var S = aa[ah + 2] || an; - var ag = X[Z]; - var ap = X[Z + 2] || an; - var W = Math.min(S, ap); - var ak = aa[ah + 1]; - var U; - if (ak.nodeType !== 1 && (U = al.substring(V, W))) { - if (aj) { - U = U.replace(am, "\r"); - } - ak.nodeValue = U; - var ai = ak.ownerDocument; - var ao = ai.createElement("SPAN"); - ao.className = X[Z + 1]; - var ad = ak.parentNode; - ad.replaceChild(ao, ak); - ao.appendChild(ak); - if (V < S) { - aa[ah + 1] = ak = ai.createTextNode(al.substring(W, S)); - ad.insertBefore(ak, ao.nextSibling); - } - } - V = W; - if (V >= S) { - ah += 2; - } - if (V >= ap) { - Z += 2; - } - } - } - var t = {}; - function c(U, V) { - for (var S = V.length; --S >= 0; ) { - var T = V[S]; - if (!t.hasOwnProperty(T)) { - t[T] = U; - } else { - if (window.console) { - console.warn("cannot override language handler %s", T); - } - } - } - } - function q(T, S) { - if (!(T && t.hasOwnProperty(T))) { - T = /^\s*]*(?:>|$)/], - [j, /^<\!--[\s\S]*?(?:-\->|$)/], - ["lang-", /^<\?([\s\S]+?)(?:\?>|$)/], - ["lang-", /^<%([\s\S]+?)(?:%>|$)/], - [L, /^(?:<[%?]|[%?]>)/], - ["lang-", /^]*>([\s\S]+?)<\/xmp\b[^>]*>/i], - ["lang-js", /^]*>([\s\S]*?)(<\/script\b[^>]*>)/i], - ["lang-css", /^]*>([\s\S]*?)(<\/style\b[^>]*>)/i], - ["lang-in.tag", /^(<\/?[a-z][^<>]*>)/i] - ] - ), - ["default-markup", "htm", "html", "mxml", "xhtml", "xml", "xsl"] - ); - c( - g( - [ - [F, /^[\s]+/, null, " \t\r\n"], - [n, /^(?:\"[^\"]*\"?|\'[^\']*\'?)/, null, "\"'"] - ], - [ - [m, /^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i], - [P, /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i], - ["lang-uq.val", /^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/], - [L, /^[=<>\/]+/], - ["lang-js", /^on\w+\s*=\s*\"([^\"]+)\"/i], - ["lang-js", /^on\w+\s*=\s*\'([^\']+)\'/i], - ["lang-js", /^on\w+\s*=\s*([^\"\'>\s]+)/i], - ["lang-css", /^style\s*=\s*\"([^\"]+)\"/i], - ["lang-css", /^style\s*=\s*\'([^\']+)\'/i], - ["lang-css", /^style\s*=\s*([^\"\'>\s]+)/i] - ] - ), - ["in.tag"] - ); - c(g([], [[n, /^[\s\S]+/]]), ["uq.val"]); - c(i({ keywords: l, hashComments: true, cStyleComments: true, types: e }), [ - "c", - "cc", - "cpp", - "cxx", - "cyc", - "m" - ]); - c(i({ keywords: "null,true,false" }), ["json"]); - c( - i({ - keywords: R, - hashComments: true, - cStyleComments: true, - verbatimStrings: true, - types: e - }), - ["cs"] - ); - c(i({ keywords: x, cStyleComments: true }), ["java"]); - c(i({ keywords: H, hashComments: true, multiLineStrings: true }), [ - "bsh", - "csh", - "sh" - ]); - c( - i({ - keywords: I, - hashComments: true, - multiLineStrings: true, - tripleQuotedStrings: true - }), - ["cv", "py"] - ); - c( - i({ - keywords: s, - hashComments: true, - multiLineStrings: true, - regexLiterals: true - }), - ["perl", "pl", "pm"] - ); - c( - i({ - keywords: f, - hashComments: true, - multiLineStrings: true, - regexLiterals: true - }), - ["rb"] - ); - c(i({ keywords: w, cStyleComments: true, regexLiterals: true }), ["js"]); - c( - i({ - keywords: r, - hashComments: 3, - cStyleComments: true, - multilineStrings: true, - tripleQuotedStrings: true, - regexLiterals: true - }), - ["coffee"] - ); - c(g([], [[C, /^[\s\S]+/]]), ["regex"]); - function d(V) { - var U = V.langExtension; - try { - var S = a(V.sourceNode); - var T = S.sourceCode; - V.sourceCode = T; - V.spans = S.spans; - V.basePos = 0; - q(U, T)(V); - D(V); - } catch (W) { - if ("console" in window) { - console.log(W && W.stack ? W.stack : W); - } - } - } - function y(W, V, U) { - var S = document.createElement("PRE"); - S.innerHTML = W; - if (U) { - Q(S, U); - } - var T = { langExtension: V, numberLines: U, sourceNode: S }; - d(T); - return S.innerHTML; - } - function b(ad) { - function Y(af) { - return document.getElementsByTagName(af); - } - var ac = [Y("pre"), Y("code"), Y("xmp")]; - var T = []; - for (var aa = 0; aa < ac.length; ++aa) { - for (var Z = 0, V = ac[aa].length; Z < V; ++Z) { - T.push(ac[aa][Z]); - } - } - ac = null; - var W = Date; - if (!W.now) { - W = { - now: function () { - return +new Date(); - } - }; - } - var X = 0; - var S; - var ab = /\blang(?:uage)?-([\w.]+)(?!\S)/; - var ae = /\bprettyprint\b/; - function U() { - var ag = window.PR_SHOULD_USE_CONTINUATION - ? W.now() + 250 - : Infinity; - for (; X < T.length && W.now() < ag; X++) { - var aj = T[X]; - var ai = aj.className; - if (ai.indexOf("prettyprint") >= 0) { - var ah = ai.match(ab); - var am; - if (!ah && (am = o(aj)) && "CODE" === am.tagName) { - ah = am.className.match(ab); - } - if (ah) { - ah = ah[1]; - } - var al = false; - for (var ak = aj.parentNode; ak; ak = ak.parentNode) { - if ( - (ak.tagName === "pre" || - ak.tagName === "code" || - ak.tagName === "xmp") && - ak.className && - ak.className.indexOf("prettyprint") >= 0 - ) { - al = true; - break; - } - } - if (!al) { - var af = aj.className.match(/\blinenums\b(?::(\d+))?/); - af = af - ? af[1] && af[1].length - ? +af[1] - : true - : false; - if (af) { - Q(aj, af); - } - S = { - langExtension: ah, - sourceNode: aj, - numberLines: af - }; - d(S); - } - } - } - if (X < T.length) { - setTimeout(U, 250); - } else { - if (ad) { - ad(); - } - } - } - U(); - } - window.prettyPrintOne = y; - window.prettyPrint = b; - window.PR = { - createSimpleLexer: g, - registerLangHandler: c, - sourceDecorator: i, - PR_ATTRIB_NAME: P, - PR_ATTRIB_VALUE: n, - PR_COMMENT: j, - PR_DECLARATION: E, - PR_KEYWORD: z, - PR_LITERAL: G, - PR_NOCODE: N, - PR_PLAIN: F, - PR_PUNCTUATION: L, - PR_SOURCE: J, - PR_STRING: C, - PR_TAG: m, - PR_TYPE: O - }; -})(); -PR.registerLangHandler( - PR.createSimpleLexer( - [], - [ - [PR.PR_DECLARATION, /^]*(?:>|$)/], - [PR.PR_COMMENT, /^<\!--[\s\S]*?(?:-\->|$)/], - [PR.PR_PUNCTUATION, /^(?:<[%?]|[%?]>)/], - ["lang-", /^<\?([\s\S]+?)(?:\?>|$)/], - ["lang-", /^<%([\s\S]+?)(?:%>|$)/], - ["lang-", /^]*>([\s\S]+?)<\/xmp\b[^>]*>/i], - [ - "lang-handlebars", - /^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i - ], - ["lang-js", /^]*>([\s\S]*?)(<\/script\b[^>]*>)/i], - ["lang-css", /^]*>([\s\S]*?)(<\/style\b[^>]*>)/i], - ["lang-in.tag", /^(<\/?[a-z][^<>]*>)/i], - [PR.PR_DECLARATION, /^{{[#^>/]?\s*[\w.][^}]*}}/], - [PR.PR_DECLARATION, /^{{&?\s*[\w.][^}]*}}/], - [PR.PR_DECLARATION, /^{{{>?\s*[\w.][^}]*}}}/], - [PR.PR_COMMENT, /^{{![^}]*}}/] - ] - ), - ["handlebars", "hbs"] -); -PR.registerLangHandler( - PR.createSimpleLexer( - [[PR.PR_PLAIN, /^[ \t\r\n\f]+/, null, " \t\r\n\f"]], - [ - [ - PR.PR_STRING, - /^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/, - null - ], - [ - PR.PR_STRING, - /^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/, - null - ], - ["lang-css-str", /^url\(([^\)\"\']*)\)/i], - [ - PR.PR_KEYWORD, - /^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i, - null - ], - [ - "lang-css-kw", - /^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i - ], - [PR.PR_COMMENT, /^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//], - [PR.PR_COMMENT, /^(?:)/], - [PR.PR_LITERAL, /^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i], - [PR.PR_LITERAL, /^#(?:[0-9a-f]{3}){1,2}/i], - [ - PR.PR_PLAIN, - /^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i - ], - [PR.PR_PUNCTUATION, /^[^\s\w\'\"]+/] - ] - ), - ["css"] -); -PR.registerLangHandler( - PR.createSimpleLexer( - [], - [ - [ - PR.PR_KEYWORD, - /^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i - ] - ] - ), - ["css-kw"] -); -PR.registerLangHandler( - PR.createSimpleLexer([], [[PR.PR_STRING, /^[^\)\"\']+/]]), - ["css-str"] -); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png deleted file mode 100644 index 6ed68316eb3f65dec9063332d2f69bf3093bbfab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js deleted file mode 100644 index aa007d25f..000000000 --- a/coverage/lcov-report/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function () { - "use strict"; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector(".coverage-summary"); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector("thead tr"); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector("tbody"); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll("th")[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById("fileSearch").value; - const rows = document.getElementsByTagName("tbody")[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, "i"); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? "" : "none"; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById("filterTemplate"); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById("fileSearch").oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll("th"), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute("data-col"), - sortable: !colNode.getAttribute("data-nosort"), - type: colNode.getAttribute("data-type") || "string" - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === "number"; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll("td"), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute("data-value"); - if (col.type === "number") { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll("tr"), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function (a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector(".coverage-summary tbody"), - rowNodes = tableBody.querySelectorAll("tr"), - rows = [], - i; - - if (desc) { - finalSorter = function (a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, "").replace(/ sorted-desc$/, ""); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? " sorted-desc" - : " sorted"; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function () { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector(".sorter").parentElement; - if (el.addEventListener) { - el.addEventListener("click", ithSorter(i)); - } else { - el.attachEvent("onclick", ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function () { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener("load", addSorting); diff --git a/coverage/lcov.info b/coverage/lcov.info deleted file mode 100644 index e69de29bb..000000000 From 7665593f44df523d4676cb2b69f34425c3cd70c2 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 19 Jul 2026 19:20:36 -0400 Subject: [PATCH 08/10] modified: src/backend/routes/favorites.ts modified: src/frontend/cards/card-components.tsx modified: src/frontend/cards/insertable-card.tsx modified: src/frontend/favorites/favorite-card.tsx modified: src/frontend/groups/group-card.tsx modified: src/shared/schema.ts --- src/backend/routes/favorites.ts | 45 ++++++++--------------- src/frontend/cards/card-components.tsx | 46 ++++++++++++++++-------- src/frontend/cards/insertable-card.tsx | 4 +-- src/frontend/favorites/favorite-card.tsx | 4 +-- src/frontend/groups/group-card.tsx | 4 +-- src/shared/schema.ts | 2 +- 6 files changed, 54 insertions(+), 51 deletions(-) diff --git a/src/backend/routes/favorites.ts b/src/backend/routes/favorites.ts index f73521b54..4a6b2aff1 100644 --- a/src/backend/routes/favorites.ts +++ b/src/backend/routes/favorites.ts @@ -33,7 +33,7 @@ async function getFavorites( insertableId: row.insertableId, libraryId, defaultConfiguration: row.defaultConfiguration ?? undefined, - name: row.name || undefined + name: row.name ?? undefined }; favoritesOut[row.id] = fav; favoriteOrder.push(row.id); @@ -67,16 +67,10 @@ favoriteRoutes.post("/favorites" + libraryRoute(), async (c) => { await db.insert(users).values({ id: userId }).onConflictDoNothing(); - const existingCount = await db - .select({ sortOrder: favorites.sortOrder }) - .from(favorites) - .where( - and( - eq(favorites.userId, userId), - eq(favorites.libraryId, libraryId) - ) - ) - .all(); + const existingFavorites = await db.$count( + favorites, + and(eq(favorites.userId, userId), eq(favorites.libraryId, libraryId)) + ); await db .insert(favorites) @@ -86,7 +80,7 @@ favoriteRoutes.post("/favorites" + libraryRoute(), async (c) => { libraryId, insertableId, name, - sortOrder: existingCount.length + sortOrder: existingFavorites }) .onConflictDoNothing(); @@ -133,7 +127,7 @@ favoriteRoutes.post("/favorite-order" + libraryRoute(), async (c) => { favoriteRoutes.post("/favorites/:favoriteId", async (c) => { const favoriteId = c.req.param("favoriteId"); const body = await c.req.json<{ - defaultConfiguration?: Configuration; + defaultConfiguration?: Configuration | null; name?: string; }>(); @@ -141,19 +135,7 @@ favoriteRoutes.post("/favorites/:favoriteId", async (c) => { return c.json({ error: "favoriteId is required" }, 400); } - const updateValues: { - defaultConfiguration?: Configuration | null; - name?: string; - } = {}; - - if (body.defaultConfiguration !== undefined) { - updateValues.defaultConfiguration = body.defaultConfiguration; - } - if (body.name !== undefined) { - updateValues.name = body.name; - } - - if (Object.keys(updateValues).length === 0) { + if (Object.keys(body).length === 0) { return c.json( { error: "defaultConfiguration or name is required" }, 400 @@ -161,10 +143,13 @@ favoriteRoutes.post("/favorites/:favoriteId", async (c) => { } const db = getDb(c.env.DB); - await db - .update(favorites) - .set(updateValues) - .where(eq(favorites.id, favoriteId)); + + if (Object.keys(body).length > 0) { + await db + .update(favorites) + .set(body) + .where(eq(favorites.id, favoriteId)); + } return c.json({ success: true }); }); diff --git a/src/frontend/cards/card-components.tsx b/src/frontend/cards/card-components.tsx index b6a4360d1..2bdae9203 100644 --- a/src/frontend/cards/card-components.tsx +++ b/src/frontend/cards/card-components.tsx @@ -112,6 +112,15 @@ export function QuickInsertItems(props: QuickInsertItemProps) { ); } +interface CardTitleVariants { + /** When a `titleComponent` is supplied, this is the override node. */ + titleOverride: ReactNode; + /** When `searchHit` is supplied, this is the highlighted title node. */ + highlightedTitle: ReactNode | null; + /** The plain fallback title node. */ + defaultTitle: ReactNode; +} + interface CardTitleProps { /** * True to use disabled text styles. @@ -125,7 +134,6 @@ interface CardTitleProps { showHiddenTag?: boolean; /** * The title to display. - * Ignored if SearchHit is provided. */ title: string; searchHit?: SearchHit; @@ -134,9 +142,14 @@ interface CardTitleProps { titleComponent?: ReactNode; /** Optional build-status badge rendered after the title. */ buildStatusBadge?: ReactNode; + /** + * Optional render prop that receives all title variants and returns the + * one that should be displayed. + */ + children?: (titles: CardTitleVariants) => ReactNode; } -export function CardTitle(props: CardTitleProps) { +export function CardTitleGroup(props: CardTitleProps) { const { searchHit, title, @@ -147,18 +160,23 @@ export function CardTitle(props: CardTitleProps) { const disabled = props.disabled ?? false; const isHidden = props.showHiddenTag ?? false; - let cardTitle: ReactNode; - if (titleComponent) { - cardTitle = titleComponent; - } else if (searchHit) { - cardTitle = ; - } else { - cardTitle = ( - - {title} - - ); - } + const titleOverride = titleComponent; + const highlightedTitle = searchHit ? ( + + ) : null; + const defaultTitle = ( + + {title} + + ); + + const cardTitle: ReactNode = props.children + ? props.children({ + titleOverride, + highlightedTitle, + defaultTitle + }) + : (titleOverride ?? highlightedTitle ?? defaultTitle); return ( diff --git a/src/frontend/cards/insertable-card.tsx b/src/frontend/cards/insertable-card.tsx index d072f2e52..8981d2228 100644 --- a/src/frontend/cards/insertable-card.tsx +++ b/src/frontend/cards/insertable-card.tsx @@ -25,7 +25,7 @@ import { useIsInsertableHidden, useSetVisibilityMutation } from "./card-hooks"; import { InsertableStatusBadge } from "./build-status"; import { AdminOptionsSubmenu, - CardTitle, + CardTitleGroup, ItemRow, OpenDocumentItems, QuickInsertItems, @@ -88,7 +88,7 @@ export function InsertableCard(props: InsertableCardProps): ReactNode { openInsertMenu({ insertable }); }} left={ - } diff --git a/src/shared/schema.ts b/src/shared/schema.ts index 097bb46b0..c2b95de39 100644 --- a/src/shared/schema.ts +++ b/src/shared/schema.ts @@ -147,7 +147,7 @@ export const favorites = sqliteTable( defaultConfiguration: text("default_configuration", { mode: "json" }).$type(), - name: text("name").notNull().default(""), + name: text("name").$type(), sortOrder: integer("sort_order").notNull().default(0) }, (t) => [unique().on(t.userId, t.libraryId, t.insertableId)] From 01f404391a95ca882673313370d6a060b9cf6ab9 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 5 Aug 2026 21:54:27 -0400 Subject: [PATCH 09/10] modified: src/frontend/favorites/favorite-card.tsx modified: src/frontend/favorites/favorite-search.ts --- src/frontend/favorites/favorite-card.tsx | 53 +++++++++++------------ src/frontend/favorites/favorite-search.ts | 4 +- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/frontend/favorites/favorite-card.tsx b/src/frontend/favorites/favorite-card.tsx index 0049d04c5..6713253d1 100644 --- a/src/frontend/favorites/favorite-card.tsx +++ b/src/frontend/favorites/favorite-card.tsx @@ -79,33 +79,32 @@ export function FavoriteCard(props: FavoriteCardProps): ReactNode { }); }; - const isSearchActive = Boolean(uiState.searchQuery && searchHit); - const titleComponent = - isSearchActive && searchHit ? ( - - ) : ( - setFavoriteName(event.currentTarget.value)} - onBlur={saveFavoriteName} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.currentTarget.blur(); - } - }} - onClick={(event) => event.stopPropagation()} - onFocus={(event) => event.stopPropagation()} - styles={{ input: { minWidth: 0 } }} - style={{ - width: "50%", - maxWidth: "100%", - minWidth: 0 - }} - /> - ); + const isSearchActive = uiState.searchQuery && searchHit; + const titleComponent = isSearchActive ? ( + + ) : ( + setFavoriteName(event.currentTarget.value)} + onBlur={saveFavoriteName} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.currentTarget.blur(); + } + }} + onClick={(event) => event.stopPropagation()} + onFocus={(event) => event.stopPropagation()} + styles={{ input: { minWidth: 0 } }} + style={{ + width: "50%", + maxWidth: "100%", + minWidth: 0 + }} + /> + ); return ( ; @@ -32,8 +31,7 @@ function buildFavoriteSearchDb( searchDb.addAll( favorites.map((favorite) => ({ id: favorite.id, - name: favorite.name ?? "", - favoriteId: favorite.id + name: favorite.name ?? "" })) ); From bbdbffb65c116092217503e6a82aa4151ddaea22 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 5 Aug 2026 22:09:51 -0400 Subject: [PATCH 10/10] completly vibe coded --- src/frontend/favorites/favorite-card.tsx | 17 ++-- src/frontend/favorites/favorite-menu.tsx | 2 +- .../favorites/favorite-search.test.ts | 59 ++++++++++++- src/frontend/favorites/favorite-search.ts | 85 +++++++++++++++---- src/frontend/favorites/favorites-list.tsx | 4 +- 5 files changed, 142 insertions(+), 25 deletions(-) diff --git a/src/frontend/favorites/favorite-card.tsx b/src/frontend/favorites/favorite-card.tsx index 6713253d1..585e10394 100644 --- a/src/frontend/favorites/favorite-card.tsx +++ b/src/frontend/favorites/favorite-card.tsx @@ -33,7 +33,6 @@ import { getAppErrorHandler } from "../api-utils/errors"; import { favoritesQueryKey, useFavoritesQuery } from "../queries"; import { produce } from "immer"; import { SearchHit } from "../search/insertable-search"; -import { SearchHitTitle } from "../search/search-results"; import { toLibraryPath, useLibraryId } from "../api-utils/library"; interface FavoriteCardProps { @@ -80,8 +79,8 @@ export function FavoriteCard(props: FavoriteCardProps): ReactNode { }; const isSearchActive = uiState.searchQuery && searchHit; - const titleComponent = isSearchActive ? ( - + const titleContent = isSearchActive ? ( + favoriteName ) : ( + > + {({ defaultTitle }) => { + if (isSearchActive) { + return defaultTitle; + } + return titleContent; + }} + } rightSection={ @@ -244,7 +249,7 @@ function useUpdateFavoriteNameMutation() { return useMutation({ mutationKey: ["update-favorite-name"], mutationFn: async (args: { favoriteId: string; name: string }) => { - return apiPost("/favorite/" + args.favoriteId, { + return apiPost("/favorites/" + args.favoriteId, { body: { name: args.name } }); }, diff --git a/src/frontend/favorites/favorite-menu.tsx b/src/frontend/favorites/favorite-menu.tsx index 9b6f476a0..3437a8c2b 100644 --- a/src/frontend/favorites/favorite-menu.tsx +++ b/src/frontend/favorites/favorite-menu.tsx @@ -70,7 +70,7 @@ function FavoriteMenuContent(props: FavoriteMenuContentProps): ReactNode { const setDefaultConfigurationMutation = useMutation({ mutationKey: ["set-default-configuration"], mutationFn: async () => { - return apiPost("/favorite/" + favoriteId, { + return apiPost("/favorites/" + favoriteId, { body: { defaultConfiguration: configuration } }); }, diff --git a/src/frontend/favorites/favorite-search.test.ts b/src/frontend/favorites/favorite-search.test.ts index 406e261b2..96f5479cb 100644 --- a/src/frontend/favorites/favorite-search.test.ts +++ b/src/frontend/favorites/favorite-search.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { LibraryId } from "../../shared/types"; -import type { Favorite } from "../../shared/api-models"; +import { LibraryId, Vendor } from "../../shared/types"; +import type { Favorite, InsertableOut } from "../../shared/api-models"; import { filterFavoritesForSearch } from "./favorite-search"; describe("filterFavoritesForSearch", () => { @@ -50,4 +50,59 @@ describe("filterFavoritesForSearch", () => { filterFavoritesForSearch(favorites, "different-insertable") ).toEqual([]); }); + + it("falls back to the insertable name when the favorite has no custom name", () => { + const favorites: Favorite[] = [ + { + id: "fav-3", + insertableId: "insertable-3", + libraryId: LibraryId.FRC_DESIGN_LIB + } + ]; + + const insertables = { + "insertable-3": { + name: "Wheel Assembly", + vendors: [Vendor.REV] + } as InsertableOut + }; + + expect( + filterFavoritesForSearch(favorites, "assembly", insertables).map( + (result) => result.favorite.id + ) + ).toEqual(["fav-3"]); + }); + + it("filters favorites by vendor when a vendor filter is active", () => { + const favorites: Favorite[] = [ + { + id: "fav-rev", + insertableId: "insertable-rev", + libraryId: LibraryId.FRC_DESIGN_LIB + }, + { + id: "fav-vex", + insertableId: "insertable-vex", + libraryId: LibraryId.FRC_DESIGN_LIB + } + ]; + + const insertables = { + "insertable-rev": { + name: "REV Wheel", + vendors: [Vendor.REV] + } as InsertableOut, + "insertable-vex": { + name: "VEX Wheel", + vendors: [Vendor.VEX] + } as InsertableOut + }; + + expect( + filterFavoritesForSearch(favorites, "wheel", insertables, [ + Vendor.REV + ]).map((result) => result.favorite.id) + ).toEqual(["fav-rev"]); + }); }); diff --git a/src/frontend/favorites/favorite-search.ts b/src/frontend/favorites/favorite-search.ts index 119f41ba3..01ba42730 100644 --- a/src/frontend/favorites/favorite-search.ts +++ b/src/frontend/favorites/favorite-search.ts @@ -1,19 +1,22 @@ import MiniSearch, { Options } from "minisearch"; -import { Favorite } from "../../shared/api-models"; +import { Favorite, InsertableOut } from "../../shared/api-models"; +import { Vendor } from "../../shared/types"; import { tokenize, TypedMiniSearchResult } from "../../shared/search-utils"; import { processTerm } from "../../shared/search-utils"; import { Position, SearchHit } from "../search/insertable-search"; interface FavoriteSearchDocument { id: string; + favoriteId: string; name: string; + vendors: Vendor[]; } type FavoriteSearchResult = TypedMiniSearchResult; const FAVORITE_SEARCH_OPTIONS: Options = { - fields: ["name", "favoriteId"], - storeFields: ["id", "name"], + fields: ["name"], + storeFields: ["id", "name", "vendors"], searchOptions: { prefix: true }, @@ -21,8 +24,21 @@ const FAVORITE_SEARCH_OPTIONS: Options = { processTerm }; +function getFavoriteSearchName( + favorite: Favorite, + insertables?: Record +): string { + if (favorite.name) { + return favorite.name; + } + + const insertable = insertables?.[favorite.insertableId]; + return insertable?.name ?? ""; +} + function buildFavoriteSearchDb( - favorites: Favorite[] + favorites: Favorite[], + insertables?: Record ): MiniSearch { const searchDb = new MiniSearch( FAVORITE_SEARCH_OPTIONS @@ -31,7 +47,9 @@ function buildFavoriteSearchDb( searchDb.addAll( favorites.map((favorite) => ({ id: favorite.id, - name: favorite.name ?? "" + favoriteId: favorite.id, + name: getFavoriteSearchName(favorite, insertables), + vendors: insertables?.[favorite.insertableId]?.vendors ?? [] })) ); @@ -61,28 +79,36 @@ function generateHighlightPositions(result: FavoriteSearchResult): Position[] { export interface FilteredFavoritesResult { favorite: Favorite; searchHit: SearchHit | undefined; + vendors: Vendor[]; } export function filterFavoritesForSearch( favorites: Favorite[], - query?: string + query?: string, + insertables?: Record, + vendors?: Vendor[] ): FilteredFavoritesResult[] { if (!query || query.trim() === "") { return favorites.map((favorite) => ({ favorite, - searchHit: undefined + searchHit: undefined, + vendors: insertables?.[favorite.insertableId]?.vendors ?? [] })); } - const searchDb = buildFavoriteSearchDb(favorites); + const searchDb = buildFavoriteSearchDb(favorites, insertables); const miniSearchResults = searchDb.search(query, { - filter: () => { - // const searchResult = result as FavoriteSearchResult; - return true; + filter: (result) => { + const searchResult = result as FavoriteSearchResult; + if (!vendors || vendors.length === 0) { + return true; + } + const vendorSet = new Set(vendors); + return searchResult.vendors.some((vendor) => vendorSet.has(vendor)); } }) as FavoriteSearchResult[]; - const results: FilteredFavoritesResult[] = []; + const resultsById = new Map(); for (const miniSearchResult of miniSearchResults) { const favorite = favorites.find( @@ -92,14 +118,43 @@ export function filterFavoritesForSearch( continue; } - results.push({ + resultsById.set(favorite.id, { favorite, searchHit: { id: favorite.id, positions: generateHighlightPositions(miniSearchResult) - } + }, + vendors: miniSearchResult.vendors }); } - return results; + const queryText = query.trim().toLowerCase(); + for (const favorite of favorites) { + const matchesId = favorite.id.toLowerCase().includes(queryText); + if (!matchesId) { + continue; + } + + const insertable = insertables?.[favorite.insertableId]; + const vendorList = insertable?.vendors ?? []; + if (vendors && vendors.length > 0) { + const vendorSet = new Set(vendors); + if (!vendorList.some((vendor) => vendorSet.has(vendor))) { + continue; + } + } + + if (!resultsById.has(favorite.id)) { + resultsById.set(favorite.id, { + favorite, + searchHit: { + id: favorite.id, + positions: [] + }, + vendors: vendorList + }); + } + } + + return Array.from(resultsById.values()); } diff --git a/src/frontend/favorites/favorites-list.tsx b/src/frontend/favorites/favorites-list.tsx index e34c15b31..8a463c955 100644 --- a/src/frontend/favorites/favorites-list.tsx +++ b/src/frontend/favorites/favorites-list.tsx @@ -60,7 +60,9 @@ export function FavoritesList(): ReactNode { if (uiState.searchQuery) { const matchedFavorites = filterFavoritesForSearch( orderedFavorites, - uiState.searchQuery + uiState.searchQuery, + insertables, + uiState.vendorFilters ); const matchedInsertables = matchedFavorites