Skip to content

Commit 4bc3b80

Browse files
authored
Merge pull request open-doubao-ai#1 from TommyLemon/multi_tables
merge multi tables to main
2 parents 691598c + b703a1e commit 4bc3b80

32 files changed

Lines changed: 5258 additions & 637 deletions

.cursor/skills/a2api-demo/SKILL.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ description: Work on the A2API monorepo (protocol, runtime, chat-demo UI/APIJSON
1010
- `packages/protocol` — A2API 0.1 envelopes / validators
1111
- `packages/runtime``ApiJsonClient`, HITL, `BoundExecutor`
1212
- `apps/chat-demo` — Hono API + Vite client (Bootstrap chat + steady-state UI)
13-
- `apps/admin` — Apply submit/status + `available-requests` (Access∩Request+Document) + approve → Access/Request/Document/Chain; SPA list/edit via APIJSON; Login/Settings (same chrome as chat-demo); SQL `sql/sys_Apply.sql`, `sql/sys_Call.sql`
13+
- `apps/admin` — Apply submit/status + `available-requests` (Access∩Request+Document) + approve → Access/Request/Document/Chain then `/post/verify` (TYPE_RELOAD=4) + `/reload`; SPA list/edit via APIJSON; Login/Settings (same chrome as chat-demo); SQL `sql/sys_Apply.sql`, `sql/sys_Call.sql`
1414
- chat-demo calls admin for permission-gate Apply submit/poll and available request catalog
1515
- `conversations/` — git-managed chat examples
1616
- `.cursor/skills/` — project skills (this file)
@@ -29,7 +29,8 @@ npm install && npm run dev
2929

3030
- Steady-state filter/sort/page must not call the LLM (`usedLlm: false`); client rebuilds the body and calls `/apijson/{method}` (Node BFF) — never absolute `:8080` URLs from bind templates.
3131
- Sensitive writes (default `delete`) go to Admin approval; other writes auto-execute and store `auto_approved` audit rows (`apps/chat-demo/data/approvals.jsonl`).
32-
- Edit/delete: do **not** auto-jump Data API. Check Document+Access (`/api/write-gate`): call if allowed, Apply if Document exists without Access; if no Document, try then Apply on permission error. Refresh polls Apply and notifies only on status change.
32+
- Edit/delete: do **not** auto-jump Data API. Always try the API first; on permission / parameter / illegal errors auto-submit Admin Apply. Demo never shows Approve/Reject (Admin only). Refresh polls Apply and notifies only on status change.
33+
- Apply / Request.tag: from page title → lowercase English, spaces→`_`, strip other specials (e.g. `Moment Detail``moment_detail`); also set on write body `tag` for retry.
3334
- Chart field pool = all query tables × fields (not table visible-column config).
3435
- User list / primary User: omit `@column` (all fields). JOIN User defaults include `name,tag,head,pictureList` (not only `name`).
3536
- UI copy is English; Chinese NLP matching may remain for intent only.
@@ -41,7 +42,14 @@ npm install && npm run dev
4142
- DDL **Show** (`ColumnMeta.show`: Auto / Text / Picture / File) — auto-filled on prompt via `inferColumnShow`; editable in table DDL; drives smart UI
4243
- Evidence (Show=Auto): (1) url + `.jpg`/… or `data:image`; (2) name segments / comment (`头像`…) + url-like
4344
- `sex` / `gender` → Male(0) / Female(1); **Raw** / **Smart** toggle (detail header + table tabs top-right)
44-
- FK: `ColumnMeta.onTable` (外表) + optional `onField` (外键 key, default `id`). Detail/create/list use `resolveFkRef` (meta overrides auto). `Comment.toId` → self `Comment.id`; do not invent short stems like `toId``To`
45+
- FK: `ColumnMeta.onTable` (Relate Table) + optional `onField` (Relate Field, default `id`). Detail/create/list use `resolveFkRef` (meta overrides auto). `Comment.toId` → self `Comment.id`; do not invent short stems like `toId``To`
46+
- FK id-list columns (`contactIdList` / `praiseUserIdList`…): list cells — each id → related **Detail**; `· all` / `+N` → related **List** with `id` IN filter (`onOpenFkList`). Detail smart-mode chips → related **Detail** (`openFkDetail`).
47+
- Detail multi-table: op Add/View/Edit/Remove + table (+ Relate on secondary) → `POST /crud` with `@get`/`@post`/`@put`/`@delete`. Secondary Relate UI: **vice field** + **=|IN|Contains** + Relate table + Relate field → Request.structure `UPDATE` e.g. `"momentId@":"/Moment/id"`, `"id{}@":"/User/contactIdList"`, `"contactIdList<>@":"/Comment/userId"`. Relate refs go in the call body; if the API fails (permission/param/illegal), demo auto-submits **Apply** with structure → Admin approve → Request. Relate also syncs Table DDL (`onTable`/`onField`) via `onRelateSync`. After picking a table, load schema and show all columns (not sparse JOIN `@column`)
48+
- Detail/create: auto-mount verification code under `phone`/`email`; Save attaches body keys in order `"@delete":"Verify"`, `"Verify":{verify}`, then User/… (promote single post/put → crud). Apply structure: Verify first, with `@delete` + `UPDATE.phone@/email@` → owning table
49+
- Detail/create field show/hide: click table name → same list table DDL popover (`ColumnMeta.visible`); left checkbox = show on detail; field row × hides; hidden fields omitted from Save. ▾ next to table name changes table. Schema keys honor logical↔physical aliases (`Privacy` / `apijson_privacy`) so DDL lists all columns, not just `id`
50+
- List / Detail / Create are **independent pages** — titles & surfaceIds must include the kind (`Moment List` / `Moment Detail` / `Create Moment`; `moment_list` / `moment_detail` / `moment_create`). Never collapse both to a bare table name.
51+
- Detail/create header title is an **editable input** (synced with top page selector). Editing the title on detail/create **forks a new saved page** (e.g. Create User → `register`); the previous page is left unchanged. Multi-table slots persist so the new page can be reopened from the top menu.
52+
- Detail header keeps `#` + **id search input** to the right of the title — Enter/blur reloads that record; page title itself does not embed `#id`
4553

4654
## Before finishing
4755

apps/admin/client/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ <h2>Applications</h2>
5555
<option>post</option>
5656
<option>put</option>
5757
<option>delete</option>
58+
<option>crud</option>
5859
</select>
5960
</label>
6061
<label class="toolbar-label">Sort
@@ -78,6 +79,15 @@ <h2>Applications</h2>
7879
</div>
7980
</section>
8081

82+
<div
83+
class="layout-split-handle"
84+
id="apply-split-handle"
85+
role="separator"
86+
aria-orientation="vertical"
87+
aria-label="Resize list and detail"
88+
tabindex="0"
89+
></div>
90+
8191
<section class="panel detail-panel">
8292
<div class="panel-head">
8393
<h2>Review &amp; edit</h2>
@@ -100,6 +110,7 @@ <h2>Review &amp; edit</h2>
100110
<option value="post">post</option>
101111
<option value="put">put</option>
102112
<option value="delete">delete</option>
113+
<option value="crud">crud</option>
103114
</select>
104115
</label>
105116
<label>Role
@@ -168,6 +179,7 @@ <h2>Calls</h2>
168179
<option>post</option>
169180
<option>put</option>
170181
<option>delete</option>
182+
<option>crud</option>
171183
</select>
172184
</label>
173185
<label>OK
@@ -211,6 +223,14 @@ <h2>Calls</h2>
211223
<button type="button" id="call-next" class="ghost pager-btn" disabled>Next</button>
212224
</div>
213225
</section>
226+
<div
227+
class="layout-split-handle"
228+
id="calls-split-handle"
229+
role="separator"
230+
aria-orientation="vertical"
231+
aria-label="Resize list and detail"
232+
tabindex="0"
233+
></div>
214234
<section class="panel detail-panel">
215235
<div class="panel-head"><h2>Call detail</h2></div>
216236
<div id="call-detail-empty" class="muted pad">Select a call log</div>

apps/admin/client/main.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
type CallLog,
3333
type CallStats,
3434
} from "./call-api.js";
35+
import { mountVerticalSplit } from "./split-resize.js";
3536

3637
type WriteTargetResult = {
3738
ok: boolean;
@@ -145,6 +146,31 @@ const accountUi = mountAccountUi({
145146
},
146147
});
147148

149+
{
150+
const applyLayout = $("view-apply");
151+
const callsLayout = $("view-calls");
152+
const splitOpts = {
153+
cssVar: "--admin-list-pct",
154+
storageKey: "a2api.adminListSplitPct",
155+
defaultPct: 32,
156+
minPct: 18,
157+
maxPct: 60,
158+
bodyClass: "is-resizing-admin",
159+
} as const;
160+
mountVerticalSplit({
161+
split: applyLayout,
162+
handle: $("apply-split-handle"),
163+
...splitOpts,
164+
syncSplits: [callsLayout],
165+
});
166+
mountVerticalSplit({
167+
split: callsLayout,
168+
handle: $("calls-split-handle"),
169+
...splitOpts,
170+
syncSplits: [applyLayout],
171+
});
172+
}
173+
148174
setSessionExpiredHandler(() => {
149175
logoutAccount();
150176
accountUi.refresh();
@@ -352,9 +378,14 @@ function renderList() {
352378
btn.dataset.testid = "apply-item";
353379
btn.dataset.applyId = row.id;
354380
btn.dataset.requestId = row.requestId || "";
381+
const tag = row.tag?.trim();
382+
const tagHtml =
383+
tag && tag !== row.table
384+
? ` <span class="app-tag">${escapeHtml(tag)}</span>`
385+
: "";
355386
btn.innerHTML = `
356387
<div class="title">
357-
${escapeHtml(row.operation.toUpperCase())} ${escapeHtml(row.table)}
388+
${escapeHtml(row.operation.toUpperCase())} ${escapeHtml(row.table)}${tagHtml}
358389
<span class="badge badge-${row.status}">${row.status}</span>
359390
</div>
360391
<div class="meta">${escapeHtml(row.role)} · v${row.version} · ${fmtTime(row.createdAt)}</div>

apps/admin/client/split-resize.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/** Vertical split handle: drag / keyboard to set left pane %. */
2+
3+
export function mountVerticalSplit(opts: {
4+
split: HTMLElement;
5+
handle: HTMLElement;
6+
/** CSS custom property on `split`, e.g. --admin-list-pct */
7+
cssVar: string;
8+
storageKey: string;
9+
defaultPct?: number;
10+
minPct?: number;
11+
maxPct?: number;
12+
/** class on body while dragging */
13+
bodyClass?: string;
14+
/** keep sibling layouts in sync (same CSS var) */
15+
syncSplits?: HTMLElement[];
16+
}): void {
17+
const min = opts.minPct ?? 20;
18+
const max = opts.maxPct ?? 80;
19+
const bodyClass = opts.bodyClass ?? "is-resizing-split";
20+
21+
const load = (): number => {
22+
try {
23+
const n = Number(localStorage.getItem(opts.storageKey));
24+
if (Number.isFinite(n) && n >= min && n <= max) return n;
25+
} catch {
26+
/* ignore */
27+
}
28+
return opts.defaultPct ?? 40;
29+
};
30+
31+
const save = (pct: number) => {
32+
try {
33+
localStorage.setItem(opts.storageKey, String(Math.round(pct)));
34+
} catch {
35+
/* ignore */
36+
}
37+
};
38+
39+
const apply = (pct: number) => {
40+
const clamped = Math.min(max, Math.max(min, pct));
41+
const targets = [opts.split, ...(opts.syncSplits ?? [])];
42+
for (const el of targets) {
43+
el.style.setProperty(opts.cssVar, `${clamped}%`);
44+
}
45+
save(clamped);
46+
};
47+
48+
apply(load());
49+
50+
let dragging = false;
51+
const onMove = (clientX: number) => {
52+
const rect = opts.split.getBoundingClientRect();
53+
if (rect.width < 80) return;
54+
apply(((clientX - rect.left) / rect.width) * 100);
55+
};
56+
57+
opts.handle.addEventListener("pointerdown", (e) => {
58+
e.preventDefault();
59+
dragging = true;
60+
opts.handle.setPointerCapture(e.pointerId);
61+
document.body.classList.add(bodyClass);
62+
});
63+
opts.handle.addEventListener("pointermove", (e) => {
64+
if (!dragging) return;
65+
onMove(e.clientX);
66+
});
67+
const endDrag = (e: PointerEvent) => {
68+
if (!dragging) return;
69+
dragging = false;
70+
try {
71+
opts.handle.releasePointerCapture(e.pointerId);
72+
} catch {
73+
/* ignore */
74+
}
75+
document.body.classList.remove(bodyClass);
76+
};
77+
opts.handle.addEventListener("pointerup", endDrag);
78+
opts.handle.addEventListener("pointercancel", endDrag);
79+
80+
opts.handle.addEventListener("keydown", (e) => {
81+
const cur = load();
82+
if (e.key === "ArrowLeft") {
83+
e.preventDefault();
84+
apply(cur - 2);
85+
} else if (e.key === "ArrowRight") {
86+
e.preventDefault();
87+
apply(cur + 2);
88+
}
89+
});
90+
}

apps/admin/client/styles.css

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,18 +464,44 @@ body {
464464
flex: 1;
465465
min-height: 0;
466466
display: grid;
467-
grid-template-columns: minmax(280px, 360px) 1fr;
467+
--admin-list-pct: 32%;
468+
grid-template-columns: minmax(220px, var(--admin-list-pct)) 6px minmax(280px, 1fr);
468469
gap: 0;
469470
}
470471

472+
.layout-split-handle {
473+
grid-column: 2;
474+
cursor: col-resize;
475+
background: var(--line);
476+
position: relative;
477+
z-index: 2;
478+
touch-action: none;
479+
}
480+
481+
.layout-split-handle::after {
482+
content: "";
483+
position: absolute;
484+
inset: 0 -3px;
485+
}
486+
487+
.layout-split-handle:hover,
488+
.layout-split-handle:focus-visible,
489+
body.is-resizing-admin .layout-split-handle {
490+
background: var(--accent);
491+
}
492+
493+
body.is-resizing-admin {
494+
cursor: col-resize;
495+
user-select: none;
496+
}
497+
471498
.panel {
472499
min-height: 0;
473500
overflow: auto;
474501
background: color-mix(in srgb, var(--panel) 92%, transparent);
475502
}
476503

477504
.list-panel {
478-
border-right: 1px solid var(--line);
479505
display: flex;
480506
flex-direction: column;
481507
min-height: 0;
@@ -639,6 +665,13 @@ body {
639665
margin-bottom: 4px;
640666
}
641667

668+
.app-item .app-tag {
669+
font-weight: 500;
670+
color: var(--muted);
671+
font-family: "IBM Plex Mono", ui-monospace, monospace;
672+
font-size: 0.88em;
673+
}
674+
642675
.app-item .meta {
643676
font-size: 0.78rem;
644677
color: var(--muted);
@@ -790,8 +823,11 @@ button:disabled {
790823
grid-template-columns: 1fr;
791824
}
792825

826+
.layout-split-handle {
827+
display: none;
828+
}
829+
793830
.list-panel {
794-
border-right: none;
795831
border-bottom: 1px solid var(--line);
796832
max-height: 40vh;
797833
}

apps/admin/server/app.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -369,24 +369,29 @@ export function createAdminApp(deps: AdminAppDeps): Hono {
369369
password: body.password,
370370
});
371371
const status = applied.ok ? "approved" : "pending";
372+
const writeErrors = [
373+
applied.results.Access?.ok
374+
? null
375+
: `Access: ${applied.results.Access?.error}`,
376+
applied.results.Request?.ok
377+
? null
378+
: `Request: ${applied.results.Request?.error}`,
379+
applied.results.Document?.ok
380+
? null
381+
: `Document: ${applied.results.Document?.error}`,
382+
applied.results.Chain?.ok
383+
? null
384+
: `Chain: ${applied.results.Chain?.error}`,
385+
applied.results.Reload == null || applied.results.Reload.ok
386+
? null
387+
: `Reload: ${applied.results.Reload.error}`,
388+
].filter(Boolean);
389+
// Approve succeeds when Request+Document write; Reload failure is surfaced but non-blocking.
372390
const error = applied.ok
373-
? undefined
374-
: [
375-
applied.results.Access?.ok
376-
? null
377-
: `Access: ${applied.results.Access?.error}`,
378-
applied.results.Request?.ok
379-
? null
380-
: `Request: ${applied.results.Request?.error}`,
381-
applied.results.Document?.ok
382-
? null
383-
: `Document: ${applied.results.Document?.error}`,
384-
applied.results.Chain?.ok
385-
? null
386-
: `Chain: ${applied.results.Chain?.error}`,
387-
]
388-
.filter(Boolean)
389-
.join("; ");
391+
? writeErrors.length
392+
? writeErrors.join("; ")
393+
: undefined
394+
: writeErrors.join("; ") || undefined;
390395

391396
const updated = await store.update(id, {
392397
...applied.application,

apps/admin/server/apply-flow.e2e.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,15 @@ function testApp(store: FileApplicationStore) {
6464
Request: { ok: true, action: "post" },
6565
Document: { ok: true, action: "post" },
6666
Chain: { ok: true, action: "post" },
67+
Reload: { ok: true, action: "post" },
6768
},
6869
},
6970
results: {
7071
Access: { ok: true, action: "put" },
7172
Request: { ok: true, action: "post" },
7273
Document: { ok: true, action: "post" },
7374
Chain: { ok: true, action: "post" },
75+
Reload: { ok: true, action: "post" },
7476
},
7577
}),
7678
});

0 commit comments

Comments
 (0)