Skip to content

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

Open
R-Hart80 wants to merge 2 commits 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 2 commits 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 use cardinality-aware trailing placeholders, including correct behavior for single-value, per-language, and multi-value annotations—keeping language coverage and preventing mismatched empty entries during editing and updates.
  • Tests
    • Added automated test coverage for annotation-cardinality placeholder normalization and for predicate-to-cardinality mapping, ensuring consistent behavior across known annotation property types and the default for unknown properties.

…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 accurately and concisely summarizes the main change: making annotation placeholder rows respect property cardinality.
Linked Issues check ✅ Passed All objectives from issue #203 are implemented: AnnotationCardinality type added with cardinality metadata in KnownAnnotationProperty, getAnnotationCardinality helper provided, ensureTrailingPlaceholder centralizes placeholder logic, all detail panels updated with cardinality-aware calls, and comprehensive tests added.
Out of Scope Changes check ✅ Passed All changes directly implement issue #203 requirements. No out-of-scope modifications detected; test file, ontology helpers, and three detail panels are all within scope.

✏️ 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 thread lib/ontology/annotationCardinality.ts Outdated
When all common languages (en, pt, es, fr, de, it) are already filled,
the add-row logic now appends a blank-lang placeholder instead of
returning early — letting users type locales like "ja" or "ko" that
are not in the predefined list.

Also normalises filledLangs comparison by trimming and lower-casing
each lang tag, preventing missed matches from casing differences.

Addresses CodeRabbit review comment on PR CatholicOS#273.

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

@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.

🧹 Nitpick comments (1)
lib/ontology/annotationCardinality.ts (1)

13-13: ⚡ Quick win

Update JSDoc to reflect blank-lang placeholder behavior.

The comment states "no placeholder when all common langs are filled," but the implementation now adds a blank-lang placeholder ({ value: "", lang: "" }) in that case (lines 41-44). This could confuse developers reading the code.

📝 Suggested documentation fix
- * - "single-per-lang": placeholder lang is the first common language not yet
- *   covered by a filled value; no placeholder when all common langs are filled.
+ * - "single-per-lang": placeholder lang is the first common language not yet
+ *   covered by a filled value; blank-lang placeholder when all common langs are filled.
🤖 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` at line 13, Update the JSDoc comment
in the annotationCardinality.ts file that currently states "no placeholder when
all common langs are filled" to accurately reflect the actual implementation
behavior. The implementation adds a blank-lang placeholder object with empty
value and lang properties in that case (see lines 41-44), so the documentation
should be corrected to indicate that a blank-lang placeholder IS added when all
common languages are filled, not that no placeholder is added.
🤖 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.

Nitpick comments:
In `@lib/ontology/annotationCardinality.ts`:
- Line 13: Update the JSDoc comment in the annotationCardinality.ts file that
currently states "no placeholder when all common langs are filled" to accurately
reflect the actual implementation behavior. The implementation adds a blank-lang
placeholder object with empty value and lang properties in that case (see lines
41-44), so the documentation should be corrected to indicate that a blank-lang
placeholder IS added when all common languages are filled, not that no
placeholder is added.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24023c38-a259-496c-8c80-37b6a5e6572b

📥 Commits

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

📒 Files selected for processing (2)
  • __tests__/lib/ontology/annotationCardinality.test.ts
  • lib/ontology/annotationCardinality.ts

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