Skip to content

fix: make annotation placeholder rows respect property cardinality (closes #203)#273

Open
R-Hart80 wants to merge 1 commit into
CatholicOS:devfrom
R-Hart80:fix/annotation-cardinality-aware-placeholder
Open

fix: make annotation placeholder rows respect property cardinality (closes #203)#273
R-Hart80 wants to merge 1 commit into
CatholicOS:devfrom
R-Hart80:fix/annotation-cardinality-aware-placeholder

Conversation

@R-Hart80

@R-Hart80 R-Hart80 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • skos:prefLabel @en already filled → editor now shows a @pt placeholder (not a second @en), respecting SKOS S14.
  • skos:definition @en already filled → same behaviour (single-per-lang).
  • skos:notation / dcterms:created / dcterms:modified etc. filled → no trailing empty at all (single).
  • skos:altLabel, rdfs:comment, all DC Elements → unchanged (always one trailing empty, multiple).

Changes

File What changed
lib/ontology/annotationProperties.ts Add AnnotationCardinality type + cardinality field on every KnownAnnotationProperty; add getAnnotationCardinality(iri) helper
lib/ontology/annotationCardinality.ts (new) Centralised ensureTrailingPlaceholder(values, cardinality) — replaces three identical copies of ensureTrailingEmpty
ClassDetailPanel.tsx Import and use ensureTrailingPlaceholder for annotation value arrays; keep ensureTrailingEmpty as deprecated wrapper for backwards-compat
PropertyDetailPanel.tsx Same; editDefinitions now uses "single-per-lang"
IndividualDetailPanel.tsx Same; editLabels and editDefinitions now use "single-per-lang"
__tests__/lib/ontology/annotationCardinality.test.ts (new) 23 unit tests covering all three cardinality modes and the IRI lookup helper

Test plan

  • Open a class with skos:prefLabel "Foo"@en → detail panel shows the value row + one empty @pt row (not @en).
  • Open a class with skos:altLabel "Bar"@en → still shows value row + empty @en (multi-valued, unchanged).
  • Open a class with dcterms:created "2024-01-01" → no trailing empty row.
  • npx vitest run __tests__/lib/ontology/annotationCardinality.test.ts → 23/23 pass.
  • Full vitest suite: 2760/2760 pass (pre-existing LanguagePicker failure is unrelated to this PR).

Closes #203.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements

    • Annotation fields in the detail editor panels now display intelligent placeholders that adapt to each annotation property's cardinality settings, with specialized handling for single-value annotations, per-language variants, and multi-value properties.
  • Tests

    • Added test coverage for annotation cardinality behavior validation across different property types.

…loses CatholicOS#203)

Annotation properties like skos:prefLabel (SKOS S14) and skos:definition
allow at most one value per language tag, but the editor was unconditionally
appending an empty @en placeholder even when an English value already existed,
inviting a duplicate that violates the cardinality constraint.

- Add `AnnotationCardinality` type (`single` | `single-per-lang` | `multiple`)
  and a `cardinality` field to every entry in `ANNOTATION_PROPERTIES`.
- Add `getAnnotationCardinality(iri)` helper (defaults to `"multiple"` for
  unknown IRIs, the safe non-restrictive fallback).
- Centralise placeholder logic in `lib/ontology/annotationCardinality.ts`:
  `ensureTrailingPlaceholder(values, cardinality)` replaces the three
  identical copies of `ensureTrailingEmpty` that lived in the three panels.
  - `"multiple"` → unchanged trailing-empty behaviour.
  - `"single-per-lang"` → placeholder lang is the first common language
    (en → pt → es → fr → de → it) not yet covered by a filled value;
    no placeholder when all common langs are present.
  - `"single"` → no placeholder once any value is filled.
- Update ClassDetailPanel, PropertyDetailPanel, IndividualDetailPanel to use
  the cardinality-aware helper at every call site.
- Keep `ensureTrailingEmpty` exported from ClassDetailPanel as a deprecated
  wrapper so existing test imports compile without changes.
- Add 23 unit tests covering all three cardinality modes and the cardinality
  lookup helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements cardinality-aware trailing placeholder handling for annotation values. It adds a type system and metadata catalog to distinguish single, single-per-lang, and multiple cardinality annotations, centralizes placeholder logic into a pure helper, and applies it across the three editor detail panels to prevent duplicate rows for constrained annotations like skos:prefLabel.

Changes

Cardinality-aware Annotation Placeholders

Layer / File(s) Summary
Annotation cardinality schema and catalog
lib/ontology/annotationProperties.ts
New AnnotationCardinality type with three modes; KnownAnnotationProperty extended with cardinality metadata; ANNOTATION_PROPERTIES populated with SKOS/RDFS/DCTERMS cardinality assignments; getAnnotationCardinality(iri) function returns cardinality with "multiple" default for unknown IRIs.
Cardinality-aware placeholder helper
lib/ontology/annotationCardinality.ts
ensureTrailingPlaceholder(values, cardinality) implements per-cardinality row normalization: single strips all rows once one is filled, single-per-lang preserves existing placeholders and appends uncovered language codes, multiple appends a trailing empty row when the last entry is non-empty.
Cardinality placeholder tests
__tests__/lib/ontology/annotationCardinality.test.ts
Vitest suite validates ensureTrailingPlaceholder across all three cardinality modes with language-selection logic for single-per-lang, and asserts getAnnotationCardinality lookups for known SKOS/RDFS/DCTERMS IRIs and the "multiple" default for unknowns.
Apply cardinality logic across detail panels
components/editor/ClassDetailPanel.tsx, components/editor/IndividualDetailPanel.tsx, components/editor/PropertyDetailPanel.tsx
All three panels remove the local ensureTrailingEmpty helper, update imports to include getAnnotationCardinality and ensureTrailingPlaceholder, and replace unconditional trailing-empty appending with cardinality-aware calls during edit-state initialization, draft restore, and annotation value updates/removals.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through labels, each with their own way—
Some love many values, while others just stay
With one per language, or one alone.
Now placeholders respect what's written in stone,
No more duplicate rows where they shouldn't have grown! 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: making annotation placeholder rows respect property cardinality, which is the primary objective of this PR.
Linked Issues check ✅ Passed The PR implements all coding objectives from issue #203: adds AnnotationCardinality type with cardinality metadata to KnownAnnotationProperty, provides getAnnotationCardinality(iri) helper, replaces duplicated ensureTrailingEmpty with cardinality-aware ensureTrailingPlaceholder, updates detail panels to use the new helper, and adds 23 unit tests covering all cardinality modes.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing cardinality-aware annotation placeholder handling as defined in issue #203. No unrelated modifications to UI structure, unrelated features, or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/editor/IndividualDetailPanel.tsx (1)

277-289: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Inline annotation add/restore paths still bypass cardinality normalization.

Line 692 and Line 695 hardcode a trailing @en placeholder, which breaks "single" / "single-per-lang" behavior. Line 288 also restores annotations without re-normalizing, so stale drafts can keep invalid placeholder shapes.

Suggested fix
-      setEditAnnotations(d.annotations);
+      setEditAnnotations(
+        d.annotations.map((a) => ({
+          ...a,
+          values: ensureTrailingPlaceholder(
+            a.values.map((v) => ({ ...v })),
+            getAnnotationCardinality(a.property_iri),
+          ),
+        })),
+      );

...
                   onAdd={(propIri, value, lang) => {
                     setEditAnnotations((prev) => {
                       const existing = prev.find((a) => a.property_iri === propIri);
+                      const cardinality = getAnnotationCardinality(propIri);
                       if (existing) {
                         return prev.map((a) => a.property_iri === propIri
-                          ? { ...a, values: [...a.values.filter((v) => v.value.trim()), { value, lang }, { value: "", lang: "en" }] } : a
+                          ? {
+                              ...a,
+                              values: ensureTrailingPlaceholder(
+                                [...a.values.filter((v) => v.value.trim()), { value, lang }],
+                                cardinality,
+                              ),
+                            }
+                          : a
                         );
                       }
-                      return [...prev, { property_iri: propIri, values: [{ value, lang }, { value: "", lang: "en" }] }];
+                      return [
+                        ...prev,
+                        {
+                          property_iri: propIri,
+                          values: ensureTrailingPlaceholder([{ value, lang }], cardinality),
+                        },
+                      ];
                     });
                   }}

Also applies to: 687-696

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/editor/IndividualDetailPanel.tsx` around lines 277 - 289, Restored
drafts are bypassing cardinality normalization causing hardcoded trailing `@en`
placeholders and invalid annotation shapes; when restoring in
IndividualDetailPanel replace the direct restore calls that use
ensureTrailingPlaceholder with the correct cardinality modes (e.g., use "single"
or "single-per-lang" consistently for setEditComments and setEditDefinitions)
and re-run the same normalization used on user edits before calling
setEditAnnotations (and other annotation setters like
setEditComments/setEditDefinitions) so annotations and label/definition
placeholders conform to cardinality rules; locate and update the restore logic
around setEditComments, setEditDefinitions, setEditAnnotations, and related
setters to invoke the normalization helper instead of hardcoding "`@en`".
components/editor/PropertyDetailPanel.tsx (1)

285-297: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Property annotation add/restore flow still reintroduces non-cardinality placeholders.

Line 734 and Line 738 still hardcode ... + { value: "", lang: "en" }, so "single" / "single-per-lang" constraints are bypassed on add. Line 296 restores raw annotation arrays without re-normalization.

Suggested fix
-      setEditAnnotations(d.annotations);
+      setEditAnnotations(
+        d.annotations.map((a) => ({
+          ...a,
+          values: ensureTrailingPlaceholder(
+            a.values.map((v) => ({ ...v })),
+            getAnnotationCardinality(a.property_iri),
+          ),
+        })),
+      );

...
                   onAdd={(propIri, value, lang) => {
                     setEditAnnotations((prev) => {
                       const existing = prev.find((a) => a.property_iri === propIri);
+                      const cardinality = getAnnotationCardinality(propIri);
                       if (existing) {
                         return prev.map((a) =>
                           a.property_iri === propIri
-                            ? { ...a, values: [...a.values.filter((v) => v.value.trim()), { value, lang }, { value: "", lang: "en" }] }
+                            ? {
+                                ...a,
+                                values: ensureTrailingPlaceholder(
+                                  [...a.values.filter((v) => v.value.trim()), { value, lang }],
+                                  cardinality,
+                                ),
+                              }
                             : a
                         );
                       }
-                      return [...prev, { property_iri: propIri, values: [{ value, lang }, { value: "", lang: "en" }] }];
+                      return [
+                        ...prev,
+                        {
+                          property_iri: propIri,
+                          values: ensureTrailingPlaceholder([{ value, lang }], cardinality),
+                        },
+                      ];
                     });
                   }}

Also applies to: 728-739

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/editor/PropertyDetailPanel.tsx` around lines 285 - 297, The
property annotation add/restore flow is reintroducing incorrect hardcoded
placeholders and bypassing cardinality rules; fix it by normalizing annotations
through the existing helper instead of injecting raw placeholders: update the
add-annotation code paths (e.g., any addAnnotation handler) to call
ensureTrailingPlaceholder(...) with the correct mode ("single" or
"single-per-lang") rather than appending { value: "", lang: "en" }, and when
restoring drafts in the PropertyDetailPanel restore block (where
setEditAnnotations(d.annotations) is used) call
setEditAnnotations(ensureTrailingPlaceholder(d.annotations,
"<appropriate-mode>")) so annotations respect single vs multi-per-language
constraints; use the same ensureTrailingPlaceholder utility that other fields
(comments/definitions) use to determine placeholder shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/ontology/annotationCardinality.ts`:
- Around line 34-37: The code in the add-row logic uses COMMON_LANGS and returns
early when nextLang is undefined which prevents adding additional valid locales
beyond that list; change the behavior in the block that computes
filledLangs/nextLang so that when no COMMON_LANGS remain you still append a new
empty row with an open/blank locale instead of returning early — i.e., in the
code around filledLangs, nextLang and the final return replace the early return
with returning [...values, { value: "", lang: nextLang ?? "" }] (or equivalent
so lang is empty/placeholder when nextLang is undefined) so users can add
locales like "ja" even if not in COMMON_LANGS.

---

Outside diff comments:
In `@components/editor/IndividualDetailPanel.tsx`:
- Around line 277-289: Restored drafts are bypassing cardinality normalization
causing hardcoded trailing `@en` placeholders and invalid annotation shapes; when
restoring in IndividualDetailPanel replace the direct restore calls that use
ensureTrailingPlaceholder with the correct cardinality modes (e.g., use "single"
or "single-per-lang" consistently for setEditComments and setEditDefinitions)
and re-run the same normalization used on user edits before calling
setEditAnnotations (and other annotation setters like
setEditComments/setEditDefinitions) so annotations and label/definition
placeholders conform to cardinality rules; locate and update the restore logic
around setEditComments, setEditDefinitions, setEditAnnotations, and related
setters to invoke the normalization helper instead of hardcoding "`@en`".

In `@components/editor/PropertyDetailPanel.tsx`:
- Around line 285-297: The property annotation add/restore flow is reintroducing
incorrect hardcoded placeholders and bypassing cardinality rules; fix it by
normalizing annotations through the existing helper instead of injecting raw
placeholders: update the add-annotation code paths (e.g., any addAnnotation
handler) to call ensureTrailingPlaceholder(...) with the correct mode ("single"
or "single-per-lang") rather than appending { value: "", lang: "en" }, and when
restoring drafts in the PropertyDetailPanel restore block (where
setEditAnnotations(d.annotations) is used) call
setEditAnnotations(ensureTrailingPlaceholder(d.annotations,
"<appropriate-mode>")) so annotations respect single vs multi-per-language
constraints; use the same ensureTrailingPlaceholder utility that other fields
(comments/definitions) use to determine placeholder shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b92fdd71-7cf0-472b-abb6-79354da8523c

📥 Commits

Reviewing files that changed from the base of the PR and between bba8ab4 and 8fd7f8f.

📒 Files selected for processing (6)
  • __tests__/lib/ontology/annotationCardinality.test.ts
  • components/editor/ClassDetailPanel.tsx
  • components/editor/IndividualDetailPanel.tsx
  • components/editor/PropertyDetailPanel.tsx
  • lib/ontology/annotationCardinality.ts
  • lib/ontology/annotationProperties.ts

Comment on lines +34 to +37
const filledLangs = new Set(values.filter((v) => v.value.trim()).map((v) => v.lang));
const nextLang = COMMON_LANGS.find((l) => !filledLangs.has(l));
if (nextLang === undefined) return values;
return [...values, { value: "", lang: nextLang }];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

single-per-lang currently blocks adding valid locales beyond the predefined list.

Line 36 returns early when common languages are exhausted, so users cannot add another valid language row (e.g., ja) even though cardinality allows one value per language.

Suggested fix
-      const filledLangs = new Set(values.filter((v) => v.value.trim()).map((v) => v.lang));
+      const filledLangs = new Set(
+        values
+          .filter((v) => v.value.trim() !== "")
+          .map((v) => v.lang.trim().toLowerCase())
+          .filter(Boolean),
+      );
       const nextLang = COMMON_LANGS.find((l) => !filledLangs.has(l));
-      if (nextLang === undefined) return values;
+      if (nextLang === undefined) {
+        // Force explicit language selection when the common list is exhausted.
+        return [...values, { value: "", lang: "" }];
+      }
       return [...values, { value: "", lang: nextLang }];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ontology/annotationCardinality.ts` around lines 34 - 37, The code in the
add-row logic uses COMMON_LANGS and returns early when nextLang is undefined
which prevents adding additional valid locales beyond that list; change the
behavior in the block that computes filledLangs/nextLang so that when no
COMMON_LANGS remain you still append a new empty row with an open/blank locale
instead of returning early — i.e., in the code around filledLangs, nextLang and
the final return replace the early return with returning [...values, { value:
"", lang: nextLang ?? "" }] (or equivalent so lang is empty/placeholder when
nextLang is undefined) so users can add locales like "ja" even if not in
COMMON_LANGS.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Editor proposes a duplicate row for cardinality-1 annotations (skos:prefLabel, etc.)

1 participant