Skip to content

feat(documents): hide-linked toggle filters system-wide (#1557)#1559

Merged
steilerDev merged 3 commits into
betafrom
feat/1557-hide-system-linked-documents
May 22, 2026
Merged

feat(documents): hide-linked toggle filters system-wide (#1557)#1559
steilerDev merged 3 commits into
betafrom
feat/1557-hide-system-linked-documents

Conversation

@steilerDev
Copy link
Copy Markdown
Owner

Summary

  • Extends the document hide-linked filter from per-entity scope to system-wide: documents already linked to any work item are excluded when the toggle is active
  • Backend: new linkedDocumentIds query on GET /api/document-links returns all IDs linked across the system; documentLinkService exposes a getSystemLinkedDocumentIds() helper
  • Frontend: useDocumentLinks hook and LinkedDocumentsSection consume the new system-wide ID set when building the filtered document list

Fixes #1557

Test plan

  • Unit tests pass (documentLinkService, documentLinksApi, useDocumentLinks)
  • Integration tests pass (GET /api/document-links?linkedDocumentIds=true)
  • E2E Scenario 7: hide-linked toggle correctly excludes documents linked elsewhere in the system
  • Quality Gates CI green

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) noreply@anthropic.com

Frank Steiler and others added 3 commits May 22, 2026 15:12
Adds two E2E tests covering the new "Hide already-linked documents"
toggle behaviour introduced in #1557:

- Scenario 7a: mock GET /api/document-links/linked-ids returning [42];
  opening the picker on workItemA shows both docs; checking the toggle
  hides doc #42 (linked system-wide) while doc #55 remains visible;
  unchecking restores both documents.
- Scenario 7b: verifies the toggle is visible when system IDs > 0 and
  unchecked by default, so all documents show without filtering.

Fixes #1557 (E2E coverage)

Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.6) <noreply@anthropic.com>
…linked ID filter

Extends all five existing document-link test files with coverage for the new
getAllLinkedDocumentIds service function, GET /api/document-links/linked-ids route,
listAllLinkedDocumentIds API client function, useAllLinkedDocumentIds hook, and
the system-wide filter merge logic in LinkedDocumentsSection.

Tests cover: empty/single/duplicate/multi-entity deduplication scenarios (service
and route), API client URL and response extraction, hook lifecycle (no-mount-fetch,
isLoading transitions, ApiClientError/NetworkError/unknown error branches, fetch
stability, second-call replacement), and component integration (fetch-on-open,
system+entity ID merge, deduplication, empty passthrough).

Co-Authored-By: Claude qa-integration-tester (Sonnet 4.6) <noreply@anthropic.com>
Add a system-wide filter that hides documents already linked to any
work item when the "hide linked" toggle is active. Previously the
filter only excluded links for the current entity.

Fixes #1557

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude backend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
@steilerDev steilerDev force-pushed the feat/1557-hide-system-linked-documents branch from 5624f20 to f13970e Compare May 22, 2026 13:13
@steilerDev
Copy link
Copy Markdown
Owner Author

[security-engineer] APPROVE — no security findings.

Auth enforcement: The new GET /linked-ids handler opens with if (!request.user) { throw new UnauthorizedError(); } (line 128-130). This is identical to the three existing handlers in the same file (POST / line 74, GET / line 103, DELETE /:id line 149). Pattern is consistent; no regression.

Information disclosure: The endpoint returns all Paperless document IDs across all entities without per-user filtering. This matches the existing GET / endpoint which also returns links for any entity without ownership restriction. The single-tenant model (1-5 homeowners) has no per-user data isolation for documents — all authenticated users already see all links. No regression.

SQL injection: db.selectDistinct({ paperlessDocumentId: documentLinks.paperlessDocumentId }).from(documentLinks).all() is a pure Drizzle ORM call with no user-controlled input and no string interpolation. Fully parameterized.

CSRF: Read-only GET with no state mutation. SameSite=strict session cookie applies. No CSRF surface.

Response content: paperlessDocumentIds is typed as number[] and constructed from integer DB column values. No user-controllable strings in the response; no XSS surface.

Verdict: APPROVE. Checklist clear — no injection vectors, auth enforced, no sensitive data exposed, no new dependencies, no hardcoded credentials.

@steilerDev
Copy link
Copy Markdown
Owner Author

[product-owner]

Verdict: APPROVE (posted as comment — PR author cannot self-approve via review API)

All 5 acceptance criteria are met.

AC Status Evidence
1. Hide docs linked anywhere PASS documentLinkService.getAllLinkedDocumentIds() selects DISTINCT paperless_document_id; LinkedDocumentsSection unions systemLinkedIds.ids with current hook.links doc IDs and passes to DocumentBrowser via existing linkedDocumentIds prop.
2. Fresh fetch on each open PASS useAllLinkedDocumentIds does NOT fetch on mount (asserted by unit test); useEffect([showPicker]) calls systemLinkedIds.fetch() each time the picker opens. Subsequent fetches replace prior ids (asserted by hook test).
3. Toggle disabled shows all PASS E2E Scenario 7b verifies toggle unchecked by default — both documents visible despite system-wide link present.
4. Hidden when both sets empty PASS LinkedDocumentsSection test "passes empty linkedDocumentIds when both systemLinkedIds.ids=[] and hook.links=[]" verifies the union is []; DocumentBrowser only renders the checkbox when linkedDocumentIds.length > 0.
5. i18n key unchanged PASS No changes to client/src/i18n/**/documents.json; E2E locates checkbox via existing documents:browser.hideLinked label.

Out-of-scope items: All correctly avoided (no visual changes, no cross-open caching, no per-entity breakdown).

Test authorship: Correct — qa-integration-tester wrote unit/integration tests, e2e-test-engineer wrote Scenarios 7a/7b, dev-team-lead committed production code.

Non-blocking nit: PR summary says "new linkedDocumentIds query on GET /api/document-links" but the implementation uses the dedicated path GET /api/document-links/linked-ids (matching the issue's technical notes). Description text only.

Ready to merge after CI gates pass.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[product-architect] Verdict: APPROVE

(Cannot --approve own PR via gh — posting as comment per agent convention.)

Architecture, contract, and coverage all check out. Verified against Wiki Architecture / API Contract / Schema and the existing documentLinks module conventions.

Verified:

  1. Drizzle selectDistinct (server/src/services/documentLinkService.ts:224-230) — canonical pattern for column-level distinct. Pushes dedup to SQLite rather than JS-side Set; correct for the scale and the right call.
  2. Route ordering (server/src/routes/documentLinks.ts:126 vs 145)GET /linked-ids is registered before DELETE /:id. There is no actual method+path collision risk here (the :id route is DELETE, the new route is GET), but the static-before-param ordering is the right defensive convention and matches Fastify guidance.
  3. API contract shape{ paperlessDocumentIds: number[] } is consistent with the existing single-field wrapping in DocumentLinkResponse { documentLink } and DocumentLinkListResponse { documentLinks }. Auth gating (401 UNAUTHORIZED) matches the rest of the file. No JSON schema on the response is consistent with the sibling GET / (existing in-file style).
  4. Wiki documentation (wiki/API-Contract.md 6109–6133) — endpoint documented with auth requirement, empty response noted, error table present. Submodule ref 98204a8 is committed in the PR. Good wiki discipline.
  5. Test coverage — 5 service / 5 integration / 8 hook / 5 component / 2 E2E. Critical paths (dedup within entity type, dedup across entity types, lazy fetch on picker open, error categorisation) are all exercised. Coverage is more than adequate for the change.

Informational (non-blocking):

  • The local interface AllLinkedDocumentIdsResponse in documentLinks.test.ts (lines 28-32) is a worktree symlink workaround for @cornerstone/shared. Documented inline; will dissolve once merged.
  • Lazy fetch on picker open (not mount) is the right design — keeps cost off pages where the picker is never opened.

No architectural debt introduced. Cleared for merge.

@steilerDev steilerDev merged commit 4f261f1 into beta May 22, 2026
29 of 32 checks passed
@steilerDev steilerDev deleted the feat/1557-hide-system-linked-documents branch May 22, 2026 13:31
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.7.0-beta.6 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant