Skip to content

CLUE-570: teacher-created custom comment tags#2908

Merged
lbondaryk merged 9 commits into
masterfrom
CLUE-570-user-created-comment-tags
Jul 15, 2026
Merged

CLUE-570: teacher-created custom comment tags#2908
lbondaryk merged 9 commits into
masterfrom
CLUE-570-user-created-comment-tags

Conversation

@lbondaryk

Copy link
Copy Markdown
Contributor

Summary

Lets teachers add custom comment tags for their class (scoped to class + unit) that sync in real time and are immediately usable for manual comments, AI evaluation, and Sort Work grouping. Gated by a new unit authoring switch so legacy units are unaffected.

Out of scope (future): editing/combining/deleting tags without orphaning docs; Class-Summary tag suggestions.

Changes

Authoring

  • New unit config allowCustomCommentTags (default false), threaded end-to-end like defaultSharedDocuments.
  • New authoring Comments page (comments-settings.tsx) with the showCommentTag toggle (previously had no UI) and the allowCustomCommentTags switch; the comment roles / rating / tag-list editor moved here out of AI Settings. Registered in left-nav + workspace routing. AI Settings still mirrors AI categories from the tag list. No unit-JSON migration — only the authoring surface moved.

Sync (class + unit)

  • CommentTags store + DBCommentTagsListener: Firestore onSnapshot at commentTags/{classHash}/units/{unit}/tags, reactively (re)subscribed as class/unit load.
  • New firestore.rules entry grants in-class students read (teachers/researchers already covered by the parent /authed/{portal} rule).
  • Effective tag list = unit-config tags + synced custom tags, used by the comment picker/display and Sort Work.
  • getTagsWithDocs hardened so any tag on a document forms a group even when its label isn't loaded — no document is orphaned from the tag sort at Unit/All scope.

Add UI (Sort Work)

  • Teacher-only "+ Add Tag" control below the "Not Tagged" section in the Strategy (tag) sort. Adding immediately creates a new empty sort section. <datalist> typeahead over existing tags; Add is disabled for a duplicate name (case-insensitive).

AI

  • Custom tag ids are merged into aiPrompt.categories at the client's analysis-metadata write, so custom-evaluation units categorize with them. No functions-v2 change.

Test plan

  • Unit tests added/updated: allowCustomCommentTags config getter, CommentTags store (merge/replace/id/path), getTagsWithDocs orphan guard, SortWorkAddTag (add + teacher gating + duplicate disable). tsc, ESLint, and affected Jest suites are green.
  • Manual: unit with showCommentTag + allowCustomCommentTags on → teacher adds a tag in Sort Work → new empty section appears, tag is selectable in comments; tag a doc → it lands in that group. Demo mode (shared root) demonstrates teacher→student propagation locally.

Deploy notes

  • Requires deploying the new Firestore rule (npm run deploy:firestore:rules) before students can read custom tags in authed mode.
  • Cypress coverage for the Sort Work add-tag flow is not yet written (unit tests cover it).

🤖 Generated with Claude Code

Let teachers add custom comment tags for their class (class+unit scoped) that
sync in real time and are usable for manual comments, AI evaluation, and Sort
Work grouping. Gated by a new unit authoring switch so legacy units are
unaffected. Tag editing/combining/deleting and Class-Summary suggestions are
out of scope (future).

Authoring
- New unit config `allowCustomCommentTags` (default false), end-to-end like
  `defaultSharedDocuments`.
- New authoring "Comments" page (comments-settings.tsx) with the showCommentTag
  toggle (previously had no UI) and the allowCustomCommentTags switch; the
  comment roles/rating/tag-list editor moved here out of AI Settings. Registered
  in left-nav + workspace routing. AI Settings still mirrors categories from the
  tag list. No unit-JSON migration (only the authoring surface moved).

Sync (class + unit scope)
- CommentTags store + DBCommentTagsListener: Firestore onSnapshot at
  commentTags/{classHash}/units/{unit}/tags, reactively (re)subscribed as
  class/unit load. Firestore rule grants in-class students read.
- Effective tag list = unit-config tags + synced custom tags, used by the comment
  picker/display and Sort Work. getTagsWithDocs hardened so any tag on a document
  forms a group even if its label isn't loaded (orphan guard for cross-unit sort).

Add UI (Sort Work)
- Teacher-only "+ Add Tag" control below the "Not Tagged" section in the Strategy
  (tag) sort; adding immediately creates a new empty sort section. Datalist
  typeahead over existing tags; Add is disabled for a duplicate name.

AI
- Custom tag ids are merged into aiPrompt.categories at the client's analysis
  metadata write, so `custom`-evaluation units categorize with them (no
  functions-v2 change).

Tests: config getter, CommentTags store, getTagsWithDocs orphan guard, and
SortWorkAddTag (add + teacher gating + duplicate disable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds support for teacher-created custom comment tags (scoped to class + unit) that sync in real time and are usable for comment tagging, Sort Work grouping, and AI categorization, with authoring UI moved to a new Comments settings page.

Changes:

  • Introduces allowCustomCommentTags unit config and a new authoring “Comments” page; AI Settings now mirrors categories from tags.
  • Implements a CommentTags store + Firestore snapshot listener for per-class/unit custom tags, and updates tag consumers to use merged (config + custom) tags.
  • Adds a teacher-only “+ Add Tag” control in Sort Work and hardens tag grouping so documents with unknown tag ids still form groups.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/utilities/sort-document-utils.ts Prevents “orphaned” tagged docs by creating groups for unknown strategy/tag ids.
src/utilities/sort-document-utils.test.ts Adds unit tests for getTagsWithDocs grouping + orphan guard.
src/models/stores/unit-configuration.ts Adds allowCustomCommentTags to unit configuration schema.
src/models/stores/stores.ts Registers new commentTags store in root stores.
src/models/stores/sorted-documents.ts Uses merged tag list (config + custom) for sorting/grouping.
src/models/stores/document-group.ts Uses merged tag list for Strategy grouping.
src/models/stores/document-group.test.ts Updates store stubs to include commentTags.
src/models/stores/configuration-manager.ts Adds computed getter for allowCustomCommentTags gated by showCommentTag.
src/models/stores/configuration-manager.test.ts Adds tests for new config getter behavior.
src/models/stores/comment-tags.ts New MobX store for custom tags + helper functions + Firestore write method.
src/models/stores/comment-tags.test.ts Unit tests for id/path helpers and merge/replace behavior.
src/models/stores/base-stores-types.ts Extends base store types to include commentTags.
src/models/stores/app-config-model.ts Exposes allowCustomCommentTags via app config model.
src/lib/firebase.ts Includes custom tag ids in AI categories when writing “custom” evaluation prompts.
src/lib/db-listeners/index.ts Starts/stops the new Firestore comment-tags listener.
src/lib/db-listeners/db-comment-tags-listener.ts New Firestore onSnapshot listener that syncs tag docs into the store.
src/components/document/sorted-section.test.tsx Passes commentTags store into sorted-section test setup.
src/components/document/sort-work-view.tsx Renders the new “Add Tag” control when sorting by Strategy.
src/components/document/sort-work-add-tag.tsx New UI to add tags (teacher-only), with datalist suggestions + duplicate blocking.
src/components/document/sort-work-add-tag.test.tsx Tests for teacher gating, add flow, and duplicate disable.
src/components/document/sort-work-add-tag.scss Styles for the new “Add Tag” control.
src/components/chat/comment-card.tsx Makes CommentCard reactive to merged tags so the picker includes custom tags.
src/authoring/types.ts Adds allowCustomCommentTags to authoring unit config typing.
src/authoring/components/workspace/comments-settings.tsx New authoring page for comment/tag settings; mirrors tags into AI categories.
src/authoring/components/workspace/ai-settings.tsx Removes tag/role/rating authoring UI; keeps AI fields and mirrors categories from tags.
src/authoring/components/workspace.tsx Adds routing to the new Comments settings page.
src/authoring/components/left-nav.tsx Adds “Comments” entry in authoring left nav.
firestore.rules Adds student read access to per-class/unit custom comment tags in authed mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +58 to +63
config.showCommentTag = data.showCommentTag;
if (data.allowCustomCommentTags) {
config.allowCustomCommentTags = true;
} else {
delete config.allowCustomCommentTags;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the submit handler now only persists allowCustomCommentTags when showCommentTag is true, and deletes it otherwise.

Comment on lines +1 to +24
import { observer } from "mobx-react";
import React, { useState } from "react";
import { useStores } from "../../hooks/use-stores";

import "./sort-work-add-tag.scss";

// Teacher-only "+ Add Tag" control shown below the "Not Tagged" section when documents are sorted
// by tag (Strategy) in the Sort Work view. Adding a tag writes it to Firestore; it syncs back into
// the effective tag list, which immediately creates a new (empty) sort section for it. A datalist
// gives typeahead over existing tag labels so teachers reuse tags rather than creating variants.
export const SortWorkAddTag: React.FC = observer(function SortWorkAddTag() {
const { appConfig, commentTags, user } = useStores();
const [adding, setAdding] = useState(false);
const [text, setText] = useState("");

const canAdd = !!appConfig.showCommentTag && !!appConfig.allowCustomCommentTags && user.isTeacher;
if (!canAdd) return null;

const tags = commentTags.mergedWith(appConfig.commentTags);
const trimmed = text.trim();
// Disallow creating a tag whose name already exists (case-insensitive), for tag hygiene.
const isDuplicate = !!trimmed &&
Object.values(tags).some(label => label.trim().toLowerCase() === trimmed.toLowerCase());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the Add button is now also disabled when commentTagId(entered) matches an existing tag key (id collision), not just a matching display name. Added a test for the config-key-collision case.

Comment on lines +49 to +55
<input
type="text"
list="sort-work-tag-suggestions"
className="add-tag-input"
data-testid="sort-work-add-tag-input"
placeholder="New tag"
value={text}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added aria-label="New tag name" to the input.

- comments-settings: don't persist allowCustomCommentTags when showCommentTag
  is off (the disabled checkbox could otherwise retain a true value).
- sort-work-add-tag: also block a new tag whose generated id collides with an
  existing tag key (would override that tag's label in the merged list), not
  just a matching display name.
- sort-work-add-tag: add an aria-label to the new-tag input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.37748% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.98%. Comparing base (1b061f5) to head (457f670).

Files with missing lines Patch % Lines
src/models/stores/comment-tags.ts 87.50% 4 Missing ⚠️
src/components/document/sort-work-add-tag.tsx 94.33% 3 Missing ⚠️
src/lib/db-listeners/db-comment-tags-listener.ts 94.28% 2 Missing ⚠️
src/models/stores/sorted-documents.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           master    #2908    +/-   ##
========================================
  Coverage   85.98%   85.98%            
========================================
  Files         918      921     +3     
  Lines       52713    52854   +141     
  Branches    13940    13978    +38     
========================================
+ Hits        45324    45447   +123     
- Misses       7373     7391    +18     
  Partials       16       16            
Flag Coverage Δ
cypress ?
cypress-regression 73.11% <52.27%> (-0.07%) ⬇️
cypress-smoke 42.01% <40.90%> (-0.01%) ⬇️
jest 54.97% <80.13%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cypress

cypress Bot commented Jul 10, 2026

Copy link
Copy Markdown

collaborative-learning    Run #19363

Run Properties:  status check passed Passed #19363  •  git commit 457f670346: CLUE-570: trim redundant/obvious comments
Project collaborative-learning
Branch Review CLUE-570-user-created-comment-tags
Run status status check passed Passed #19363
Run duration 10m 34s
Commit git commit 457f670346: CLUE-570: trim redundant/obvious comments
Committer Leslie Bondaryk
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 1
Tests that did not run due to a developer annotating a test with .skip  Pending 5
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 220
View all changes introduced in this branch ↗︎

Leslie Bondaryk and others added 2 commits July 10, 2026 13:20
- DBCommentTagsListener.start() is now idempotent (disposes any existing
  reaction/subscription first) so a double start() can't leak them.
- Document that custom tags feed AI categories only for "custom" evaluation
  (categorize-design/mock use fixed cloud-function categories).
- Document the class-hash escaping coupling between customCommentTagsPath and
  the firestore.rules read rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
comment-card (now an observer reading stores.commentTags) and firebase's
updateEvaluation depend on the new commentTags store; several suites use
hand-rolled partial mock stores that lacked it.

- Add a commentTags stub to the mock stores in comment-card / chat-panel /
  chat-thread / firebase tests.
- updateEvaluation now only rewrites aiPrompt when there are custom categories
  to add (unchanged otherwise) — more conservative and avoids mangling a
  non-object aiPrompt. Added a test for the merge path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@dougmartin dougmartin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice work — the architecture here is clean: the CommentTags store / listener / merge split is easy to follow, the tag-id collision guard and the getTagsWithDocs orphan fix are thoughtful, typecheck is clean, and all the affected Jest suites pass for me.

Requesting changes on one blocker, though: the feature doesn't work in authed mode. The new Firestore rule grants only read, and parent-document rules don't cascade into subcollections, so the teacher's tag write is denied. I confirmed this against the rules emulator rather than inferring it. It passes manual testing only because demo mode is covered by the permissive /demo/{demoName}/{restOfPath=**} rule — and the failure is invisible in the UI, since the write isn't awaited or caught. Both fixes are small.

One other note: you also need to fix a merge conflict, probably from another PR that landed while you were developing this one.

Details below.

Findings

  • firestore.rules:482-487 — BLOCKER: no write rule, so addTag() fails with PERMISSION_DENIED in authed mode.
    The new path grants only allow read. Firestore rules do not cascade into subcollections without a recursive wildcard, so the allow read, write on the parent match /authed/{portal} (line 123) does not apply to /authed/{portal}/commentTags/{classHash}/units/{unit}/tags/{tagId}. Confirmed against the rules emulator: a teacher write to that path fails with false for 'create' @ L12 (the catch-all deny). Reads behave correctly. Add an explicit write grant scoped to the teacher's class/unit, and validate the payload the way the existing comment/support rules do (id == tagId, classHash == classHash, unit == unit, createdBy is the requesting user, label is a non-empty string) so an in-class teacher can't write arbitrary or mismatched fields:

    match /commentTags/{classHash}/units/{unit}/tags/{tagId} {
      allow read: if isAuthed() && request.auth.token.class_hash == classHash;
      allow write: if isAuthedTeacher() &&
        (request.auth.token.class_hash == classHash || teacherIsInClass(classHash)) &&
        isValidCommentTagRequest(classHash, unit, tagId);
    }
    

    Also fix the comment above the rule, which asserts the (incorrect) parent-rule inheritance. Consider whether teachers/researchers viewing a different class of theirs need read access too — the current condition is class_hash == classHash only, whereas the neighboring aicontent rule (lines 475-479) also allows teacherIsInClass(classId).

  • src/components/document/sort-work-add-tag.tsx:29-35 and src/models/stores/comment-tags.ts:51-66 — failed tag writes fail silently. commit() calls the async addTag() without awaiting or catching it, and addTag() has no error handling, so a rejected Firestore write surfaces only as an unhandled promise rejection while the UI clears the input and closes the row: the teacher sees nothing happen and gets no error. Make commit async, await the write, keep the entered text on failure, and show an error state.

  • firebase-test/src/ — no rules test for the new path. Every other subcollection under /authed/{portal} has one (documents-rules.test.ts, supports-rules.test.ts, …). A comment-tags-rules.test.ts covering teacher-write / in-class-student-read / in-class-student-write-deny / out-of-class-student-read-deny would have caught the missing write rule.

  • src/lib/db-listeners/db-comment-tags-listener.ts:26-33 — the reaction cannot observe a unit change. const { user, unit } = this.db.stores; captures the UnitModel instance; Stores.setUnit() (src/models/stores/stores.ts:324-326) replaces that instance wholesale, so a reaction reading unit.code off the captured object never re-fires when the unit changes. Stores is makeAutoObservable, so read through the store instead — () => ({ classHash: this.db.stores.user.classHash, unit: this.db.stores.unit.code }) — which makes the reassignment observable. Latent today (listeners start after unitLoadedPromise, and switching units does a full page reload), but the code comment claims the reaction "handles the unit loading asynchronously after listeners start", which is not true as written; without the fix, a unit change in place would leave the store subscribed to the previous unit's tags.

  • src/utilities/sort-document-utils.ts:94-105 — nit: the orphan guard uses the raw tag id as the section label (tagValue: strategy), so an unresolved tag renders as a slug (e.g. my-custom-tag) in the Sort Work heading. Fine as a fallback, but worth confirming with design that a slug heading is acceptable versus something like "Unknown tag".

Leslie Bondaryk and others added 2 commits July 13, 2026 16:48
…ed-comment-tags

# Conflicts:
#	src/models/stores/document-group.ts
…ener reactivity

- firestore.rules: grant an explicit write on the commentTags subcollection (rules
  don't cascade from /authed/{portal}, so the teacher write was denied in authed
  mode). Scoped to teachers of the class or their networked classes, with a new
  isValidCommentTagRequest validating id/classHash/unit match the path, createdBy is
  the requesting teacher, and label is non-empty. Read: in-class users + networked
  teachers. Fixed the misleading comment. (matches the neighboring aicontent rule)
- Add firebase-test/comment-tags-rules.test.ts (teacher write, payload validation,
  in-class student read, student/other-class/unauthed denies).
- sort-work-add-tag: commit() now awaits addTag() and shows an error (keeping the
  entered text) instead of silently clearing on a failed write.
- db-comment-tags-listener: read unit/classHash through the observable stores so a
  Stores.setUnit() swap re-fires the reaction (was capturing the UnitModel instance).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lbondaryk

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all addressed in 3018b16 (plus a master merge to clear the conflict).

  • Blocker — firestore.rules missing write. Added an explicit allow write on commentTags/{classHash}/units/{unit}/tags/{tagId} (confirmed the parent /authed/{portal} rule doesn't cascade). Scoped per your recommendation to match the neighboring aicontent rule: teachers of the class or their networked classes (class_hash == classHash || teacherIsInClass(classHash)) can write, gated by a new isValidCommentTagRequest(classHash, unit, tagId) that checks id/classHash/unit match the path, createdBy == platform_user_id, and label is a non-empty string. Read now also allows networked teachers in addition to in-class users. Fixed the misleading comment. (Write stays teacher-only; researchers aren't granted, matching aicontent — flag if you want researcher read.)
  • Silent failure. commit() is now async and awaits addTag(); on rejection it keeps the entered text, holds the row open, and shows a "Couldn't save tag — try again" error instead of clearing.
  • Missing rules test. Added firebase-test/src/comment-tags-rules.test.ts: teacher-write, payload-validation rejects (bad createdBy / empty label / path mismatch), out-of-class-teacher write-deny, in-class-student read, in-class-student write-deny, out-of-class-student read-deny, and unauth/generic denies. I couldn't run it locally (no emulator here) — I'll verify on staging as usual.
  • Listener reactivity. The reaction now reads this.db.stores.user.classHash / this.db.stores.unit.code through the observable Stores (was capturing the UnitModel instance, which setUnit() replaces), and the comment is corrected.
  • Nit (slug heading). Leaving the raw-id fallback as-is — confirmed acceptable on our side.

tsc + ESLint clean; comment-tags / document-group / sort-work Jest suites pass.

- Confirm/clarify that researchers can read custom tags: the read rule's
  role-agnostic in-class clause (class_hash == classHash) already covers
  students, teachers, and researchers, so a researcher can see and use custom
  tags when commenting. Made the comment explicit and added rules tests
  (in-class researcher read allowed; researcher write denied).
- Log a CREATE_CUSTOM_COMMENT_TAG event (with the tag name) when a teacher
  successfully creates a tag.
- Test that a failed tag write keeps the entered text and shows an error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lbondaryk

Copy link
Copy Markdown
Contributor Author

Follow-up (0f62c89):

  • Researcher read — correction to my note above. Researchers can already read custom tags: the read rule's first clause isAuthed() && request.auth.token.class_hash == classHash is role-agnostic, so any in-class member (student, teacher, or researcher, all of whom carry class_hash == classHash) satisfies it, and the commenting tag picker (comment-card.tsx) isn't role-gated. So researchers see and use the tags when commenting with no rule change needed — I only made the rule comment explicit and added tests (in-class researcher read allowed; researcher write denied). Write remains teacher-only.
  • Log event added. A teacher successfully creating a tag now logs CREATE_CUSTOM_COMMENT_TAG with the tag name ({ tagName, tagId, unit, classHash }).

I can't run the rules suite locally (no emulator), so I'll verify the write rule on staging as usual.

@dougmartin dougmartin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed — approving. All four items from my last pass are genuinely fixed, and I verified the blocker against the emulator rather than taking the description's word for it.

Verified

  • Firestore write rule. Ran the new firebase-test/src/comment-tags-rules.test.ts under the rules emulator: all 12 cases pass, including the teacher write that previously failed with PERMISSION_DENIED. I also ran the full rules suite to make sure the new isValidCommentTagRequest and the edited match block didn't regress a neighboring rule — clean. Worth noting for the record, since the tests alone don't prove it: the rule requires createdBy == platform_user_id, and the client writes createdBy: user.id, which traces back to uidAsString in src/lib/auth.ts:279 — the same identity the existing comment/support rules match on. So the real client write satisfies the rule, not just the test's hand-built payload.
  • Researcher read. Your correction is right: the read rule's first clause is role-agnostic, so an in-class researcher already satisfies it, and there's now a test asserting it.
  • Listener reactivity. Correct fix — Stores calls makeAutoObservable(this) (stores.ts:124) and setUnit reassigns this.unit (line 324), so reading through this.db.stores.unit.code does make the unit swap observable.
  • Silent failure. commit() now awaits, keeps the entered text, and surfaces an error, with a test covering the rejected-write path.
  • Merge conflict cleared; tsc and ESLint clean; the affected Jest suites pass (64 tests).

Non-blocking nits — fine to fold into a follow-up:

  • src/components/document/sort-work-add-tag.tsx:95-102 — the "Tag already exists" / "Couldn't save tag" messages are plain <span>s, so a screen reader user gets no announcement when a save fails or a duplicate blocks Add; they just find the button silently disabled. Add role="alert" to both spans (the repo already does this in group-management-modal.tsx and picked-up-tile-ghost.tsx), and consider aria-invalid / aria-describedby on the input at lines 65-79.
  • src/models/stores/comment-tags.ts:53-57 and sort-work-add-tag.tsx:31-45addTag() returns early successfully when classHash or unit is missing, so commit() clears the input and closes the row as though the write happened. Throw (or return a boolean the caller checks) so it hits the same error state as a rejected write.
  • src/components/document/sort-work-add-tag.tsx:39-41 — the catch swallows the rejection, so a PERMISSION_DENIED in the field leaves nothing in the console to diagnose. Log the caught error alongside setting the error state.
  • firebase-test/src/comment-tags-rules.test.ts:28-52 — the networked-teacher branch (teacherIsInClass in firestore.rules:502 and 504) is untested; the write cases cover only an in-class teacher and an out-of-class teacher with no network. Add a case with a teacher whose network includes the class, mirroring the aicontent rules tests.

Don't forget the npm run deploy:firestore:rules step in your deploy notes — students and teachers both need the new rule live.

…logging, networked-teacher test

- sort-work-add-tag: the duplicate/error messages get role="alert" and the input
  gets aria-invalid + aria-describedby, so screen readers announce why Add is
  blocked/failed instead of leaving the button silently disabled.
- addTag throws when classHash/unit is missing instead of returning as though the
  write happened, so commit() hits the same error state as a rejected write.
- commit() logs the caught error so a PERMISSION_DENIED is diagnosable.
- comment-tags-rules test: add networked-teacher (teacherIsInClass) write + read
  cases, seeding the class-teachers doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lbondaryk

Copy link
Copy Markdown
Contributor Author

Addressed the latest nits in a462464:

  • A11y announcements — the "Tag already exists" / "Couldn't save tag" spans now have role="alert", and the input carries aria-invalid + aria-describedby, so a screen-reader user hears why Add is blocked/failed instead of finding the button silently disabled.
  • Non-silent addTag — it now throws when classHash/unit is missing instead of returning as though the write happened, so commit() lands in the same error state as a rejected write (input kept, error shown).
  • Error loggingcommit()'s catch now console.errors the caught rejection so a PERMISSION_DENIED is diagnosable.
  • Networked-teacher rules test — added write + read cases for a teacher whose teacherIsInClass grants access (seeding the class-teachers doc), covering the teacherIsInClass branch in the write/read rules.

Added unit coverage too: role="alert" + aria-invalid on the error path, and addTag rejecting when class/unit is unavailable. tsc + ESLint clean; sort-work-add-tag + comment-tags suites green.

Remove comments that restate the code (the repeated "Effective tag list = ..."
before mergedWith calls, "Remove spaces as user types", "Log only after the
write succeeds", test comments that restate their it() description) and tighten
a few verbose blocks (the add-tag header, firestore.rules tag comments) while
keeping the non-obvious "why" notes (rules-don't-cascade, MobX reassignment,
escapeKey, orphan-tag rationale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lbondaryk lbondaryk merged commit cef6ed5 into master Jul 15, 2026
30 of 31 checks passed
@lbondaryk lbondaryk deleted the CLUE-570-user-created-comment-tags branch July 15, 2026 15:11
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.

3 participants