fix(collections): 未設定の embed(idField が空)が「レコードが見つかりません」の赤カードになる — 仕様は fail-soft
> MulmoTerminal(`receptron/mulmoterminal`、ローカル開発起動)でコレクションのレコード詳細を開いていて気づきました。該当コードは `@mulmoclaude/collection-plugin` にあるため、こちらのリポジトリに出しています。
## What happened
任意(`required` でない)の `ref` を `idField` に取る `embed` フィールドが、**その値が空のとき**にレコード詳細でエラー表示になります。
```
⚠ 埋め込み参照が見つかりません
projects に「」のレコードが見つかりません。 [設定する →]
```
id を差し込む位置が空文字なので「「」のレコードが見つかりません」という文になります。参照先が消えた(リンク切れ)のではなく、**まだ設定していないだけ**の状態です。embed が2つあるレコードでは、この赤いカードが2枚並びます。
保存自体は阻害されません(書き込み経路に参照先の存在チェックは無い)ので、**表示だけの問題**です。
## What I expected
空の参照は、リンク切れとは区別して穏当に表示されること(他の空フィールドと同じ em-dash など)。理由は、契約がそう書いているからです — `packages/core/src/collection/core/schemaZ.ts:265`、`EmbedFieldZ` の doc comment:
> `idField` (…); **an absent/empty value resolves fail-soft to "no record"**
`packages/core/src/collection/core/schema.ts:290-294`(`embedTargetId`)も "empty string when neither applies — the caller renders that as 'no record'" で、空を正常系として呼び出し側へ渡しています。赤枠 + `error_outline` + 「見つかりません」は fail-soft の表示ではないと思います。
同型の「フェイルソフト契約が破れる」問題として #2322 があります(あちらは proto キー、こちらは描画分岐)。
## Steps to reproduce
1. 任意の `ref` を `idField` に取る `embed` を持つコレクションを作る
```json
{
"fields": {
"id": { "type": "string", "label": "ID", "primary": true, "required": true },
"clientId": { "type": "ref", "to": "clients", "label": "顧客" },
"client": { "type": "embed", "to": "clients", "idField": "clientId", "label": "顧客(参照)" }
}
}
```
2. `clientId` が空のレコードを1件置く(キーごと無い場合も同じ)— 例: `{ "id": "a1" }`
3. `/collections/<slug>` でそのレコードを開く
4. 「顧客(参照)」の位置に赤い `embedMissing` カードが出る。メッセージ中の id は空
## Environment
- mulmoterminal **4.8.1**(`receptron/mulmoterminal` @ `93ad0628`、ソースからのローカル開発起動 = `concurrently -n server,vite`)
- `@mulmoclaude/collection-plugin` **3.1.0**(npm 最新)/ `@mulmoclaude/core` **3.3.0**
- `receptron/mulmoclaude` main `946245f` のソースでも同じコードであることを確認
- Node v24.18.1 / Darwin arm64 (macOS) / zsh
- ブラウザ: Chrome 151.0.7922.108(`localhost:5173`)
- tmux 3.7b / gh 2.92.0 / claude 2.1.227 / codex-cli 0.146.0
- ロケール: ja
## Attachments
- ブラウザのコンソールは未確認(描画分岐の問題なので例外は出ていないと思われます)
- スクリーンショット無し。表示文言は「What happened」に転記したとおり
---
以下は調べた範囲の補足です。判断はメンテナ側でお願いします。
### 原因
**2つの異なる状態が1つの boolean に潰れている**のが実体だと思います。
1. `packages/core/src/collection/core/schema.ts:295` `embedTargetId()` — `idField` が空なら `""` を返す
2. `packages/plugins/collection-plugin/src/vue/useCollectionRendering.renderers.ts:93` — `found: Boolean(item)` に落とす。**「参照が空」も「参照先が無い」も同じ `false`**
3. `packages/plugins/collection-plugin/src/vue/components/CollectionEmbedView.vue` — 分岐は `v-if="view.found"`(L5)と `v-else`(L44、赤カード)の2つだけ
4. `packages/plugins/collection-plugin/src/vue/components/CollectionRecordPanel.vue:450` — `v-else-if="field.type === 'embed' && embedViews[key]"`。`buildEmbedViews` は embed ごとに必ずキーを作るので常に真
### 編集側と閲覧側で扱いが割れている
同じパネルの中で、編集側は「空」を正規の値として提供しています。
- `CollectionRecordPanel.vue:126` — embed のピッカーは先頭に `<option value="">選択…</option>` を出す
- `CollectionRecordPanel.vue:716` `embedPickerRequired()` — `required` を storage フィールド(`idField`)から読む。`required` でなければ空のまま保存できる
また `CollectionRecordPanel.vue:379` の `ref` 表示は `&& detailRecord[key]` で空を除外して素の表示に落ちます。embed だけがこの扱いを持っていません。
### テストの空白
- `test/plugins/collection/test_collectionRenderers.ts:86` — `resolveEmbed` の**空**ケースは既にある(`{}` → `item: null`)
- 同 `:139` — `buildEmbedViews` は **`customerId: "ghost"`(リンク切れ)** のケースだけで、空 `idField` のケースが無い
- Vue コンポーネントの単体テストはリポジトリに無い(`mount(` の該当なし)
- `e2e/tests/collection-*.spec.ts` に `collections-embed` を参照する spec が無い
### 直し方(案)
**最小案 — 1ファイル、core 変更なし、i18n 変更なし。** `recordId` は既にビューモデルにあるので、`CollectionEmbedView.vue` の L44 の前に枝を1本足すだけで足ります。固定 `id` の embed では `recordId` が空にならないので、per-record な `idField` の embed だけが拾われます。
```vue
<!-- 未設定: 任意の embed で idField が空。参照先が消えたのとは別物 -->
<span v-else-if="!view.recordId" class="text-slate-300">—</span>
<div v-else class="... border-red-100 ..."> <!-- ghost のときだけ赤 -->
```
ラベルは親が描いているので「顧客(参照) —」となり、パネル内の他の空フィールドと揃います。同ファイルに bare `—` の前例(L35)があります。
派生案が2つあり、波及範囲が変わります。
- 文言(「未設定」など)を出す → `lang/en.ts` にキー追加。`CollectionMessages` 型経由で **8ロケール全部**(de / en / es / fr / ja / ko / ptBR / zh)の追従が必要
- 状態を型で持たせる → `packages/core/src/collection/core/uiTypes.ts:94` の `EmbedView.found: boolean` を3状態に。設計としては綺麗ですが `found` は公開済み API なので **core + plugin の2本リリース**になります
テストは `buildEmbedViews` に空 `idField` のケースを1つと、`data-testid`(`collections-embed-*` / `collections-embed-missing-*`)が既にあるので e2e を1本、が素直だと思います。
### 影響範囲
`idField` を持つ embed のうち、その storage フィールドが `required` でないもの全部。埋まっていない行が普通にある関係(タスク → 工程、タスク → 目標 など)を embed で見せている限り、正常なレコードが常にエラー表示になります。
关闭于 5 天前 2 条评论