@yuneta/gobj-ui 7.23.169 → 7.23.171

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.169",
3
+ "version": "7.23.171",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@rollup/plugin-terser": "^1.0.0",
32
32
  "@vitest/coverage-v8": "^4.1.10",
33
- "@yuneta/gobj-js": "^7.16.6",
33
+ "@yuneta/gobj-js": "^7.21.0",
34
34
  "vite": "^8.2.1",
35
35
  "vitest": "^4.1.10"
36
36
  },
@@ -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="
@@ -96,7 +96,9 @@ import {t} from "i18next";
96
96
 
97
97
  import {yui_toolbar_icon} from "./yui_toolbar.js";
98
98
 
99
- import {plan_treedb_writes, READONLY_FORM_TOOLBAR} from "./treedb_write_plan.js";
99
+ import {
100
+ plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb
101
+ } from "./treedb_write_plan.js";
100
102
  import {
101
103
  yui_file_read, yui_files_manifest, yui_file_id_label, yui_file_size_label
102
104
  } from "./yui_file_field.js";
@@ -3262,42 +3264,6 @@ function get_schema_col(gobj, id)
3262
3264
  return null;
3263
3265
  }
3264
3266
 
3265
- /************************************************************
3266
- * Does column `col` travel back to treedb on a write?
3267
- *
3268
- * The form SHOWS every field and sends back only what the topic
3269
- * accepts: the writable cols, the fkeys (a link is edited by
3270
- * linking) and the pkey, which is not written but is what
3271
- * ADDRESSES the record.
3272
- *
3273
- * It matters because `treedb_update_node()` does not check
3274
- * `writable` -- it writes any col it is handed -- so a read-only
3275
- * field travelling back is written with whatever the form made of
3276
- * it, and the fields that describe a record are exactly the ones
3277
- * that do not survive a round trip through a widget: a `time` is an
3278
- * integer rendered as a `datetime-local`, with no seconds, so every
3279
- * save moved it back up to 59 s. Nothing looked wrong until the
3280
- * stored timestamp had moved.
3281
- *
3282
- * `is_file` and not the type: a `file` column IS an fkey (it is
3283
- * flagged ['fkey','file']) but answers `type: "file"`, and the write
3284
- * goes out with `autolink`, which rebuilds the links from what the
3285
- * record carries. Dropped, a read-only `file` column -- the one only
3286
- * a load fills -- was UNLINKED by every save of any other field.
3287
- ************************************************************/
3288
- function col_goes_back_to_treedb(gobj, col)
3289
- {
3290
- let desc = gobj_read_attr(gobj, "desc");
3291
- let pkey = desc.pkey || "id";
3292
- const field_desc = treedb_get_field_desc(col);
3293
-
3294
- if(field_desc.is_writable || field_desc.type === "fkey" ||
3295
- field_desc.is_file || col.id === pkey) {
3296
- return true;
3297
- }
3298
- return false;
3299
- }
3300
-
3301
3267
  /************************************************************
3302
3268
  * The record the FORM hands over, without the columns that do
3303
3269
  * not go back to treedb (see col_goes_back_to_treedb()). The
@@ -3309,7 +3275,7 @@ function strip_read_only_cols(gobj, record)
3309
3275
  let row = {};
3310
3276
  for(let field_name of Object.keys(record || {})) {
3311
3277
  let col = get_schema_col(gobj, field_name);
3312
- if(col && !col_goes_back_to_treedb(gobj, col)) {
3278
+ if(col && !col_goes_back_to_treedb(gobj_read_attr(gobj, "desc"), col)) {
3313
3279
  continue;
3314
3280
  }
3315
3281
  row[field_name] = record[field_name];
@@ -3336,7 +3302,7 @@ function transform__form_record_2_treedb_record(gobj, kw, operation)
3336
3302
  continue;
3337
3303
  }
3338
3304
 
3339
- if(!col_goes_back_to_treedb(gobj, col)) {
3305
+ if(!col_goes_back_to_treedb(gobj_read_attr(gobj, "desc"), col)) {
3340
3306
  continue;
3341
3307
  }
3342
3308
 
@@ -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
+ });
@@ -19,6 +19,9 @@
19
19
  * All Rights Reserved.
20
20
  ***********************************************************************/
21
21
 
22
+ import {treedb_get_field_desc} from "@yuneta/gobj-js";
23
+ import {topic_pkey2s} from "./schema_model.js";
24
+
22
25
  /* What a read-only form keeps: reading a record includes taking it with
23
26
  * you, so `copy` stays and the write half goes. */
24
27
  const READONLY_FORM_TOOLBAR = ["copy"];
@@ -70,4 +73,47 @@ function plan_treedb_writes(flags)
70
73
  }
71
74
 
72
75
 
73
- export {plan_treedb_writes, READONLY_FORM_TOOLBAR};
76
+ /************************************************************
77
+ * Does column `col` travel back to treedb on a write?
78
+ *
79
+ * The form SHOWS every field and sends back only what the topic
80
+ * accepts: the writable cols, the fkeys (a link is edited by
81
+ * linking) and the pkey, which is not written but is what
82
+ * ADDRESSES the record.
83
+ *
84
+ * It matters because `treedb_update_node()` does not check
85
+ * `writable` -- it writes any col it is handed -- so a read-only
86
+ * field travelling back is written with whatever the form made of
87
+ * it, and the fields that describe a record are exactly the ones
88
+ * that do not survive a round trip through a widget: a `time` is an
89
+ * integer rendered as a `datetime-local`, with no seconds, so every
90
+ * save moved it back up to 59 s. Nothing looked wrong until the
91
+ * stored timestamp had moved.
92
+ *
93
+ * `is_file` and not the type: a `file` column IS an fkey (it is
94
+ * flagged ['fkey','file']) but answers `type: "file"`, and the write
95
+ * goes out with `autolink`, which rebuilds the links from what the
96
+ * record carries. Dropped, a read-only `file` column -- the one only
97
+ * a load fills -- was UNLINKED by every save of any other field.
98
+ *
99
+ * The pkey2s go back for the same reason as the pkey: a secondary key
100
+ * names the INSTANCE the update is for (`yunos.yuno_release` in the
101
+ * agent), and it is `persistent, required`, not `writable`. Dropped,
102
+ * the update reached the backend with no pkey2 and C_NODE resolved it
103
+ * to the PRIMARY instance -- right by chance while the table lists
104
+ * primaries, wrong from a form opened on a row of `instances`.
105
+ ************************************************************/
106
+ function col_goes_back_to_treedb(desc, col)
107
+ {
108
+ let d = desc || {};
109
+ let pkey = d.pkey || "id";
110
+ const field_desc = treedb_get_field_desc(col);
111
+
112
+ if(field_desc.is_writable || field_desc.type === "fkey" ||
113
+ field_desc.is_file || col.id === pkey) {
114
+ return true;
115
+ }
116
+ return topic_pkey2s(d).includes(col.id);
117
+ }
118
+
119
+ export {plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb};
@@ -8,7 +8,9 @@
8
8
  * affordance back by accident.
9
9
  ***********************************************************************/
10
10
  import { describe, test, expect } from "vitest";
11
- import { plan_treedb_writes, READONLY_FORM_TOOLBAR } from "./treedb_write_plan.js";
11
+ import {
12
+ plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb
13
+ } from "./treedb_write_plan.js";
12
14
 
13
15
  const WRITES = ["edition_mode", "new_button", "delete_button", "paste_button", "in_row_icons"];
14
16
 
@@ -81,3 +83,53 @@ describe("readonly beats every with_*", () => {
81
83
  expect(plan_treedb_writes({readonly: "yes"}).new_button).toBe(false);
82
84
  });
83
85
  });
86
+
87
+
88
+ /***********************************************************************
89
+ * Which columns of a form go back in the write. The shape is the
90
+ * agent's `yunos`: a pkey, a secondary key that is required but not
91
+ * writable, and the read-only columns that never survive a widget.
92
+ ***********************************************************************/
93
+ const YUNOS_DESC = {
94
+ pkey: "id",
95
+ pkey2s: ["yuno_release"],
96
+ };
97
+ const COL = {
98
+ id: {id: "id", type: "string", flag: ["persistent", "required", "rowid"]},
99
+ yuno_release: {id: "yuno_release", type: "string", flag: ["persistent", "required"]},
100
+ yuno_name: {id: "yuno_name", type: "string", flag: ["persistent", "writable"]},
101
+ date: {id: "date", type: "integer", flag: ["persistent", "time"]},
102
+ realm_id: {id: "realm_id", type: "string", flag: ["persistent", "fkey"]},
103
+ photo: {id: "photo", type: "string", flag: ["persistent", "fkey", "file"]},
104
+ yunos: {id: "yunos", type: "object", flag: ["hook"], hook: {yunos: "realm_id"}},
105
+ };
106
+
107
+ describe("what goes back to treedb from a form", () => {
108
+ test("a writable column, an fkey and a file column go back", () => {
109
+ expect(col_goes_back_to_treedb(YUNOS_DESC, COL.yuno_name)).toBe(true);
110
+ expect(col_goes_back_to_treedb(YUNOS_DESC, COL.realm_id)).toBe(true);
111
+ expect(col_goes_back_to_treedb(YUNOS_DESC, COL.photo)).toBe(true);
112
+ });
113
+
114
+ test("a read-only time and a hook stay: they do not survive the widget", () => {
115
+ expect(col_goes_back_to_treedb(YUNOS_DESC, COL.date)).toBe(false);
116
+ expect(col_goes_back_to_treedb(YUNOS_DESC, COL.yunos)).toBe(false);
117
+ });
118
+
119
+ test("the pkey goes back although it is not writable: it addresses the record", () => {
120
+ expect(col_goes_back_to_treedb(YUNOS_DESC, COL.id)).toBe(true);
121
+ });
122
+
123
+ test("a pkey2 goes back although it is not writable: it names the INSTANCE", () => {
124
+ expect(col_goes_back_to_treedb(YUNOS_DESC, COL.yuno_release)).toBe(true);
125
+ });
126
+
127
+ test("a pkey2s declared as a bare string (the C literal) counts the same", () => {
128
+ expect(col_goes_back_to_treedb({pkey: "id", pkey2s: "yuno_release"}, COL.yuno_release)).toBe(true);
129
+ });
130
+
131
+ test("without pkey2s in the desc the same column stays read-only", () => {
132
+ expect(col_goes_back_to_treedb({pkey: "id"}, COL.yuno_release)).toBe(false);
133
+ expect(col_goes_back_to_treedb(null, COL.yuno_release)).toBe(false);
134
+ });
135
+ });