@yuneta/gobj-ui 5.16.0 → 5.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -10
- package/dist/gobj-ui.cjs.js +349 -56
- package/dist/gobj-ui.es.js +349 -56
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +38 -24
- package/src/c_yui_treedb_schema.js +348 -43
- package/src/treedb_node_label.js +73 -0
- package/src/treedb_node_label.test.js +92 -0
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -78,6 +78,8 @@ import {
|
|
|
78
78
|
getStrokeColor,
|
|
79
79
|
} from "./lib_graph.js";
|
|
80
80
|
|
|
81
|
+
import {node_label} from "./treedb_node_label.js";
|
|
82
|
+
|
|
81
83
|
import {
|
|
82
84
|
BaseLayout,
|
|
83
85
|
ExtensionCategory,
|
|
@@ -1227,7 +1229,8 @@ function create_topic_node(gobj, desc, record)
|
|
|
1227
1229
|
style.dx = -58;
|
|
1228
1230
|
style.dy = -20;
|
|
1229
1231
|
style.innerHTML = build_chip_innerHTML(
|
|
1230
|
-
desc.color, priv.theme, record.icon,
|
|
1232
|
+
desc.color, priv.theme, record.icon,
|
|
1233
|
+
node_label(desc, record), record.id
|
|
1231
1234
|
);
|
|
1232
1235
|
|
|
1233
1236
|
} else if(node_treedb_type === 'extended') {
|
|
@@ -1239,8 +1242,8 @@ function create_topic_node(gobj, desc, record)
|
|
|
1239
1242
|
style.dx = -72;
|
|
1240
1243
|
style.dy = -33;
|
|
1241
1244
|
style.innerHTML = build_node_innerHTML(
|
|
1242
|
-
desc.color, priv.theme, record.icon, record
|
|
1243
|
-
desc.topic_name, true
|
|
1245
|
+
desc.color, priv.theme, record.icon, node_label(desc, record),
|
|
1246
|
+
desc.topic_name, true, record.id
|
|
1244
1247
|
);
|
|
1245
1248
|
|
|
1246
1249
|
} else {
|
|
@@ -1250,7 +1253,8 @@ function create_topic_node(gobj, desc, record)
|
|
|
1250
1253
|
style.dx = -86;
|
|
1251
1254
|
style.dy = -48;
|
|
1252
1255
|
style.innerHTML = build_node_innerHTML(
|
|
1253
|
-
desc.color, priv.theme, record.icon, record
|
|
1256
|
+
desc.color, priv.theme, record.icon, node_label(desc, record),
|
|
1257
|
+
desc.topic_name, false, record.id
|
|
1254
1258
|
);
|
|
1255
1259
|
}
|
|
1256
1260
|
|
|
@@ -1353,7 +1357,8 @@ function update_topic_node(gobj, desc, node_name, record)
|
|
|
1353
1357
|
id: node_name,
|
|
1354
1358
|
style: {
|
|
1355
1359
|
innerHTML: build_chip_innerHTML(
|
|
1356
|
-
desc.color, priv.theme, record.icon,
|
|
1360
|
+
desc.color, priv.theme, record.icon,
|
|
1361
|
+
node_label(desc, record), record.id
|
|
1357
1362
|
),
|
|
1358
1363
|
}
|
|
1359
1364
|
}]);
|
|
@@ -1362,8 +1367,9 @@ function update_topic_node(gobj, desc, node_name, record)
|
|
|
1362
1367
|
id: node_name,
|
|
1363
1368
|
style: {
|
|
1364
1369
|
innerHTML: build_node_innerHTML(
|
|
1365
|
-
desc.color, priv.theme, record.icon,
|
|
1366
|
-
desc
|
|
1370
|
+
desc.color, priv.theme, record.icon,
|
|
1371
|
+
node_label(desc, record),
|
|
1372
|
+
desc.topic_name, true, record.id
|
|
1367
1373
|
),
|
|
1368
1374
|
}
|
|
1369
1375
|
}]);
|
|
@@ -1372,8 +1378,9 @@ function update_topic_node(gobj, desc, node_name, record)
|
|
|
1372
1378
|
id: node_name,
|
|
1373
1379
|
style: {
|
|
1374
1380
|
innerHTML: build_node_innerHTML(
|
|
1375
|
-
desc.color, priv.theme, record.icon,
|
|
1376
|
-
desc
|
|
1381
|
+
desc.color, priv.theme, record.icon,
|
|
1382
|
+
node_label(desc, record),
|
|
1383
|
+
desc.topic_name, false, record.id
|
|
1377
1384
|
),
|
|
1378
1385
|
}
|
|
1379
1386
|
}]);
|
|
@@ -4184,8 +4191,9 @@ function show_node_popover(gobj)
|
|
|
4184
4191
|
if(node_graph_type === 'hierarchical') {
|
|
4185
4192
|
let record = nodeData.data.record || {};
|
|
4186
4193
|
updateStyle.innerHTML = build_node_innerHTML(
|
|
4187
|
-
fill, priv.theme, record.icon,
|
|
4188
|
-
nodeData.data.desc
|
|
4194
|
+
fill, priv.theme, record.icon,
|
|
4195
|
+
node_label(nodeData.data.desc, record),
|
|
4196
|
+
nodeData.data.desc.topic_name, false, record.id
|
|
4189
4197
|
);
|
|
4190
4198
|
}
|
|
4191
4199
|
graph.updateNodeData([{ id: node_id, style: updateStyle }]);
|
|
@@ -4221,8 +4229,9 @@ function show_node_popover(gobj)
|
|
|
4221
4229
|
if(node_graph_type === 'hierarchical') {
|
|
4222
4230
|
let record = nodeData.data.record || {};
|
|
4223
4231
|
restoreStyle.innerHTML = build_node_innerHTML(
|
|
4224
|
-
origFill, priv.theme, record.icon,
|
|
4225
|
-
nodeData.data.desc
|
|
4232
|
+
origFill, priv.theme, record.icon,
|
|
4233
|
+
node_label(nodeData.data.desc, record),
|
|
4234
|
+
nodeData.data.desc.topic_name, false, record.id
|
|
4226
4235
|
);
|
|
4227
4236
|
}
|
|
4228
4237
|
graph.updateNodeData([{ id: node_id, style: restoreStyle }]);
|
|
@@ -4398,7 +4407,8 @@ function refresh_html_nodes_theme(gobj, theme)
|
|
|
4398
4407
|
style: {
|
|
4399
4408
|
innerHTML: build_node_innerHTML(
|
|
4400
4409
|
nd.data.desc.color, theme, record.icon,
|
|
4401
|
-
|
|
4410
|
+
node_label(nd.data.desc, record),
|
|
4411
|
+
nd.data.desc.topic_name, false, record.id
|
|
4402
4412
|
)
|
|
4403
4413
|
}
|
|
4404
4414
|
});
|
|
@@ -4408,7 +4418,7 @@ function refresh_html_nodes_theme(gobj, theme)
|
|
|
4408
4418
|
style: {
|
|
4409
4419
|
innerHTML: build_chip_innerHTML(
|
|
4410
4420
|
nd.data.desc.color, theme, record.icon,
|
|
4411
|
-
record.id
|
|
4421
|
+
node_label(nd.data.desc, record), record.id
|
|
4412
4422
|
)
|
|
4413
4423
|
}
|
|
4414
4424
|
});
|
|
@@ -4418,7 +4428,8 @@ function refresh_html_nodes_theme(gobj, theme)
|
|
|
4418
4428
|
style: {
|
|
4419
4429
|
innerHTML: build_node_innerHTML(
|
|
4420
4430
|
nd.data.desc.color, theme, record.icon,
|
|
4421
|
-
|
|
4431
|
+
node_label(nd.data.desc, record),
|
|
4432
|
+
nd.data.desc.topic_name, true, record.id
|
|
4422
4433
|
)
|
|
4423
4434
|
}
|
|
4424
4435
|
});
|
|
@@ -4464,8 +4475,9 @@ function refresh_default_edges_theme(gobj, theme)
|
|
|
4464
4475
|
* but lighter (1px border, no shadow, single line). The name is
|
|
4465
4476
|
* always legible (ellipsis + native title tooltip on overflow).
|
|
4466
4477
|
************************************************************/
|
|
4467
|
-
function build_chip_innerHTML(color, theme, icon,
|
|
4478
|
+
function build_chip_innerHTML(color, theme, icon, label, key)
|
|
4468
4479
|
{
|
|
4480
|
+
let title = key || label;
|
|
4469
4481
|
let dark = (theme === "dark");
|
|
4470
4482
|
let surface = dark ? "#1b2230" : "#ffffff";
|
|
4471
4483
|
let bg = dark
|
|
@@ -4485,7 +4497,7 @@ function build_chip_innerHTML(color, theme, icon, id)
|
|
|
4485
4497
|
}
|
|
4486
4498
|
|
|
4487
4499
|
return `
|
|
4488
|
-
<div title="${escapeHtml(
|
|
4500
|
+
<div title="${escapeHtml(title)}" style="
|
|
4489
4501
|
box-sizing: border-box;
|
|
4490
4502
|
width: 100%;
|
|
4491
4503
|
height: 100%;
|
|
@@ -4502,7 +4514,7 @@ function build_chip_innerHTML(color, theme, icon, id)
|
|
|
4502
4514
|
">${icon_html}<span style="
|
|
4503
4515
|
font-size: 12px; font-weight: 600; line-height: 1;
|
|
4504
4516
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
4505
|
-
">${escapeHtml(
|
|
4517
|
+
">${escapeHtml(label)}</span>
|
|
4506
4518
|
</div>
|
|
4507
4519
|
`;
|
|
4508
4520
|
}
|
|
@@ -4517,8 +4529,9 @@ function build_chip_innerHTML(color, theme, icon, id)
|
|
|
4517
4529
|
* colour is kept (per-topic differentiation) but softened via
|
|
4518
4530
|
* color-mix instead of a harsh saturated fill. Theme-aware.
|
|
4519
4531
|
************************************************************/
|
|
4520
|
-
function build_node_innerHTML(color, theme, icon,
|
|
4532
|
+
function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key)
|
|
4521
4533
|
{
|
|
4534
|
+
let title = key || label;
|
|
4522
4535
|
let dark = (theme === "dark");
|
|
4523
4536
|
let surface = dark ? "#1b2230" : "#ffffff";
|
|
4524
4537
|
let bg, border, border_style;
|
|
@@ -4568,7 +4581,7 @@ function build_node_innerHTML(color, theme, icon, id, topic_name, structural)
|
|
|
4568
4581
|
}
|
|
4569
4582
|
|
|
4570
4583
|
return `
|
|
4571
|
-
<div title="${escapeHtml(
|
|
4584
|
+
<div title="${escapeHtml(title)}" style="
|
|
4572
4585
|
box-sizing: border-box;
|
|
4573
4586
|
width: 100%;
|
|
4574
4587
|
height: 100%;
|
|
@@ -4591,7 +4604,7 @@ function build_node_innerHTML(color, theme, icon, id, topic_name, structural)
|
|
|
4591
4604
|
max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
|
4592
4605
|
display: -webkit-box; -webkit-line-clamp: 2;
|
|
4593
4606
|
-webkit-box-orient: vertical; word-break: break-word;
|
|
4594
|
-
">${escapeHtml(
|
|
4607
|
+
">${escapeHtml(label)}</div>${sub_html}
|
|
4595
4608
|
</div>
|
|
4596
4609
|
`;
|
|
4597
4610
|
}
|
|
@@ -4945,8 +4958,9 @@ function apply_node_properties(gobj, node_id, fill, stroke, lineWidth, scope)
|
|
|
4945
4958
|
if(nd.data.desc.node_treedb_type === 'hierarchical') {
|
|
4946
4959
|
let record = nd.data.record || {};
|
|
4947
4960
|
updateStyle.innerHTML = build_node_innerHTML(
|
|
4948
|
-
fill, priv.theme, record.icon,
|
|
4949
|
-
nd.data.desc
|
|
4961
|
+
fill, priv.theme, record.icon,
|
|
4962
|
+
node_label(nd.data.desc, record),
|
|
4963
|
+
nd.data.desc.topic_name, false, record.id
|
|
4950
4964
|
);
|
|
4951
4965
|
}
|
|
4952
4966
|
updates.push({ id: nodes[i].id, style: updateStyle });
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
/***********************************************************************
|
|
2
2
|
* c_yui_treedb_schema.js
|
|
3
3
|
*
|
|
4
|
-
* Schema-graph landing
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
4
|
+
* Schema-graph landing: the treedb drawn the way its `.c` literal
|
|
5
|
+
* draws it in ASCII (treedb_schema_*.c, treedb_system_schema.c) —
|
|
6
|
+
* one CARD per topic listing its fields in schema order, one edge
|
|
7
|
+
* per hook, from the row that declares the hook to the fkey row of
|
|
8
|
+
* the child it names. Built from the schema `descs` alone: no data,
|
|
9
|
+
* no backend calls.
|
|
10
|
+
*
|
|
11
|
+
* WHY THE CARD AND NOT A DOT. A topic is its fields; a schema is
|
|
12
|
+
* read to find out what a topic holds and what links to what. A
|
|
13
|
+
* graph of labelled dots answers neither, and the node graph next
|
|
14
|
+
* door (C_G6_NODES_TREE) answers a different question entirely —
|
|
15
|
+
* it draws the RECORDS, so on a treedb whose records are schemas
|
|
16
|
+
* it draws one box per column, hundreds of them, each labelled by
|
|
17
|
+
* a pkey that may be a rowid. This view draws the schema itself.
|
|
18
|
+
*
|
|
19
|
+
* The marks are the notation of the `.c` literals, so the drawing
|
|
20
|
+
* and the source read the same — see schema_rows().
|
|
21
|
+
*
|
|
22
|
+
* A node click opens that topic's table (a real hash navigation
|
|
23
|
+
* via the host-supplied `node_route`). An alternate landing to the
|
|
9
24
|
* topic cards, in the spirit of "every treedb is a graph".
|
|
10
25
|
*
|
|
11
26
|
* Copyright (c) 2026, ArtGins.
|
|
@@ -29,10 +44,13 @@ import {
|
|
|
29
44
|
gobj_read_bool_attr,
|
|
30
45
|
gobj_read_str_attr,
|
|
31
46
|
is_object,
|
|
47
|
+
escapeHtml,
|
|
32
48
|
} from "@yuneta/gobj-js";
|
|
33
49
|
|
|
34
50
|
import {Graph, NodeEvent} from "@antv/g6";
|
|
35
51
|
|
|
52
|
+
import {getStrokeColor} from "./lib_graph.js";
|
|
53
|
+
|
|
36
54
|
import {yui_is_dark, yui_watch_theme} from "./yui_theme.js";
|
|
37
55
|
|
|
38
56
|
import {t} from "i18next";
|
|
@@ -42,6 +60,31 @@ import {t} from "i18next";
|
|
|
42
60
|
***************************************************************/
|
|
43
61
|
const GCLASS_NAME = "C_YUI_TREEDB_SCHEMA";
|
|
44
62
|
|
|
63
|
+
/************************************************************
|
|
64
|
+
* Geometry of a card, in one place: the row height is what
|
|
65
|
+
* turns a field's index into the vertical position of its
|
|
66
|
+
* port, so an edge leaves the very row that declares the
|
|
67
|
+
* hook and lands on the row that declares the fkey.
|
|
68
|
+
************************************************************/
|
|
69
|
+
const CARD_HEADER_H = 30;
|
|
70
|
+
const CARD_ROW_H = 19;
|
|
71
|
+
const CARD_PAD_V = 7;
|
|
72
|
+
const CARD_MIN_W = 210;
|
|
73
|
+
const CARD_MAX_W = 340;
|
|
74
|
+
|
|
75
|
+
/* Same palette as the node graph (C_G6_NODES_TREE), so a topic
|
|
76
|
+
* keeps its colour when the operator moves between the two. */
|
|
77
|
+
const topic_colors = [
|
|
78
|
+
'rgb(237, 201, 73)',
|
|
79
|
+
'rgb(118, 183, 178)',
|
|
80
|
+
'rgb(255, 157, 167)',
|
|
81
|
+
'rgb(175, 122, 161)',
|
|
82
|
+
'rgb(89, 161, 79)',
|
|
83
|
+
'rgb(186, 176, 171)',
|
|
84
|
+
'rgb(66, 146, 198)',
|
|
85
|
+
];
|
|
86
|
+
const SYSTEM_TOPIC_COLOR = 'rgb(148, 163, 184)';
|
|
87
|
+
|
|
45
88
|
/***************************************************************
|
|
46
89
|
* Data
|
|
47
90
|
***************************************************************/
|
|
@@ -138,61 +181,318 @@ function build_ui(gobj)
|
|
|
138
181
|
gobj_write_attr(gobj, "$container", $container);
|
|
139
182
|
}
|
|
140
183
|
|
|
184
|
+
/************************************************************
|
|
185
|
+
* The shape of a hook/fkey field, from the type it declares.
|
|
186
|
+
* `dict` and `object` are the SAME thing to treedb (both build
|
|
187
|
+
* a json object keyed by child id), and so are `list` and
|
|
188
|
+
* `array` — see the hook/fkey switches in tr_treedb.c. A
|
|
189
|
+
* `string` holds one reference, so it draws as one.
|
|
190
|
+
************************************************************/
|
|
191
|
+
function hook_mark(type)
|
|
192
|
+
{
|
|
193
|
+
if(type === "dict" || type === "object") {
|
|
194
|
+
return "{}";
|
|
195
|
+
}
|
|
196
|
+
if(type === "list" || type === "array") {
|
|
197
|
+
return "[]";
|
|
198
|
+
}
|
|
199
|
+
return "()";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/************************************************************
|
|
203
|
+
* The fields of one topic, as the schema declares them.
|
|
204
|
+
*
|
|
205
|
+
* The marks are the notation of the schema `.c` literals
|
|
206
|
+
* (treedb_schema_*.c, treedb_system_schema.c), so the
|
|
207
|
+
* drawing and the source read the same:
|
|
208
|
+
* {} dict hook (N unique children)
|
|
209
|
+
* [] list hook (n not-unique children)
|
|
210
|
+
* () 1 child
|
|
211
|
+
* (↖) 1 fkey (1 parent)
|
|
212
|
+
* [↖] n fkeys (n parents)
|
|
213
|
+
* {↖} N fkeys (N parents)
|
|
214
|
+
* * required
|
|
215
|
+
* # the primary key
|
|
216
|
+
*
|
|
217
|
+
* A column can be BOTH hook and fkey, so the flags are read
|
|
218
|
+
* here rather than through treedb_get_field_desc's single
|
|
219
|
+
* `type`, which keeps only the last flag it saw.
|
|
220
|
+
************************************************************/
|
|
221
|
+
function schema_rows(desc)
|
|
222
|
+
{
|
|
223
|
+
let rows = [];
|
|
224
|
+
if(!is_object(desc) || !Array.isArray(desc.cols)) {
|
|
225
|
+
return rows;
|
|
226
|
+
}
|
|
227
|
+
for(let col of desc.cols) {
|
|
228
|
+
if(!is_object(col) || !col.id) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
let flags = Array.isArray(col.flag)? col.flag : [];
|
|
232
|
+
let is_hook = flags.indexOf("hook") >= 0;
|
|
233
|
+
let is_fkey = flags.indexOf("fkey") >= 0;
|
|
234
|
+
let mark = "";
|
|
235
|
+
if(is_hook) {
|
|
236
|
+
mark = hook_mark(col.type);
|
|
237
|
+
}
|
|
238
|
+
if(is_fkey) {
|
|
239
|
+
let fmark = hook_mark(col.type).replace("}", "↖}")
|
|
240
|
+
.replace("]", "↖]")
|
|
241
|
+
.replace(")", "↖)");
|
|
242
|
+
mark = mark? (mark + " " + fmark) : fmark;
|
|
243
|
+
}
|
|
244
|
+
rows.push({
|
|
245
|
+
name: col.id,
|
|
246
|
+
type: col.type || "",
|
|
247
|
+
mark: mark,
|
|
248
|
+
is_hook: is_hook,
|
|
249
|
+
is_fkey: is_fkey,
|
|
250
|
+
is_pkey: (col.id === desc.pkey),
|
|
251
|
+
required: (flags.indexOf("required") >= 0 || flags.indexOf("notnull") >= 0),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return rows;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/************************************************************
|
|
258
|
+
* Size of the card that holds those rows. The width follows
|
|
259
|
+
* the longest line so a long field name is not clipped into
|
|
260
|
+
* an ellipsis — the point of the drawing is to read them.
|
|
261
|
+
************************************************************/
|
|
262
|
+
function card_size(rows)
|
|
263
|
+
{
|
|
264
|
+
let longest = 0;
|
|
265
|
+
for(let row of rows) {
|
|
266
|
+
let len = row.name.length + (row.mark? row.mark.length + 2 : 0) + 2;
|
|
267
|
+
if(len > longest) {
|
|
268
|
+
longest = len;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
let w = Math.round(26 + longest * 6.6);
|
|
272
|
+
if(w < CARD_MIN_W) {
|
|
273
|
+
w = CARD_MIN_W;
|
|
274
|
+
}
|
|
275
|
+
if(w > CARD_MAX_W) {
|
|
276
|
+
w = CARD_MAX_W;
|
|
277
|
+
}
|
|
278
|
+
let h = CARD_HEADER_H + CARD_PAD_V * 2 + rows.length * CARD_ROW_H;
|
|
279
|
+
return [w, h];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/************************************************************
|
|
283
|
+
* Vertical placement of a field's port, as a fraction of the
|
|
284
|
+
* card height: the centre of its own row.
|
|
285
|
+
************************************************************/
|
|
286
|
+
function row_placement(index, height)
|
|
287
|
+
{
|
|
288
|
+
let y = CARD_HEADER_H + CARD_PAD_V + index * CARD_ROW_H + CARD_ROW_H / 2;
|
|
289
|
+
return y / height;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/************************************************************
|
|
293
|
+
* The card itself. Header = topic name on its topic colour;
|
|
294
|
+
* body = one line per field, in schema order.
|
|
295
|
+
************************************************************/
|
|
296
|
+
function build_card_innerHTML(topic, rows, color, dark)
|
|
297
|
+
{
|
|
298
|
+
let surface = dark? "#1b2230" : "#ffffff";
|
|
299
|
+
let bg = dark
|
|
300
|
+
? `color-mix(in srgb, ${color} 18%, #232b38)`
|
|
301
|
+
: `color-mix(in srgb, ${color} 6%, ${surface})`;
|
|
302
|
+
let border = dark
|
|
303
|
+
? `color-mix(in srgb, ${color} 85%, #ffffff)`
|
|
304
|
+
: color;
|
|
305
|
+
let header_bg = dark
|
|
306
|
+
? `color-mix(in srgb, ${color} 45%, #2c3542)`
|
|
307
|
+
: `color-mix(in srgb, ${color} 28%, ${surface})`;
|
|
308
|
+
let title_color = dark? "#e8eaed" : "#0f172a";
|
|
309
|
+
let text_color = dark? "#d3d8de" : "#334155";
|
|
310
|
+
let mute_color = dark? "#98a2b0" : "#7c8694";
|
|
311
|
+
let rule_color = dark
|
|
312
|
+
? "rgba(255,255,255,0.06)"
|
|
313
|
+
: "rgba(15,23,42,0.05)";
|
|
314
|
+
let shadow = dark
|
|
315
|
+
? "0 1px 3px rgba(0,0,0,0.45), 0 1px 2px rgba(0,0,0,0.30)"
|
|
316
|
+
: "0 1px 3px rgba(15,23,42,0.12), 0 1px 2px rgba(15,23,42,0.06)";
|
|
317
|
+
|
|
318
|
+
let rows_html = "";
|
|
319
|
+
for(let row of rows) {
|
|
320
|
+
let lead = row.is_pkey? "#" : (row.required? "*" : "");
|
|
321
|
+
let name_weight = (row.is_pkey || row.required)? "600" : "400";
|
|
322
|
+
let name_color = (row.is_hook || row.is_fkey)? title_color : text_color;
|
|
323
|
+
rows_html +=
|
|
324
|
+
` <div class="TREEDB_SCHEMA_FIELD" style="
|
|
325
|
+
display: flex; align-items: center; gap: 6px;
|
|
326
|
+
height: ${CARD_ROW_H}px; line-height: ${CARD_ROW_H}px;
|
|
327
|
+
padding: 0 10px; border-top: 1px solid ${rule_color};
|
|
328
|
+
">
|
|
329
|
+
<span style="
|
|
330
|
+
width: 8px; flex: 0 0 8px; text-align: center;
|
|
331
|
+
color: ${mute_color}; font-size: 11px;
|
|
332
|
+
">${lead}</span>
|
|
333
|
+
<span style="
|
|
334
|
+
flex: 1 1 auto; min-width: 0; overflow: hidden;
|
|
335
|
+
text-overflow: ellipsis; white-space: nowrap;
|
|
336
|
+
font-size: 12px; font-weight: ${name_weight}; color: ${name_color};
|
|
337
|
+
">${escapeHtml(row.name)}</span>
|
|
338
|
+
<span style="
|
|
339
|
+
flex: 0 0 auto; font-size: 11px; color: ${mute_color};
|
|
340
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
341
|
+
">${escapeHtml(row.mark)}</span>
|
|
342
|
+
</div>
|
|
343
|
+
`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return `
|
|
347
|
+
<div class="TREEDB_SCHEMA_CARD" title="${escapeHtml(topic)}" style="
|
|
348
|
+
box-sizing: border-box;
|
|
349
|
+
width: 100%;
|
|
350
|
+
height: 100%;
|
|
351
|
+
background: ${bg};
|
|
352
|
+
border: 1.5px solid ${border};
|
|
353
|
+
border-radius: 8px;
|
|
354
|
+
box-shadow: ${shadow};
|
|
355
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
356
|
+
display: flex;
|
|
357
|
+
flex-direction: column;
|
|
358
|
+
overflow: hidden;
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
">
|
|
361
|
+
<div class="TREEDB_SCHEMA_CARD_HEADER" style="
|
|
362
|
+
height: ${CARD_HEADER_H}px; line-height: ${CARD_HEADER_H}px;
|
|
363
|
+
flex: 0 0 ${CARD_HEADER_H}px;
|
|
364
|
+
padding: 0 10px; background: ${header_bg}; color: ${title_color};
|
|
365
|
+
font-size: 13px; font-weight: 700;
|
|
366
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
367
|
+
">${escapeHtml(topic)}</div>
|
|
368
|
+
<div class="TREEDB_SCHEMA_CARD_BODY" style="
|
|
369
|
+
flex: 1 1 auto; padding: ${CARD_PAD_V}px 0; overflow: hidden;
|
|
370
|
+
">
|
|
371
|
+
${rows_html} </div>
|
|
372
|
+
</div>
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
|
|
141
376
|
/************************************************************
|
|
142
377
|
* Derive {nodes, edges} from the schema.
|
|
143
|
-
*
|
|
144
|
-
*
|
|
378
|
+
*
|
|
379
|
+
* Node = a topic, drawn as the card the `.c` literal draws in
|
|
380
|
+
* ASCII: its name and its fields. Edge = a hook, from the row
|
|
381
|
+
* that declares it to the fkey row of the child it names —
|
|
382
|
+
* `'hook': {'users': 'departments'}` says both ends, so the
|
|
383
|
+
* arrow can land where the `.c` drawing lands it. An fkey
|
|
384
|
+
* whose parent declares no hook still gets its edge, or a
|
|
385
|
+
* half-declared schema would draw as disconnected.
|
|
386
|
+
*
|
|
145
387
|
* Left-to-right dagre follows the parent -> child data flow.
|
|
146
388
|
************************************************************/
|
|
147
389
|
function schema_to_graph(gobj)
|
|
148
390
|
{
|
|
149
391
|
let descs = gobj_read_attr(gobj, "descs");
|
|
150
392
|
let system = gobj_read_bool_attr(gobj, "system");
|
|
393
|
+
let dark = yui_is_dark();
|
|
151
394
|
let nodes = [];
|
|
152
|
-
let
|
|
395
|
+
let cards = {}; /* topic -> {rows, size, color} */
|
|
153
396
|
|
|
154
397
|
if(!is_object(descs)) {
|
|
155
398
|
return {nodes: nodes, edges: []};
|
|
156
399
|
}
|
|
157
400
|
|
|
401
|
+
let idx = 0;
|
|
158
402
|
for(let topic of Object.keys(descs)) {
|
|
159
|
-
|
|
403
|
+
let is_system = (topic.substring(0, 2) === "__");
|
|
404
|
+
if(!system && is_system) {
|
|
160
405
|
continue;
|
|
161
406
|
}
|
|
162
|
-
|
|
163
|
-
|
|
407
|
+
let rows = schema_rows(descs[topic]);
|
|
408
|
+
let color;
|
|
409
|
+
if(is_system) {
|
|
410
|
+
color = SYSTEM_TOPIC_COLOR;
|
|
411
|
+
} else {
|
|
412
|
+
color = topic_colors[idx % topic_colors.length];
|
|
413
|
+
idx++;
|
|
414
|
+
}
|
|
415
|
+
cards[topic] = {rows: rows, size: card_size(rows), color: color};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/*
|
|
419
|
+
* Ports: a hook leaves on the right, an fkey arrives on the
|
|
420
|
+
* left, each at the height of its own row.
|
|
421
|
+
*/
|
|
422
|
+
for(let topic of Object.keys(cards)) {
|
|
423
|
+
let card = cards[topic];
|
|
424
|
+
let [w, h] = card.size;
|
|
425
|
+
let ports = [];
|
|
426
|
+
for(let i = 0; i < card.rows.length; i++) {
|
|
427
|
+
let row = card.rows[i];
|
|
428
|
+
if(!row.is_hook && !row.is_fkey) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
ports.push({
|
|
432
|
+
key: row.name,
|
|
433
|
+
placement: [row.is_hook? 1 : 0, row_placement(i, h)],
|
|
434
|
+
fill: card.color,
|
|
435
|
+
stroke: getStrokeColor(card.color),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
nodes.push({
|
|
439
|
+
id: topic,
|
|
440
|
+
type: "html",
|
|
441
|
+
style: {
|
|
442
|
+
size: [w, h],
|
|
443
|
+
dx: -w / 2,
|
|
444
|
+
dy: -h / 2,
|
|
445
|
+
innerHTML: build_card_innerHTML(topic, card.rows, card.color, dark),
|
|
446
|
+
port: ports.length > 0,
|
|
447
|
+
ports: ports,
|
|
448
|
+
portR: 3,
|
|
449
|
+
portLineWidth: 1,
|
|
450
|
+
},
|
|
451
|
+
/* What a theme switch needs to repaint this card without
|
|
452
|
+
* rebuilding the graph (which would drop zoom/pan). */
|
|
453
|
+
data: {topic_name: topic, rows: card.rows, color: card.color},
|
|
454
|
+
});
|
|
164
455
|
}
|
|
165
456
|
|
|
166
457
|
let edge_seen = {};
|
|
167
458
|
let edges = [];
|
|
168
|
-
let add_edge = (source, target) => {
|
|
169
|
-
if(!
|
|
459
|
+
let add_edge = (source, source_port, target, target_port) => {
|
|
460
|
+
if(!cards[source] || !cards[target]) {
|
|
170
461
|
return;
|
|
171
462
|
}
|
|
172
|
-
let key = source
|
|
463
|
+
let key = `${source}|${source_port}|${target}|${target_port}`;
|
|
173
464
|
if(edge_seen[key]) {
|
|
174
465
|
return;
|
|
175
466
|
}
|
|
176
467
|
edge_seen[key] = true;
|
|
177
|
-
edges.push({
|
|
468
|
+
edges.push({
|
|
469
|
+
id: key,
|
|
470
|
+
type: "cubic",
|
|
471
|
+
source: source,
|
|
472
|
+
target: target,
|
|
473
|
+
style: {sourcePort: source_port, targetPort: target_port},
|
|
474
|
+
});
|
|
178
475
|
};
|
|
179
476
|
|
|
180
|
-
for(let topic of Object.keys(
|
|
477
|
+
for(let topic of Object.keys(cards)) {
|
|
181
478
|
let desc = descs[topic];
|
|
182
479
|
if(!is_object(desc) || !Array.isArray(desc.cols)) {
|
|
183
480
|
continue;
|
|
184
481
|
}
|
|
185
482
|
for(let col of desc.cols) {
|
|
186
|
-
if(!col) {
|
|
483
|
+
if(!is_object(col) || !col.id) {
|
|
187
484
|
continue;
|
|
188
485
|
}
|
|
189
486
|
if(is_object(col.hook)) {
|
|
487
|
+
/* {child_topic: fkey_field_of_the_child} */
|
|
190
488
|
for(let child of Object.keys(col.hook)) {
|
|
191
|
-
add_edge(topic, child);
|
|
489
|
+
add_edge(topic, col.id, child, col.hook[child]);
|
|
192
490
|
}
|
|
193
|
-
}
|
|
491
|
+
}
|
|
492
|
+
if(is_object(col.fkey)) {
|
|
493
|
+
/* {parent_topic: hook_field_of_the_parent} */
|
|
194
494
|
for(let parent of Object.keys(col.fkey)) {
|
|
195
|
-
add_edge(parent, topic);
|
|
495
|
+
add_edge(parent, col.fkey[parent], topic, col.id);
|
|
196
496
|
}
|
|
197
497
|
}
|
|
198
498
|
}
|
|
@@ -202,29 +502,16 @@ function schema_to_graph(gobj)
|
|
|
202
502
|
}
|
|
203
503
|
|
|
204
504
|
/************************************************************
|
|
205
|
-
* The theme-dependent
|
|
206
|
-
*
|
|
505
|
+
* The theme-dependent edge style, in ONE place: applied as
|
|
506
|
+
* the graph is built and re-applied on a theme switch
|
|
207
507
|
* (ac_theme), which restyles the LIVE graph — rebuilding it
|
|
208
508
|
* would drop the user's zoom/pan and any dragged node.
|
|
209
509
|
************************************************************/
|
|
210
|
-
function node_style(dark)
|
|
211
|
-
{
|
|
212
|
-
return {
|
|
213
|
-
size: 40,
|
|
214
|
-
fill: "#5B8FF9",
|
|
215
|
-
labelText: (d) => d.id,
|
|
216
|
-
labelPlacement: "bottom",
|
|
217
|
-
labelFill: dark ? "#e6e6e6" : "#333333",
|
|
218
|
-
labelBackground: true,
|
|
219
|
-
labelBackgroundFill: dark ? "rgba(0,0,0,0.5)" : "rgba(255,255,255,0.7)",
|
|
220
|
-
cursor: "pointer",
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
510
|
function edge_style(dark)
|
|
225
511
|
{
|
|
226
512
|
return {
|
|
227
|
-
stroke: dark ? "#
|
|
513
|
+
stroke: dark ? "#7a8593" : "#9aa4b2",
|
|
514
|
+
lineWidth: 1.2,
|
|
228
515
|
endArrow: true,
|
|
229
516
|
};
|
|
230
517
|
}
|
|
@@ -258,17 +545,14 @@ function build_graph(gobj)
|
|
|
258
545
|
container: $container,
|
|
259
546
|
autoResize: true,
|
|
260
547
|
data: data,
|
|
261
|
-
node: {
|
|
262
|
-
style: node_style(dark),
|
|
263
|
-
},
|
|
264
548
|
edge: {
|
|
265
549
|
style: edge_style(dark),
|
|
266
550
|
},
|
|
267
551
|
layout: {
|
|
268
552
|
type: "antv-dagre",
|
|
269
553
|
rankdir: "LR",
|
|
270
|
-
nodesep:
|
|
271
|
-
ranksep:
|
|
554
|
+
nodesep: 28,
|
|
555
|
+
ranksep: 90,
|
|
272
556
|
},
|
|
273
557
|
behaviors: ["zoom-canvas", "drag-canvas", "drag-element"],
|
|
274
558
|
});
|
|
@@ -386,8 +670,29 @@ function ac_theme(gobj, event, kw, src)
|
|
|
386
670
|
let dark = (kw && kw.theme) ? (kw.theme === "dark") : yui_is_dark();
|
|
387
671
|
try {
|
|
388
672
|
graph.setTheme(dark ? "dark" : "light");
|
|
389
|
-
graph.setNode({style: node_style(dark)});
|
|
390
673
|
graph.setEdge({style: edge_style(dark)});
|
|
674
|
+
|
|
675
|
+
/* A card is HTML, so its colours live in its markup: repaint
|
|
676
|
+
* each one from the rows kept in its node data. */
|
|
677
|
+
let updates = [];
|
|
678
|
+
for(let node of graph.getNodeData()) {
|
|
679
|
+
let data = node.data;
|
|
680
|
+
if(!is_object(data) || !Array.isArray(data.rows)) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
updates.push({
|
|
684
|
+
id: node.id,
|
|
685
|
+
style: {
|
|
686
|
+
innerHTML: build_card_innerHTML(
|
|
687
|
+
data.topic_name, data.rows, data.color, dark
|
|
688
|
+
)
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if(updates.length > 0) {
|
|
693
|
+
graph.updateNodeData(updates);
|
|
694
|
+
}
|
|
695
|
+
|
|
391
696
|
graph.draw().catch((e) => {
|
|
392
697
|
log_error(`${gobj_short_name(gobj)}: schema graph redraw failed: ${e}`);
|
|
393
698
|
});
|