Skip to content

feat(app): a new label can be created from the annotation page #233

Description

@JArmandoAnaya

Problem

Mid-annotation, a user who needs a class that does not exist has no path shorter than: leave the job, open the project's Schema tab, publish a new version, create a new batch (because the old one pins the old version), and re-partition. The ask: add a label from the annotation page (jobs/<id>) via a dialog carrying the same form as the Schema tab's class editor, and have it usable immediately in the current batch.

How it decomposes

"Immediately usable" requires the batch's pin to move — this issue is the UI over two existing/new API capabilities:

  1. POST /projects/{id}/schema/versions (exists) — the new version is the active version's classes plus the new class, built on active, never on the batch's pin (versions are linear).
  2. POST /batches/{id}/repin (feat(kernel): a batch can re-pin to the active schema version #229) — additive by construction on this path, so the gate never fires in the common case. If someone else moved the schema destructively past this batch's pin in the meantime, the re-pin refuses and the dialog surfaces the conflict instead of stranding annotations — the gate doing its job.

The sequencing constraint (verified in code)

Workspace (ui-core/src/annotator/AnnotationPage.tsx:352-362) builds the annotator store in a useMemo keyed on schema — a schema refetch rebuilds the store, discarding unsaved edits and undo history. So the dialog's mutation chain must run: save pending edits first (they are valid under the old schema, and the change is additive, so this cannot be refused) → create version → re-pin → invalidate queries. Losing undo history at a save boundary is the page's existing, documented behavior ("saving is a diff, and then a reload"), not a new regression. Teaching the headless core to swap a schema into a live document was considered and deferred — it touches the core document model for marginal gain.

Scope

  • Extract the class form from SchemaEditor.tsx into a shared ui-core component; host it in a dialog (existing primitives) on the annotation page.
  • The dialog auto-fills the version description (feat(kernel): a schema version records a description and when it was created #230) — e.g. "Added class 'X' from the annotation view" — editable before submit.
  • On success: set the new class as activeClass (that state lives outside the store and survives the rebuild), so the user draws with it immediately; its palette hotkey arrives free (digit N = palette row N).
  • Class colour goes through the one sanctioned spelling (classColor in the adapter's paint.ts).
  • Failure surfaces: version race (someone published concurrently — retry is honest), destructive-drift re-pin refusal (name the conflict, point at the Schema tab).

Acceptance criteria

  1. From jobs/<id>, a user adds a class and draws with it without leaving the page; unsaved work made before the dialog survives.
  2. The batch's pinned version after the flow is the new version; the Schema tab shows the new version as active.
  3. A concurrent destructive schema change makes the flow refuse with a message naming the remedy — nothing is half-applied (version may exist, pin unmoved — state that plainly).
  4. Vitest for the mutation chain ordering (save before create-version — a test must fail if the order flips); e2e scenario in the existing Playwright harness if the demo page can host it, else the ui-core screen-test convention.

Depends on #229 (re-pin) and #230 (description field for the auto-fill; degrade gracefully if sequenced earlier).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestfrontendannotator / ui-core / app packages

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions