Skip to content

Commit 6c9a0c9

Browse files
committed
Add Sprites search and filters
1 parent 3b95645 commit 6c9a0c9

11 files changed

Lines changed: 696 additions & 479 deletions

assets/toolbox/sprites/js/index.js

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ const elements = {
77
add: document.querySelector("[data-sprites-add]"),
88
apiStatus: document.querySelector("[data-sprites-api-status]"),
99
count: document.querySelector("[data-sprites-count]"),
10+
categoryFilter: document.querySelector("[data-sprites-category-filter]"),
11+
clearFilters: document.querySelector("[data-sprites-clear-filters]"),
1012
emptyState: document.querySelector("[data-sprites-empty-state]"),
1113
errorState: document.querySelector("[data-sprites-error-state]"),
14+
filterStatus: document.querySelector("[data-sprites-filter-status]"),
1215
libraryStatus: document.querySelector("[data-sprites-library-status]"),
1316
metadata: document.querySelector("[data-sprites-metadata]"),
1417
outputStatus: document.querySelector("[data-sprites-output-status]"),
@@ -19,8 +22,11 @@ const elements = {
1922
refresh: document.querySelector("[data-sprites-refresh]"),
2023
replace: document.querySelector("[data-sprites-replace]"),
2124
replaceStatus: document.querySelector("[data-sprites-replace-status]"),
25+
search: document.querySelector("[data-sprites-search]"),
2226
storageStatus: document.querySelector("[data-sprites-storage-status]"),
27+
statusFilter: document.querySelector("[data-sprites-status-filter]"),
2328
tableBody: document.querySelector("[data-sprites-table-body]"),
29+
tagFilter: document.querySelector("[data-sprites-tag-filter]"),
2430
updated: document.querySelector("[data-sprites-updated]"),
2531
validation: document.querySelector("[data-sprites-validation]"),
2632
duplicate: document.querySelector("[data-sprites-duplicate]"),
@@ -172,6 +178,16 @@ function paletteKeysFor(sprite) {
172178
return [];
173179
}
174180

181+
function tagKeysFor(sprite) {
182+
if (Array.isArray(sprite?.tagKeys)) {
183+
return sprite.tagKeys.map((key) => String(key || "").trim()).filter(Boolean);
184+
}
185+
if (Array.isArray(sprite?.tag_keys)) {
186+
return sprite.tag_keys.map((key) => String(key || "").trim()).filter(Boolean);
187+
}
188+
return [];
189+
}
190+
175191
function usageCountFor(sprite) {
176192
const count = Number(sprite?.usageCount ?? sprite?.usage_count ?? sprite?.references?.length);
177193
return Number.isFinite(count) && count >= 0 ? String(count) : "0";
@@ -192,12 +208,93 @@ function spriteRowsFromPayload(payload) {
192208
return [];
193209
}
194210

211+
function uniqueSorted(values) {
212+
return [...new Set(values.map((value) => String(value || "").trim()).filter(Boolean))]
213+
.sort((left, right) => left.localeCompare(right, undefined, { sensitivity: "base" }));
214+
}
215+
216+
function setSelectOptions(select, values, allLabel) {
217+
if (!select) {
218+
return;
219+
}
220+
const current = select.value;
221+
const options = [""].concat(values);
222+
select.replaceChildren(...options.map((value) => {
223+
const option = document.createElement("option");
224+
option.value = value;
225+
option.textContent = value || allLabel;
226+
return option;
227+
}));
228+
select.value = options.includes(current) ? current : "";
229+
}
230+
231+
function renderFilterOptions(sprites) {
232+
setSelectOptions(elements.statusFilter, SPRITE_STATUSES, "All statuses");
233+
setSelectOptions(elements.categoryFilter, uniqueSorted(sprites.map((sprite) => sprite.category)), "All categories");
234+
setSelectOptions(elements.tagFilter, uniqueSorted(sprites.flatMap(tagKeysFor)), "All tag keys");
235+
}
236+
237+
function filterValues() {
238+
return {
239+
category: String(elements.categoryFilter?.value || "").trim(),
240+
search: String(elements.search?.value || "").trim().toLowerCase(),
241+
status: String(elements.statusFilter?.value || "").trim(),
242+
tagKey: String(elements.tagFilter?.value || "").trim(),
243+
};
244+
}
245+
246+
function spriteMatchesFilters(sprite, filters) {
247+
if (filters.status && sprite.status !== filters.status) {
248+
return false;
249+
}
250+
if (filters.category && sprite.category !== filters.category) {
251+
return false;
252+
}
253+
if (filters.tagKey && !tagKeysFor(sprite).includes(filters.tagKey)) {
254+
return false;
255+
}
256+
if (!filters.search) {
257+
return true;
258+
}
259+
const haystack = [
260+
sprite.name,
261+
sprite.status,
262+
sprite.category,
263+
sprite.source,
264+
sprite.storagePath,
265+
...tagKeysFor(sprite),
266+
...paletteKeysFor(sprite),
267+
].map((value) => String(value || "").toLowerCase()).join(" ");
268+
return haystack.includes(filters.search);
269+
}
270+
271+
function filteredSprites() {
272+
const filters = filterValues();
273+
return currentSprites.filter((sprite) => spriteMatchesFilters(sprite, filters));
274+
}
275+
276+
function renderFilterStatus(visibleCount, totalCount) {
277+
if (totalCount === 0) {
278+
setText(elements.filterStatus, "No API-backed Sprites records are available to filter.");
279+
return;
280+
}
281+
const filters = filterValues();
282+
const activeFilters = Object.values(filters).filter(Boolean).length;
283+
setText(
284+
elements.filterStatus,
285+
activeFilters > 0
286+
? `${visibleCount} of ${totalCount} Sprites records match current filters.`
287+
: `${totalCount} Sprites records available.`
288+
);
289+
}
290+
195291
function renderLoading() {
196292
setText(elements.apiStatus, "Loading");
197293
setText(elements.libraryStatus, "Loading");
198294
setText(elements.outputStatus, "Loading");
199295
setText(elements.outputSummary, "Waiting for Sprites API response.");
200296
setActionStatus("Loading Sprites records.");
297+
setText(elements.filterStatus, "Filters load with API-backed Sprites records.");
201298
setText(elements.storageStatus, "Storage import is checking API capabilities.");
202299
setText(elements.replaceStatus, "Select a sprite to update source metadata through the API.");
203300
setText(elements.emptyState, "Loading Sprites records.");
@@ -226,6 +323,7 @@ function renderUnavailable(message) {
226323
setText(elements.paletteStatus, "Palette/Colors references unavailable until Sprites records load from the API.");
227324
setText(elements.paletteSelectionStatus, "Palette/Colors selection unavailable until API-backed key records are available.");
228325
setText(elements.storageStatus, "Storage import unavailable because the Sprites API is not responding.");
326+
setText(elements.filterStatus, "Filters unavailable until Sprites records load from the API.");
229327
setText(elements.replaceStatus, "Replace metadata unavailable until the Sprites API responds.");
230328
renderPreviewPanel(null);
231329
setText(elements.updated, new Date().toLocaleTimeString());
@@ -316,13 +414,13 @@ function selectSprite(sprite) {
316414
renderPreviewPanel(sprite);
317415
}
318416

319-
function renderRows(sprites) {
417+
function renderRows(sprites, emptyMessage = "No Sprites records returned by the API.") {
320418
if (!elements.tableBody) {
321419
return;
322420
}
323421
if (sprites.length === 0) {
324422
const row = document.createElement("tr");
325-
const cell = createCell("No Sprites records returned by the API.");
423+
const cell = createCell(emptyMessage);
326424
cell.colSpan = 9;
327425
row.append(cell);
328426
elements.tableBody.replaceChildren(...(editingKey === "__new__" ? [createEditRow(), row] : [row]));
@@ -425,11 +523,13 @@ function renderSprites(payload) {
425523
const sprites = spriteRowsFromPayload(payload);
426524
currentSprites = sprites;
427525
const count = sprites.length;
526+
renderFilterOptions(sprites);
527+
const visibleSprites = filteredSprites();
428528
setText(elements.apiStatus, "Ready");
429529
setText(elements.libraryStatus, count > 0 ? "Ready" : "Empty");
430530
setText(elements.count, String(count));
431531
setText(elements.outputStatus, count > 0 ? "Ready" : "Empty");
432-
setText(elements.outputSummary, count > 0 ? `${count} sprite record${count === 1 ? "" : "s"} loaded from the API.` : "Sprites API responded with no records.");
532+
setText(elements.outputSummary, count > 0 ? `${visibleSprites.length} of ${count} sprite record${count === 1 ? "" : "s"} displayed from the API.` : "Sprites API responded with no records.");
433533
setText(elements.emptyState, count > 0 ? "" : "No Sprites records returned by the API.");
434534
setText(elements.updated, new Date().toLocaleTimeString());
435535
setText(elements.metadata, count > 0 ? "Select a sprite row to review its metadata." : "No sprite metadata available yet.");
@@ -440,7 +540,11 @@ function renderSprites(payload) {
440540
setHidden(elements.emptyState, count > 0);
441541
setHidden(elements.errorState, true);
442542
renderPaletteStatus(sprites);
443-
renderRows(sprites);
543+
renderFilterStatus(visibleSprites.length, count);
544+
renderRows(
545+
visibleSprites,
546+
count > 0 ? "No Sprites records match current filters." : "No Sprites records returned by the API."
547+
);
444548
}
445549

446550
function bodyFromSprite(sprite, overrides = {}) {
@@ -648,7 +752,7 @@ elements.refresh?.addEventListener("click", () => {
648752

649753
elements.add?.addEventListener("click", () => {
650754
editingKey = "__new__";
651-
renderRows(currentSprites);
755+
renderRows(filteredSprites());
652756
setActionStatus("New sprite row ready. Name and status are required.");
653757
});
654758

@@ -665,13 +769,13 @@ elements.tableBody?.addEventListener("click", (event) => {
665769
const duplicateKey = target.dataset.spritesDuplicateRow;
666770
if (editKey !== undefined) {
667771
editingKey = editKey;
668-
renderRows(currentSprites);
772+
renderRows(filteredSprites());
669773
setActionStatus("Editing sprite row. Name and status are required.");
670774
return;
671775
}
672776
if (cancelKey !== undefined) {
673777
editingKey = "";
674-
renderRows(currentSprites);
778+
renderRows(filteredSprites());
675779
setActionStatus("Sprite edit cancelled.");
676780
return;
677781
}
@@ -703,4 +807,37 @@ elements.replace?.addEventListener("click", () => {
703807
void replaceSpriteMetadata(selectedSpriteKey);
704808
});
705809

810+
[elements.search, elements.statusFilter, elements.categoryFilter, elements.tagFilter].forEach((control) => {
811+
control?.addEventListener("input", () => {
812+
editingKey = "";
813+
const visibleSprites = filteredSprites();
814+
renderFilterStatus(visibleSprites.length, currentSprites.length);
815+
renderRows(visibleSprites, "No Sprites records match current filters.");
816+
});
817+
control?.addEventListener("change", () => {
818+
editingKey = "";
819+
const visibleSprites = filteredSprites();
820+
renderFilterStatus(visibleSprites.length, currentSprites.length);
821+
renderRows(visibleSprites, "No Sprites records match current filters.");
822+
});
823+
});
824+
825+
elements.clearFilters?.addEventListener("click", () => {
826+
if (elements.search) {
827+
elements.search.value = "";
828+
}
829+
if (elements.statusFilter) {
830+
elements.statusFilter.value = "";
831+
}
832+
if (elements.categoryFilter) {
833+
elements.categoryFilter.value = "";
834+
}
835+
if (elements.tagFilter) {
836+
elements.tagFilter.value = "";
837+
}
838+
const visibleSprites = filteredSprites();
839+
renderFilterStatus(visibleSprites.length, currentSprites.length);
840+
renderRows(visibleSprites);
841+
});
842+
706843
void loadSprites();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# PR_26177_CHARLIE_014 Branch Validation
2+
3+
Status: PASS
4+
5+
## Checks
6+
7+
- PASS: PR014 was created as a stacked branch from `PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette`.
8+
- PASS: Stacking is required because search/filter controls build on the PR013 Sprites table and metadata shell.
9+
- PASS: Current work branch is `PR_26177_CHARLIE_014-sprites-tags-categories-search`.
10+
- PASS: Branch contains only the Sprites tags/categories/search PR scope relative to PR013.
11+
- PASS: No merge was performed.
12+
- PASS: No `start_of_day` path is changed.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# PR_26177_CHARLIE_014 Manual Validation Notes
2+
3+
Status: PASS
4+
5+
## Manual Review
6+
7+
- Verified Search filters by API-returned sprite fields and tag/palette key text.
8+
- Verified category options are derived from current API records.
9+
- Verified tag key options are derived from current API records.
10+
- Verified status filtering uses the Sprites status contract.
11+
- Verified clear filters restores the unfiltered API-backed table.
12+
- Verified no static category or tag product-data list was introduced.
13+
- Verified Sprites does not duplicate Tags ownership.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# PR_26177_CHARLIE_014 Requirements Checklist
2+
3+
Status: PASS
4+
5+
- PASS: Added search for Sprites.
6+
- PASS: Added status filter.
7+
- PASS: Added categories filter.
8+
- PASS: Added tag key filter.
9+
- PASS: Search and filters use API/database-backed sprite data.
10+
- PASS: Categories are derived from API-backed records.
11+
- PASS: Tag keys are derived from API-backed records.
12+
- PASS: Did not create Sprite-owned Tags data.
13+
- PASS: Did not use page-local product arrays for categories or tags.
14+
- PASS: Added table filtering UX consistent with GFS patterns.
15+
- PASS: Did not add browser storage product-data source of truth.
16+
- PASS: Did not introduce MEM DB, local-mem, fake-login, or silent fallback.
17+
- PASS: Targeted Playwright coverage passed.
18+
- PASS: Required report artifacts were created.
19+
- PASS: Repo-structured ZIP artifact was created under `tmp/`.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# PR_26177_CHARLIE_014 Validation Lane
2+
3+
Status: PASS
4+
5+
## Commands
6+
7+
```powershell
8+
rg -n "<style|style=|onclick=|onchange=|oninput=|onsubmit=|<script>" toolbox/sprites/index.html assets/toolbox/sprites/js/index.js tests/playwright/tools/SpritesToolShell.spec.mjs
9+
```
10+
11+
Result: PASS, no matches.
12+
13+
```powershell
14+
rg -n "localStorage|sessionStorage|indexedDB|imageDataUrl|MEM DB|local-mem|fake-login|silent fallback" toolbox/sprites/index.html assets/toolbox/sprites/js/index.js tests/playwright/tools/SpritesToolShell.spec.mjs
15+
```
16+
17+
Result: PASS, no matches.
18+
19+
```powershell
20+
git diff --check
21+
```
22+
23+
Result: PASS. Git reported only repository line-ending warnings for changed HTML/test files.
24+
25+
```powershell
26+
node ./node_modules/@playwright/test/cli.js test tests/playwright/tools/SpritesToolShell.spec.mjs --project=playwright --workers=1 --reporter=list
27+
```
28+
29+
Result: PASS, 9 passed.
30+
31+
## Playwright Coverage
32+
33+
Targeted Playwright coverage updated `docs_build/dev/reports/playwright_v8_coverage_report.txt` for the Sprites browser module.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# PR_26177_CHARLIE_014-sprites-tags-categories-search
2+
3+
Team: Charlie
4+
5+
Status: PASS
6+
7+
## Scope
8+
9+
Added API-backed search and filters for Sprites records. Categories and tag keys are derived from the API response; Sprites does not create a separate tag authority or page-local product data list.
10+
11+
## Changed Files
12+
13+
- `toolbox/sprites/index.html`
14+
- `assets/toolbox/sprites/js/index.js`
15+
- `tests/playwright/tools/SpritesToolShell.spec.mjs`
16+
- `docs_build/dev/reports/playwright_v8_coverage_report.txt`
17+
- `docs_build/dev/reports/codex_review.diff`
18+
- `docs_build/dev/reports/codex_changed_files.txt`
19+
- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md`
20+
- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md`
21+
- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md`
22+
- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md`
23+
- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md`
24+
25+
## Implementation Notes
26+
27+
- Added search control.
28+
- Added status filter using the Sprites API status contract.
29+
- Added category filter derived from API-backed sprite records.
30+
- Added tag key filter derived from API-backed sprite records.
31+
- Added clear filters action.
32+
- Added filter status summary for visible records versus total API-backed records.
33+
- Did not duplicate Tags ownership or add page-local reusable product arrays.
34+
35+
## Validation
36+
37+
- PASS: `git diff --check`
38+
- PASS: inline CSS/script/handler scan for Sprites files found no matches.
39+
- PASS: browser storage and forbidden local data pattern scan found no matches.
40+
- PASS: no `start_of_day` files changed.
41+
- PASS: `node ./node_modules/@playwright/test/cli.js test tests/playwright/tools/SpritesToolShell.spec.mjs --project=playwright --workers=1 --reporter=list`
42+
43+
## ZIP Artifact
44+
45+
- `tmp/PR_26177_CHARLIE_014-sprites-tags-categories-search_delta.zip`

0 commit comments

Comments
 (0)