Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/specifications/collection-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Board and Calendar cards used to title themselves off "the first `text`-type fie
- **Storage.** `Collection.primaryFieldKey?: string` (`data-model.md` §1) — a schema key, stored the same whole-value way as `schema` itself (`readCollectionMeta`/`ymeta.set('primaryFieldKey', ...)` in `src/lib/data/records.ts`). Absent on every Collection created before this feature, and on any Collection where no field has been explicitly chosen since — no migration script runs; resolution (next bullet) is what makes an unset Collection keep behaving exactly as it did before.
- **Resolution: `resolvePrimaryField(schema, primaryFieldKey)`.** Returns the schema field named by `primaryFieldKey` when it exists and is an eligible type; otherwise falls back to the first `text` field in schema order — the exact pre-#96 implicit rule — so a Collection created before this feature, or one whose chosen primary field was since deleted or retyped away, keeps showing the same title without a migration step. The fallback only ever considers `text` fields, not every eligible type, so this returns `undefined` both when the schema has no eligible field at all and when it has eligible fields (e.g. a lone `number` or `select` field) but none of type `text` (Board/Calendar's card/entry title then reads "Untitled", same as before). Board and Calendar both call this in place of their old `schema.find(p => p.type === 'text')` line; `FieldMenu`'s primary-field indicator (next bullet) calls it too, so the star always matches what's actually titling a card.
- **Eligible types: everything except `relation`.** A `relation` value is a list of record IDs with no display string of its own (data-model.md's `PropertyValue`) — every other type (`text`, `number`, `date`, `select`, `checkbox`) already renders as one displayable value via `PropertyValueCell`, so `setPrimaryField` rejects choosing a `relation` field (`ValidationError`), and a retype that turns the current primary field _into_ `relation` clears `primaryFieldKey` in the same transaction rather than leaving it pointing at a now-invalid field. Deleting the current primary field clears it the same way. Both repairs mirror the existing `deleteCollectionProperty`/`updateCollectionProperty` pattern of fixing up stale schema references in one transaction rather than leaving a dangling key for a reader to notice later.
- **Display value: `primaryFieldDisplayValue(value, property)`** (`src/lib/data/views.ts`) — a plain-text rendering of any eligible type's value (a `select` value resolves through `property.options` to its label; `checkbox` renders "Checked"/empty), used for Board/Calendar's card-title `aria-label`s and Calendar's static entry-title text. Board's own card _editor_ still renders the primary field as a full `PropertyValueCell` (so whichever type is chosen — not just `text` — stays directly editable inline); this function is only for the places a plain string is needed instead of an editable cell.
- **Display value: `primaryFieldDisplayValue(value, property)`** (`src/lib/data/views.ts`) — a plain-text rendering of any eligible type's value (a `select` value resolves through `property.options` to its label; `checkbox` renders "Checked"/"Unchecked"), used for Board's card-title `aria-label`s and other plain-text title fallbacks. Board's card and Calendar's entry normally render their primary field as a full `PropertyValueCell` (so whichever type is chosen — not just `text` — stays directly editable inline); this function is only for places a plain string is needed instead of an editable cell.
- **Colliding with `groupBy` or `swimlaneBy` (issue #104, #105).** `primaryFieldKey` and Board/Calendar's `groupBy` (§3), or Board's `swimlaneBy`, are independent choices — nothing stops a user from pointing any of them at the same `select`/`date` field. Both Board's card title and Calendar's entry title normally render the primary field as a directly-editable `PropertyValueCell` (§4), which — when `titleProperty.key` matches `groupProperty.key` (Board or Calendar) or Board's `swimlaneProperty.key` — would otherwise sit right next to another editable control for the identical value: Board's matching "Move to column"/"Move to swimlane" `<select>` (§3), or Calendar's own date `PropertyValueCell` rendered lower in the same entry (§4). Both views resolve this identically, by falling back to the same plain non-editable label used when there's no primary field at all (`titleEditableViaCell` in `BoardCollectionView.svelte`, checked against both properties; the same-named derived value in `CalendarCollectionView.svelte`, checked against `groupBy` only, since Calendar has no swimlane dimension): the other control stays the one editable path to that value, and the title cell for every other field keeps behaving exactly as described above. Nothing prevents choosing the same field for more than one of these roles — there's no correctness reason to forbid it — this only changes which control is editable when a choice collides.
- **Control and indicator: `FieldMenu`.** A "Set as primary field" / "Unset primary field" menu item (disabled, with an explanatory `title`, for a `relation` field) calls `setPrimaryField`/`setPrimaryField(..., null)` directly — the same direct-Yjs-mutation pattern every other `FieldMenu` action already uses. The toggle's own `isPrimary` state compares the Collection's raw `primaryFieldKey` prop against `property.key` directly — deliberately **not** through `resolvePrimaryField` — because a resolved comparison would make the auto-fallback field's own toggle a permanent no-op: with `primaryFieldKey` unset, the fallback field already resolves as primary, so a resolved `isPrimary` would read `true` and the menu would only ever offer "Unset," writing `null` onto an already-`null` key with no way to actually promote that field to an explicit choice. The small star indicator next to a field's label (rendered by each of `FieldMenu`'s call sites — `TableCollectionView` and `FieldManagerDialog`; `/table/[id]` inherits it by composing `TableCollectionView` (§4) rather than rendering its own `FieldMenu` any more; Board/Calendar show no column headers, so neither renders `FieldMenu` at all) is unaffected by this distinction: those call `resolvePrimaryField` themselves for display, so the star still reflects the _resolved_ primary field, including the auto-fallback case, even while the field's own toggle correctly reads as "not yet explicitly set."
- **MCP-visible schema.** `collections.listCollections` and `collections.queryCollection` (`mcp-tools.md`) both include a `primaryFieldKey` alongside `schema` in their response — the _resolved_ key (`resolvePrimaryField`'s result), not the raw possibly-unset stored value, so an agent always sees which field is actually titling a record right now rather than having to reimplement the fallback rule itself.
Expand Down
6 changes: 4 additions & 2 deletions src/lib/data/views.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,12 +510,14 @@ describe('primaryFieldDisplayValue', () => {
);
});

it('renders a checkbox value as Checked or empty', () => {
it('renders checkbox values as Checked or Unchecked', () => {
const checkboxProperty: PropertyDefinition = { key: 'done', label: 'Done', type: 'checkbox' };
expect(primaryFieldDisplayValue({ type: 'checkbox', value: true }, checkboxProperty)).toBe(
'Checked'
);
expect(primaryFieldDisplayValue({ type: 'checkbox', value: false }, checkboxProperty)).toBe('');
expect(primaryFieldDisplayValue({ type: 'checkbox', value: false }, checkboxProperty)).toBe(
'Unchecked'
);
});

it('returns an empty string for a select value with no matching option', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/data/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export function primaryFieldDisplayValue(
case 'date':
return value.value;
case 'checkbox':
return value.value ? 'Checked' : '';
return value.value ? 'Checked' : 'Unchecked';
case 'select':
return property.options?.find((o) => o.id === value.value)?.label ?? '';
case 'relation':
Expand Down
Loading