phase: 105_hidden_folders_toggle
All completion criteria verified. Everything is green. **Phase 105 final verification pass — all criteria verified** - Verified the full implementation in the working tree: `git_sources.include_hidden` column + alembic `0019` (dev DB at head, column present), `iter_importable_files`/`import_sources` flag support with `str(root)`-keyed map used by both walk and progress pre-walk, `GitSourcePatchIn` rename with optional fields, sync/CLI pipeline wiring (OR-collision), and the per-row "Hidden" checkbox + tag + error line on the Sources page - Unit + integration: `uv run pytest` → exit 0 (2148 tests collected, all pass; this sandbox occasionally swallows pytest's final status line — exit codes verified) - Coverage: `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (3879 stmts, 15 miss) — >90% gate ✓ - Dedicated E2E: `uv run pytest tests/e2e/test_hidden_folders_toggle.py -v --no-cov` → **6 passed in 23.20s** (DB up, in isolation) - Regression E2E in isolation: `test_source_ignore_paths` 6 passed, `test_git_sources_admin` 6 passed, `test_local_directory_sources` 3 passed, `test_sync_button` 3 passed, `test_smoke` 3 passed - Lint/types: `uv run ruff check .` + `uv run pyright` → clean (0 errors/warnings) **Completion criteria:** (1) checkbox persists via PATCH 200 → "hidden on" tag + GET round-trips `include_hidden: true`; failure path reverts box + `role="alert"` canned message ✓; (2) flag OFF byte-identical (only `visible.md` indexed), ON indexes `.hidden/note.md` into the KB catalog, `EXCLUDED_DIRS` excluded both states ✓; (3) A2: flag OFF → `detail.pruned==1`, doc gone from catalog ✓; (4) PATCH bool-only/list-only/both/neither no-op, phase-89 fixed 422s unchanged, 404, anonymous 403 (incl. bool-only body) ✓; (5) env-fallback rows render no checkbox, WCAG-clean (aria-label, keyboard focus, visible label, text tag) ✓; (6) full gate green ✓; (7) commit left to the harness per instructions (no `git add`/`commit` run; phase files untouched). **Deviations:** none — no defects found; no code changes were needed on this pass. **Next pending phase:** `.agents/phases/todo/98_sync_summary_visibility`.
This commit is contained in:
@@ -178,6 +178,31 @@
|
||||
* tag next to the location <code> (text + a distinct background —
|
||||
* never color alone, WCAG 1.4.1).
|
||||
*
|
||||
* Phase 105 (task 05) — the per-row "Hidden" toggle: every STORED
|
||||
* row (s.id truthy — env-fallback rows get NO checkbox, A3: nothing
|
||||
* is stored to flag) gets a native labeled checkbox (visible
|
||||
* "Hidden" text + the checkbox's own aria-label naming the full
|
||||
* source value — setAttribute, never innerHTML) in the actions
|
||||
* cell, LEFT of the "Ignore paths" button (Remove stays last).
|
||||
* Checked state comes ONLY from the server row (s.include_hidden —
|
||||
* makeRow renders server state, never a prior local flip). When on,
|
||||
* the source cell shows a "hidden on" text tag (text + background,
|
||||
* never color alone — WCAG 1.4.1). Flipping runs the §7.4
|
||||
* never-stale lifecycle (toggleHidden): the box disables at once
|
||||
* (no double-flip), PATCH /api/git-sources/{id} with ONLY
|
||||
* { include_hidden } (task 03's optional field — the row's ignore
|
||||
* list is untouched); 200 → the error line clears, loadSources()
|
||||
* (the row re-renders from the server, the "hidden on" tag lands)
|
||||
* and the confirmation is the LAST announcement (the reload's "N
|
||||
* sources listed." lands first — the phase-89 order); failure →
|
||||
* the box REVERTS to the server state and re-enables, and the
|
||||
* detail lands in the page-level #git-sources-hidden-error line
|
||||
* (role=alert — the checkbox is a table-cell control, so its error
|
||||
* is at page level; the ignore dialog carries its own INSIDE the
|
||||
* modal). A healed load clears the line (loadSources' success path
|
||||
* calls hideHiddenError — the phase-89 "happy path heals the error
|
||||
* state" precedent).
|
||||
*
|
||||
* Scope boundary (phase locked decisions): adding a git repo does
|
||||
* NOT clone — the sync service (server-side) does that. Removing a
|
||||
* source, however, performs the FULL cleanup server-side (phase 69):
|
||||
@@ -253,6 +278,10 @@ export async function mount(root) {
|
||||
const ignoreErrorEl = root.querySelector("#ignore-editor-error");
|
||||
const ignoreCancelBtn = root.querySelector("#ignore-editor-cancel");
|
||||
const ignoreSaveBtn = root.querySelector("#ignore-editor-save");
|
||||
/* Phase 105: the per-row "Hidden" toggle's page-level error line
|
||||
(the checkbox is a table-cell control — no dialog of its own —
|
||||
so its failure announces here, next to the table). */
|
||||
const hiddenErrorEl = root.querySelector("#git-sources-hidden-error");
|
||||
|
||||
/* Polite live region: the screen-reader confirmation for loads, adds,
|
||||
and removals (the phase-15 announcer pattern). */
|
||||
@@ -312,6 +341,10 @@ export async function mount(root) {
|
||||
return;
|
||||
}
|
||||
hideLoadError();
|
||||
/* Phase 105: a healed list clears the stale hidden-toggle error
|
||||
line (the phase-89 "happy path heals the error state" — the
|
||||
same hideLoadError precedent). */
|
||||
hideHiddenError();
|
||||
const sources = Array.isArray(data.sources) ? data.sources : [];
|
||||
renderSources(sources, data.from_env === true);
|
||||
announce(`${sources.length} source${sources.length === 1 ? "" : "s"} listed.`);
|
||||
@@ -381,6 +414,16 @@ export async function mount(root) {
|
||||
count.textContent = `${s.ignore_paths.length} ignored`;
|
||||
urlTd.append(count);
|
||||
}
|
||||
/* Phase 105 (A5): the "hidden on" state tag — the
|
||||
.git-source-ignore-count idiom (TEXT + background, never
|
||||
color alone — WCAG 1.4.1), so the flag is readable at a
|
||||
glance without hovering the checkbox. */
|
||||
if (s.id && s.include_hidden === true) {
|
||||
const hiddenTag = document.createElement("span");
|
||||
hiddenTag.className = "git-source-hidden-count";
|
||||
hiddenTag.textContent = "hidden on";
|
||||
urlTd.append(hiddenTag);
|
||||
}
|
||||
tr.appendChild(urlTd);
|
||||
|
||||
const addedTd = document.createElement("td");
|
||||
@@ -390,6 +433,29 @@ export async function mount(root) {
|
||||
const actTd = document.createElement("td");
|
||||
actTd.className = "git-source-actions-cell";
|
||||
if (s.id) {
|
||||
/* Phase 105 (A5): the per-row hidden-folders toggle — a native
|
||||
labeled checkbox (the WCAG focus/label idiom) LEFT of the
|
||||
"Ignore paths" button; Remove stays last. Stored rows only
|
||||
(A3 — env-fallback rows fall through to the "from .env"
|
||||
tag). The aria-label is the ONLY place `value` appears
|
||||
(setAttribute — never innerHTML). Checked state comes from
|
||||
the SERVER row (s.include_hidden), never from a prior local
|
||||
flip (§7.4 — makeRow only ever renders server state). */
|
||||
const hiddenLabel = document.createElement("label");
|
||||
hiddenLabel.className = "git-source-hidden";
|
||||
hiddenLabel.title =
|
||||
"When checked, files inside hidden (dot) folders are indexed on the next sync. Caches (.git, node_modules, .venv, …) stay excluded.";
|
||||
const hiddenBox = document.createElement("input");
|
||||
hiddenBox.type = "checkbox";
|
||||
hiddenBox.className = "git-source-hidden-box";
|
||||
hiddenBox.checked = s.include_hidden === true;
|
||||
hiddenBox.setAttribute(
|
||||
"aria-label",
|
||||
`Index hidden folders for ${kindLabel} source: ${value}`,
|
||||
);
|
||||
hiddenLabel.append(hiddenBox, document.createTextNode("Hidden"));
|
||||
hiddenBox.addEventListener("change", () => toggleHidden(s, hiddenBox));
|
||||
actTd.appendChild(hiddenLabel);
|
||||
/* Phase 89: the per-row ignore-paths editor trigger (A3: stored
|
||||
rows only — env-fallback rows fall through to the "from
|
||||
.env" tag below). The aria-label is the ONLY place `value`
|
||||
@@ -747,6 +813,59 @@ export async function mount(root) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- hidden folders (PATCH /api/git-sources/{id}) — the per-row toggle ----------
|
||||
* Phase 105: the hidden-folders toggle — PATCH { include_hidden }
|
||||
* only (the row's list is untouched — the PATCH body's optional
|
||||
* fields, task 03). §7.4 never-stale: the box disables at once
|
||||
* (no double-flip while the PATCH is out); on 200 the row
|
||||
* re-renders from the server (loadSources) and the CONFIRMATION is
|
||||
* the LAST announcement (the reload's "N sources listed." lands
|
||||
* first — the phase-89 order); on failure the box REVERTS to the
|
||||
* server state and the detail lands in #git-sources-hidden-error
|
||||
* (role=alert). The checkbox is a table-cell control (no dialog of
|
||||
* its own — the ignore editor carries its error INSIDE the modal),
|
||||
* so this toggle's error is the PAGE-level line; a healed
|
||||
* loadSources clears it (hideHiddenError on the success path). */
|
||||
function toggleHidden(s, box) {
|
||||
const value = s.kind === "local" ? (s.path ?? s.url) : s.url;
|
||||
const wanted = box.checked;
|
||||
box.disabled = true; // a PATCH is out — the box must not flip twice
|
||||
fetch(`/api/git-sources/${s.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({ include_hidden: wanted }),
|
||||
})
|
||||
.then(async (r) => {
|
||||
if (r.ok) {
|
||||
hideHiddenError();
|
||||
await loadSources();
|
||||
announce(`Hidden folders ${wanted ? "enabled" : "disabled"} for ${value}.`);
|
||||
return;
|
||||
}
|
||||
const detail = await apiDetail(
|
||||
r, `Could not update the hidden-folders setting (${r.status}).`,
|
||||
);
|
||||
showHiddenError(detail);
|
||||
box.checked = s.include_hidden === true; // revert to server state
|
||||
box.disabled = false;
|
||||
})
|
||||
.catch(() => {
|
||||
showHiddenError("Could not reach the server — the setting was not changed.");
|
||||
box.checked = s.include_hidden === true;
|
||||
box.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function showHiddenError(message) {
|
||||
if (hiddenErrorEl) hiddenErrorEl.textContent = message;
|
||||
if (hiddenErrorEl) hiddenErrorEl.hidden = false;
|
||||
}
|
||||
function hideHiddenError() {
|
||||
if (hiddenErrorEl) hiddenErrorEl.textContent = "";
|
||||
if (hiddenErrorEl) hiddenErrorEl.hidden = true;
|
||||
}
|
||||
|
||||
/* The dialog's own buttons (static markup — wired once). */
|
||||
if (ignoreCancelBtn) ignoreCancelBtn.addEventListener("click", cancelIgnoreEditor);
|
||||
if (ignoreBackdrop) ignoreBackdrop.addEventListener("click", cancelIgnoreEditor);
|
||||
|
||||
Reference in New Issue
Block a user