fix(admin): 主人账号编辑器草稿改存无损形态 ——「+ 添加账号 ID」不再点了没点

根因:`emit` 把每次改动都写成 `flattenAccounts(groups)`(写盘形态,空行被丢掉),而
草稿本身就是界面手势的往返载体 ⇒ 刚点出来的占位空行当场蒸发,界面照旧(主人报的
「配置指定适配器主人账号的卡片内的添加账号ID按钮似乎没用」)。`+ 添加平台` 走同一条
通路(空平台组同样被丢),实测同样失效。

改法:分清两种扁平形态 ——
  · 草稿 `draftAccounts`(编辑期,无损:空行/未选平台的组都留着;不 trim 正在输入的字)
  · 写盘形态 `accountsForSave`(保存期,只留平台与账号 ID 都非空的条目)
后端契约与 `configs/users/owner.yaml` 格式一个字不改。保存路径写盘前必须收形:半成品
条目(有 id 没平台)会被后端 `Validate()` 直接 400(internal/user/load.go)。

测试:`OwnerAccountsView` 从编辑器里拆出来(无 hook),测试可直接调用它、点它自己的
按钮、把 onChange 收到的真草稿再喂回去重渲染 —— 走的是「手势 → 草稿 → 界面」真往返
(本项目没有 jsdom)。修前该测试红在「点『+ 添加账号 ID』⇒ 界面上真的多出一行 —
got 1, want 2」。
This commit is contained in:
AskaEth committed 2026-10-13 20:00:43 +08:00
1 parent b01e2ddb1a
commit 625ae33c1e
5 files changed
+369 -19

No files matched your search

@@ -12,7 +12,13 @@ import type { AdminChannelRecord, EnumItem, OwnerAccount } from "../../lib/types
* (`owner.IsOwner` 匹配不上 actor 元数据 ⇒ 主人被当陌生人,界面上没有任何提示)。
* 平台改为权威枚举下拉 + 账号 id 可从该平台已配置账号里选,消灭这类静默失效。
*
* 本模块是纯函数(无 DOM、无请求),因此分组/展平/下拉选项/候选都能在 node 下逐条测。
* 本模块是纯函数(无 DOM、无请求),因此分组/草稿/写盘形态/下拉选项/候选都能在 node 下逐条测。
*
* **两种扁平形态,别混用**(这是主人 2026-10-07 报的「添加账号 ID 按钮似乎没用」的根因所在):
* ① **草稿**(`draftAccounts`,编辑期):逐行原样保留,空行/未选平台的组都在 ——
* 界面手势产生的中间态**绝不能在往返中蒸发**;
* ② **写盘形态**(`accountsForSave`,保存期):只留平台与账号 id 都非空的条目。
* 把①错当②用(编辑期就走写盘形态),空行当场消失 ⇒ 点了等于没点,界面上还看不出原因。
*/
/** 一个平台分组:界面上的一条。 */
@@ -22,7 +28,7 @@ export interface OwnerAccountGroup {
}
/** 把扁平数组按平台分组(保持首次出现顺序,同一平台的多条账号收进一组)。 */
export function groupAccounts(accounts: OwnerAccount[]): OwnerAccountGroup[] {
export function groupAccounts(accounts: readonly OwnerAccount[]): OwnerAccountGroup[] {
const groups: OwnerAccountGroup[] = [];
const byPlatform = new Map<string, OwnerAccountGroup>();
for (const a of accounts) {
@@ -38,10 +44,27 @@ export function groupAccounts(accounts: OwnerAccount[]): OwnerAccountGroup[] {
return groups;
}
/** 展平成后端契约要的扁平数组。**平台与账号 id 都非空才写**:任一为空都匹配不上任何
* actor 元数据(写了也是噪声,还会让主人误以为已配置)。空的账号行/空平台组被忽略,
* 界面上有对应提示,不是静默丢弃。 */
export function flattenAccounts(groups: OwnerAccountGroup[]): OwnerAccount[] {
/** 组 → **草稿**(编辑期形态):逐行原样保留 —— 空行、以及「还没选平台」的组都在。
*
* 为什么必须保行:草稿是**界面手势的载体** —— 每次改动都走「草稿 → 分组 → 改 → 回草稿」,
* 写回若有损(把空行丢掉),刚点出来的「+ 添加账号 ID」空行当场蒸发 ⇒ 点了等于没点
* (主人 2026-10-07 报的正是这个)。空行只在**保存时**才丢(见 `accountsForSave`),
* 界面上另有「有 N 条空白账号 ID:保存时会被忽略」的显式提示,不是静默丢弃。
*
* 另外:编辑期**不改写**主人正在输入的字(不 trim)。逐键 trim 会把还在输入的空白吃掉;
* 规范化(去空白 + 丢空行)留到写盘那一步做。 */
export function draftAccounts(groups: readonly OwnerAccountGroup[]): OwnerAccount[] {
const out: OwnerAccount[] = [];
for (const g of groups) {
for (const accountId of g.accountIds) out.push({ platform: g.platform, accountId });
}
return out;
}
/** 展平成后端契约要的扁平数组(**写盘形态**)。**平台与账号 id 都非空才写**:任一为空都
* 匹配不上任何 actor 元数据(写了也是噪声,还会让主人误以为已配置)。空的账号行/空平台组
* 被忽略,界面上有对应提示,不是静默丢弃。 */
export function flattenAccounts(groups: readonly OwnerAccountGroup[]): OwnerAccount[] {
const out: OwnerAccount[] = [];
for (const g of groups) {
const platform = g.platform.trim();
@@ -54,6 +77,16 @@ export function flattenAccounts(groups: OwnerAccountGroup[]): OwnerAccount[] {
return out;
}
/** 草稿 → 写盘形态(`PUT /api/v1/admin/owner-profile` 的 `accounts`,即
* `configs/users/owner.yaml` 的条目)。**保存路径只走它**,别自己拼这两步。
*
* 编辑期草稿里允许有空行,但后端 `Validate()` 对「平台空、账号 id 非空」这种半成品条目
* 直接 400(`internal/user/load.go`),完全空的条目则被跳过 —— 所以这一步是必须的,不是洁癖。
* 去掉两侧空白同理:带空格的键永远匹配不上 actor 元数据。 */
export function accountsForSave(accounts: readonly OwnerAccount[]): OwnerAccount[] {
return flattenAccounts(groupAccounts(accounts));
}
/** 下拉选项:后端权威枚举(`GET /api/v1/meta/enums` 的 `platforms`)在前,随后补上
* 存量里出现过的平台 —— 未列出的旧值**原样保留**(display-raw:绝不下拉吃掉它,
* 否则一次保存就把主人已经生效的配置删了)。 */