@yuneta/gobj-ui 7.23.170 → 7.23.172

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.170",
3
+ "version": "7.23.172",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -30,13 +30,13 @@
30
30
  "devDependencies": {
31
31
  "@rollup/plugin-terser": "^1.0.0",
32
32
  "@vitest/coverage-v8": "^4.1.10",
33
- "@yuneta/gobj-js": "^7.21.0",
33
+ "@yuneta/gobj-js": "^7.22.0",
34
34
  "vite": "^8.2.1",
35
35
  "vitest": "^4.1.10"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@antv/g6": "^5.1.0",
39
- "@yuneta/gobj-js": "^7.16.6",
39
+ "@yuneta/gobj-js": "^7.22.0",
40
40
  "bulma": "^1.0.4",
41
41
  "i18next": "^26.3.6",
42
42
  "maplibre-gl": "^6.4.1",
@@ -68,6 +68,7 @@ import {
68
68
  yui_graph_update_zoom,
69
69
  } from "./yui_graph_camera.js";
70
70
  import {ensure_pinch_zoom_patch} from "./g6_touch_gestures.js";
71
+ import {col_key_roles} from "./treedb_topic_keys.js";
71
72
 
72
73
  import {getStrokeColor} from "./lib_graph.js";
73
74
 
@@ -260,12 +261,20 @@ function hook_mark(type)
260
261
  * (↖) 1 fkey (1 parent)
261
262
  * [↖] n fkeys (n parents)
262
263
  * {↖} N fkeys (N parents)
264
+ * (2) a secondary key (pkey2)
265
+ * (t) the time key (tkey)
263
266
  * * required
264
267
  * # the primary key
265
268
  *
266
- * A column can be BOTH hook and fkey, so the flags are read
267
- * here rather than through treedb_get_field_desc's single
268
- * `type`, which keeps only the last flag it saw.
269
+ * The key marks lead the mark column, and a pkey2 is drawn in
270
+ * bold like the pkey: a topic with secondary keys keeps several
271
+ * instances under one id, which is the first thing to know
272
+ * about it. `(t)` is new to the notation: the literals' legend
273
+ * carries it too.
274
+ *
275
+ * The flags are read here rather than through
276
+ * treedb_get_field_desc's single `type`, which keeps only the
277
+ * last flag it saw.
269
278
  ************************************************************/
270
279
  function schema_rows(desc)
271
280
  {
@@ -280,6 +289,14 @@ function schema_rows(desc)
280
289
  let flags = Array.isArray(col.flag)? col.flag : [];
281
290
  let is_hook = flags.indexOf("hook") >= 0;
282
291
  let is_fkey = flags.indexOf("fkey") >= 0;
292
+ let roles = col_key_roles(desc, col.id);
293
+ let key_marks = [];
294
+ if(roles.includes("pkey2")) {
295
+ key_marks.push("(2)");
296
+ }
297
+ if(roles.includes("tkey")) {
298
+ key_marks.push("(t)");
299
+ }
283
300
  let mark = "";
284
301
  if(is_hook) {
285
302
  mark = hook_mark(col.type);
@@ -290,13 +307,17 @@ function schema_rows(desc)
290
307
  .replace(")", "↖)");
291
308
  mark = mark? (mark + " " + fmark) : fmark;
292
309
  }
310
+ if(key_marks.length) {
311
+ mark = mark? (key_marks.join(" ") + " " + mark) : key_marks.join(" ");
312
+ }
293
313
  rows.push({
294
314
  name: col.id,
295
315
  type: col.type || "",
296
316
  mark: mark,
297
317
  is_hook: is_hook,
298
318
  is_fkey: is_fkey,
299
- is_pkey: (col.id === desc.pkey),
319
+ is_pkey: roles.includes("pkey"),
320
+ is_pkey2: roles.includes("pkey2"),
300
321
  required: (flags.indexOf("required") >= 0 || flags.indexOf("notnull") >= 0),
301
322
  });
302
323
  }
@@ -367,7 +388,7 @@ function build_card_innerHTML(topic, rows, color, dark)
367
388
  let rows_html = "";
368
389
  for(let row of rows) {
369
390
  let lead = row.is_pkey? "#" : (row.required? "*" : "");
370
- let name_weight = (row.is_pkey || row.required)? "600" : "400";
391
+ let name_weight = (row.is_pkey || row.is_pkey2)? "700" : (row.required? "600" : "400");
371
392
  let name_color = (row.is_hook || row.is_fkey)? title_color : text_color;
372
393
  rows_html +=
373
394
  ` <div class="TREEDB_SCHEMA_FIELD" style="
@@ -61,6 +61,9 @@
61
61
  text-overflow: ellipsis;
62
62
  color: var(--bulma-text, inherit);
63
63
  }
64
+ .TREEDB_TOPIC_CARD_PKEY2S .TREEDB_TOPIC_CARD_REL_LIST {
65
+ font-weight: 600;
66
+ }
64
67
  .TREEDB_TOPIC_CARD_ACTIONS {
65
68
  display: flex;
66
69
  justify-content: center;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Management of TreeDB's topics with Bulma tabs
5
5
  *
6
- * Copyright (c) 2025, ArtGins.
6
+ * Copyright (c) 2025-2026, ArtGins.
7
7
  * All Rights Reserved.
8
8
  ***********************************************************************/
9
9
 
@@ -66,6 +66,9 @@ import {
66
66
  } from "./c_yui_shell.js";
67
67
  import {nodes_answer} from "./nodes_answer.js";
68
68
  import {yui_toolbar_icon} from "./yui_toolbar.js";
69
+ import {
70
+ desc_pkey2s, desc_tkey, col_key_roles, system_flag_names
71
+ } from "./treedb_topic_keys.js";
69
72
 
70
73
  import {t} from "i18next";
71
74
 
@@ -688,6 +691,17 @@ function topic_card_meta(desc)
688
691
  ]]);
689
692
  meta.push(['div', {class: 'TREEDB_TOPIC_CARD_META'}, head]);
690
693
 
694
+ /* The secondary keys come before the relations: they change what
695
+ * a record of this topic IS (several instances under one id). */
696
+ let pkey2s = desc_pkey2s(desc);
697
+ if(pkey2s.length) {
698
+ let list = pkey2s.join(", ");
699
+ meta.push(['div', {class: 'TREEDB_TOPIC_CARD_REL TREEDB_TOPIC_CARD_PKEY2S', title: list}, [
700
+ ['span', {class: 'TREEDB_TOPIC_CARD_REL_LABEL', i18n: 'pkey2s'}, 'pkey2s'],
701
+ ['span', {class: 'TREEDB_TOPIC_CARD_REL_LIST'}, list]
702
+ ]]);
703
+ }
704
+
691
705
  let relation = (cls, key, arrow, names) => {
692
706
  let list = names.join(", ");
693
707
  return ['div', {class: `TREEDB_TOPIC_CARD_REL ${cls}`, title: list}, [
@@ -1021,9 +1035,15 @@ function show_topic_info(gobj, topic)
1021
1035
 
1022
1036
  /************************************************************
1023
1037
  * Build the read-only topic-info panel from a topic desc: the
1024
- * topic name, its pkey, and a table of columns (name / type /
1038
+ * topic name, its KEYS, and a table of columns (name / type /
1025
1039
  * key relationship). Everything is guarded — a malformed desc
1026
1040
  * renders a shorter panel, never throws.
1041
+ *
1042
+ * The keys are what an operator has to know before touching a
1043
+ * record, so they are all said, and the empty ones too: a
1044
+ * topic with `pkey2s` keeps several instances under one id,
1045
+ * and an empty `tkey` means the time of a record is the time
1046
+ * it was appended -- a row missing is not the same as "none".
1027
1047
  ************************************************************/
1028
1048
  function build_topic_info_panel(gobj, $info, topic, desc)
1029
1049
  {
@@ -1032,38 +1052,39 @@ function build_topic_info_panel(gobj, $info, topic, desc)
1032
1052
  }
1033
1053
 
1034
1054
  let pkey = desc.pkey || "";
1055
+ let pkey2s = desc_pkey2s(desc);
1056
+ let tkey = desc_tkey(desc);
1035
1057
 
1036
- /* Topic metadata (version matters to the operator): version /
1037
- * system / pkey / tkey. Each row is shown only when its value is
1038
- * present; the version is emphasised as a tag. */
1039
- let fmt_flag = (v) => {
1040
- if(is_array(v)) {
1041
- return v.join(", ");
1042
- }
1043
- if(is_object(v)) {
1044
- return Object.keys(v).join(", ");
1045
- }
1046
- return (v === undefined || v === null) ? "" : String(v);
1047
- };
1058
+ /* Topic metadata: version / system / pkey / pkey2s / tkey. The
1059
+ * version and the secondary keys are emphasised as tags. */
1048
1060
  let $meta = [];
1049
- let push_meta = (key, value, highlight) => {
1050
- if(value === undefined || value === null || value === "") {
1051
- return;
1052
- }
1053
- let $val = highlight
1054
- ? ["span", {class: "tag is-info"}, `${value}`]
1055
- : ["code", {}, `${value}`];
1061
+ let push_meta = (key, $val) => {
1056
1062
  $meta.push(
1057
- ["tr", {}, [
1063
+ ["tr", {class: `TREEDB_TOPIC_INFO_${key.toUpperCase()}`}, [
1058
1064
  ["th", {i18n: key, style: "width:11rem;"}, key],
1059
1065
  ["td", {}, [$val]]
1060
1066
  ]]
1061
1067
  );
1062
1068
  };
1063
- push_meta("version", desc.topic_version, true);
1064
- push_meta("system", fmt_flag(desc.system_flag));
1065
- push_meta("pkey", pkey);
1066
- push_meta("tkey", desc.tkey);
1069
+ if(desc.topic_version !== undefined && desc.topic_version !== null && desc.topic_version !== "") {
1070
+ push_meta("version", ["span", {class: "tag is-info"}, `${desc.topic_version}`]);
1071
+ }
1072
+ let sf_names = system_flag_names(desc.system_flag);
1073
+ if(sf_names.length > 0) {
1074
+ push_meta("system", ["code", {title: `${desc.system_flag}`}, sf_names.join(" | ")]);
1075
+ }
1076
+ if(pkey) {
1077
+ push_meta("pkey", ["code", {}, pkey]);
1078
+ }
1079
+ if(pkey2s.length > 0) {
1080
+ push_meta("pkey2s", ["span", {class: "tags"},
1081
+ pkey2s.map(name => ["span", {class: "tag is-warning"}, name])
1082
+ ]);
1083
+ }
1084
+ push_meta("tkey", tkey
1085
+ ? ["code", {}, tkey]
1086
+ : ["span", {class: "has-text-grey", i18n: "append time"}, "append time"]
1087
+ );
1067
1088
 
1068
1089
  let $rows = [];
1069
1090
  let cols = is_array(desc.cols) ? desc.cols : [];
@@ -1072,19 +1093,20 @@ function build_topic_info_panel(gobj, $info, topic, desc)
1072
1093
  continue;
1073
1094
  }
1074
1095
  let type = col.type || (is_array(col.flag) ? col.flag.join(", ") : (col.flag || ""));
1075
- let rel = "";
1076
- if(col.id === pkey) {
1077
- rel = "pkey";
1078
- } else if(is_object(col.fkey)) {
1079
- rel = "→ " + Object.keys(col.fkey).join(", ");
1096
+ let roles = col_key_roles(desc, col.id);
1097
+ let parts = roles.slice();
1098
+ if(is_object(col.fkey)) {
1099
+ parts.push("→ " + Object.keys(col.fkey).join(", "));
1080
1100
  } else if(is_object(col.hook)) {
1081
- rel = "hook → " + Object.keys(col.hook).join(", ");
1101
+ parts.push("hook → " + Object.keys(col.hook).join(", "));
1082
1102
  }
1103
+ let rel = parts.join(" · ");
1104
+ let is_key = roles.length > 0;
1083
1105
  $rows.push(
1084
- ["tr", {}, [
1085
- ["td", {}, [["code", {}, `${col.id}`]]],
1106
+ ["tr", is_key ? {class: "TREEDB_TOPIC_INFO_KEY_COL"} : {}, [
1107
+ ["td", {}, [["code", is_key ? {class: "has-text-weight-bold"} : {}, `${col.id}`]]],
1086
1108
  ["td", {}, `${type}`],
1087
- ["td", {}, `${rel}`]
1109
+ ["td", is_key ? {class: "has-text-weight-semibold"} : {}, `${rel}`]
1088
1110
  ]]
1089
1111
  );
1090
1112
  }
@@ -0,0 +1,122 @@
1
+ /***********************************************************************
2
+ * treedb_topic_keys.js
3
+ *
4
+ * The KEYS of a treedb topic, read from its desc: the pkey, the
5
+ * secondary keys (`pkey2s`) and the time key (`tkey`), plus the
6
+ * names behind its `system_flag` number.
7
+ *
8
+ * Every view that describes a topic -- the schema diagram, the
9
+ * topic-info panel, the topic cards -- must say them, and say them
10
+ * the same way. A topic with `pkey2s` is not a topic with one more
11
+ * column: one id holds several INSTANCES, an update has to carry
12
+ * the pkey2 or it lands on the primary, and `delete-instance`
13
+ * exists. `tkey` names where the time of every record comes from.
14
+ * The views said neither, so this is where they ask.
15
+ *
16
+ * Pure, so it is tested with no DOM.
17
+ *
18
+ * Copyright (c) 2026, ArtGins.
19
+ * All Rights Reserved.
20
+ ***********************************************************************/
21
+ import {topic_pkey2s} from "./schema_model.js";
22
+
23
+ /* The names of the system_flag bits, in bit order: the `sf_names`
24
+ * table of timeranger2.c, which the C enum points at. Keep the two in
25
+ * step -- an empty slot is a bit with no name. */
26
+ const SF_NAMES = [
27
+ "sf_string_key", // 0x0001
28
+ "sf_rowid_key", // 0x0002
29
+ "sf_int_key", // 0x0004
30
+ "", // 0x0008
31
+ "sf_zip_record", // 0x0010
32
+ "sf_cipher_record", // 0x0020
33
+ "", // 0x0040
34
+ "", // 0x0080
35
+ "sf_t_ms", // 0x0100
36
+ "sf_tm_ms", // 0x0200
37
+ "sf_deleted_instance", // 0x0400
38
+ "sf_immutable_record", // 0x0800
39
+ "sf_loading_from_disk", // 0x1000
40
+ "", // 0x2000
41
+ "", // 0x4000
42
+ "", // 0x8000
43
+ ];
44
+
45
+
46
+ /************************************************************
47
+ * The secondary keys of a topic, always a list. The desc
48
+ * carries a list; a schema literal may declare a bare
49
+ * string.
50
+ ************************************************************/
51
+ function desc_pkey2s(desc)
52
+ {
53
+ return topic_pkey2s(desc);
54
+ }
55
+
56
+ /************************************************************
57
+ * The time key of a topic, "" when it has none -- and then
58
+ * the time of a record is the time it was appended.
59
+ ************************************************************/
60
+ function desc_tkey(desc)
61
+ {
62
+ let tkey = desc ? desc.tkey : "";
63
+ return (typeof tkey === "string") ? tkey : "";
64
+ }
65
+
66
+ /************************************************************
67
+ * Which keys of its topic a column IS, in the order the
68
+ * views print them: "pkey", "pkey2", "tkey". Empty for a
69
+ * column that is none.
70
+ ************************************************************/
71
+ function col_key_roles(desc, col_id)
72
+ {
73
+ let roles = [];
74
+ if(!desc || typeof col_id !== "string" || col_id.length === 0) {
75
+ return roles;
76
+ }
77
+ if(col_id === (desc.pkey || "id")) {
78
+ roles.push("pkey");
79
+ }
80
+ if(desc_pkey2s(desc).includes(col_id)) {
81
+ roles.push("pkey2");
82
+ }
83
+ if(desc_tkey(desc) === col_id) {
84
+ roles.push("tkey");
85
+ }
86
+ return roles;
87
+ }
88
+
89
+ /************************************************************
90
+ * The names of a system_flag. The desc carries the NUMBER
91
+ * (`1`), a schema literal carries the names
92
+ * ("sf_string_key|sf_t_ms"); both come back as a list. A
93
+ * set bit with no name is shown by its value, never
94
+ * dropped.
95
+ ************************************************************/
96
+ function system_flag_names(system_flag)
97
+ {
98
+ if(Array.isArray(system_flag)) {
99
+ return system_flag.filter(n => typeof n === "string" && n.length > 0);
100
+ }
101
+ if(typeof system_flag === "string") {
102
+ return system_flag.split(/[|,]/).map(n => n.trim()).filter(n => n.length > 0);
103
+ }
104
+ if(typeof system_flag !== "number" || !Number.isInteger(system_flag) || system_flag <= 0) {
105
+ return [];
106
+ }
107
+ let names = [];
108
+ for(let bit = 0; bit < SF_NAMES.length; bit++) {
109
+ let mask = 1 << bit;
110
+ if(system_flag & mask) {
111
+ names.push(SF_NAMES[bit] || `0x${mask.toString(16).padStart(4, "0")}`);
112
+ }
113
+ }
114
+ return names;
115
+ }
116
+
117
+ export {
118
+ desc_pkey2s,
119
+ desc_tkey,
120
+ col_key_roles,
121
+ system_flag_names,
122
+ };
@@ -0,0 +1,79 @@
1
+ /***********************************************************************
2
+ * treedb_topic_keys.test.js
3
+ *
4
+ * The keys of a topic, read from its desc, pinned: the shapes the
5
+ * backend and the schema literals really send.
6
+ ***********************************************************************/
7
+ import { describe, test, expect } from "vitest";
8
+ import {
9
+ desc_pkey2s, desc_tkey, col_key_roles, system_flag_names
10
+ } from "./treedb_topic_keys.js";
11
+
12
+ /* The `binaries` topic of treedb_yuneta_agent, as `descs` answers it. */
13
+ const BINARIES = {
14
+ topic_name: "binaries", pkey: "id", tkey: "", system_flag: 1,
15
+ topic_version: 3, pkey2s: ["version"],
16
+ cols: [{id: "id"}, {id: "version"}, {id: "size"}]
17
+ };
18
+
19
+ describe("desc_pkey2s", () => {
20
+ test("a list, as the desc carries it", () => {
21
+ expect(desc_pkey2s(BINARIES)).toEqual(["version"]);
22
+ });
23
+ test("a bare string, as a schema literal declares it", () => {
24
+ expect(desc_pkey2s({pkey2s: "yuno_release"})).toEqual(["yuno_release"]);
25
+ });
26
+ test("absent or empty is no secondary key", () => {
27
+ expect(desc_pkey2s({})).toEqual([]);
28
+ expect(desc_pkey2s({pkey2s: ""})).toEqual([]);
29
+ expect(desc_pkey2s(null)).toEqual([]);
30
+ });
31
+ });
32
+
33
+ describe("desc_tkey", () => {
34
+ test("empty is the append time", () => {
35
+ expect(desc_tkey(BINARIES)).toBe("");
36
+ expect(desc_tkey({})).toBe("");
37
+ expect(desc_tkey(null)).toBe("");
38
+ });
39
+ test("a named column", () => {
40
+ expect(desc_tkey({tkey: "tm"})).toBe("tm");
41
+ });
42
+ });
43
+
44
+ describe("col_key_roles", () => {
45
+ test("the pkey, a pkey2, and a column that is neither", () => {
46
+ expect(col_key_roles(BINARIES, "id")).toEqual(["pkey"]);
47
+ expect(col_key_roles(BINARIES, "version")).toEqual(["pkey2"]);
48
+ expect(col_key_roles(BINARIES, "size")).toEqual([]);
49
+ });
50
+ test("a column can be a pkey2 AND the tkey", () => {
51
+ expect(col_key_roles({pkey: "id", pkey2s: ["tm"], tkey: "tm"}, "tm"))
52
+ .toEqual(["pkey2", "tkey"]);
53
+ });
54
+ test("an empty tkey marks no column", () => {
55
+ expect(col_key_roles({pkey: "id", tkey: ""}, "")).toEqual([]);
56
+ });
57
+ test("the pkey defaults to id", () => {
58
+ expect(col_key_roles({}, "id")).toEqual(["pkey"]);
59
+ });
60
+ });
61
+
62
+ describe("system_flag_names", () => {
63
+ test("the number the desc carries", () => {
64
+ expect(system_flag_names(1)).toEqual(["sf_string_key"]);
65
+ expect(system_flag_names(0x0002 | 0x0100)).toEqual(["sf_rowid_key", "sf_t_ms"]);
66
+ });
67
+ test("a set bit with no name is shown by its value", () => {
68
+ expect(system_flag_names(0x0009)).toEqual(["sf_string_key", "0x0008"]);
69
+ });
70
+ test("the names a schema literal carries", () => {
71
+ expect(system_flag_names("sf_string_key|sf_t_ms")).toEqual(["sf_string_key", "sf_t_ms"]);
72
+ expect(system_flag_names(["sf_int_key"])).toEqual(["sf_int_key"]);
73
+ });
74
+ test("nothing, zero or garbage is no name", () => {
75
+ expect(system_flag_names(0)).toEqual([]);
76
+ expect(system_flag_names(undefined)).toEqual([]);
77
+ expect(system_flag_names(1.5)).toEqual([]);
78
+ });
79
+ });