@yuneta/gobj-ui 6.1.0 → 6.1.2

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.
@@ -32,6 +32,7 @@ import {
32
32
  col_enum,
33
33
  topic_pkey2s,
34
34
  as_json,
35
+ is_empty_value,
35
36
  } from "./schema_model.js";
36
37
 
37
38
 
@@ -60,9 +61,12 @@ const COL_KEY_ORDER = [
60
61
  * hand-written schema without looking foreign. */
61
62
  const VERSION_KEYS = ["schema_version", "topic_version"];
62
63
 
63
- /* Storage-only: they say where the record lives, not what it declares. */
64
- const COL_SKIP = ["id", "value", "topics", "order", "__md_treedb__"];
65
- const TOPIC_SKIP = ["id", "value", "treedbs", "cols", "order", "__md_treedb__"];
64
+ /* Storage-only: they say where the record lives, not what it declares.
65
+ * `_geometry` is here as a FIELD — where the graph editor put this
66
+ * record's box and it is not the same thing as a column NAMED
67
+ * `_geometry`, which the .c literals do declare and which stays. */
68
+ const COL_SKIP = ["id", "value", "topics", "order", "_geometry", "__md_treedb__"];
69
+ const TOPIC_SKIP = ["id", "value", "treedbs", "cols", "order", "_geometry", "__md_treedb__"];
66
70
 
67
71
  /* Fields whose stored form may be the JSON text of the value. */
68
72
  const JSON_FIELDS = ["enum", "hook", "default", "template", "properties", "pkey2s"];
@@ -149,10 +153,14 @@ function schema_to_json(treedb)
149
153
  if(TOPIC_SKIP.indexOf(key) >= 0) {
150
154
  continue;
151
155
  }
152
- if(value === null || value === undefined || value === "") {
156
+ if(is_empty_value(value)) {
153
157
  continue;
154
158
  }
155
- jn_topic[key] = JSON_FIELDS.indexOf(key) >= 0 ? as_json(value) : value;
159
+ let read = JSON_FIELDS.indexOf(key) >= 0 ? as_json(value) : value;
160
+ if(is_empty_value(read)) {
161
+ continue;
162
+ }
163
+ jn_topic[key] = read;
156
164
  }
157
165
  if(jn_topic.topic_version !== undefined) {
158
166
  jn_topic.topic_version = version(jn_topic.topic_version);
@@ -175,10 +183,14 @@ function schema_to_json(treedb)
175
183
  if(COL_SKIP.indexOf(key) >= 0) {
176
184
  continue;
177
185
  }
178
- if(value === null || value === undefined || value === "") {
186
+ if(is_empty_value(value)) {
179
187
  continue;
180
188
  }
181
- jn_col[key] = JSON_FIELDS.indexOf(key) >= 0 ? as_json(value) : value;
189
+ let read = JSON_FIELDS.indexOf(key) >= 0 ? as_json(value) : value;
190
+ if(is_empty_value(read)) {
191
+ continue;
192
+ }
193
+ jn_col[key] = read;
182
194
  }
183
195
  let flags = col_flags(col_record);
184
196
  if(flags.length > 0) {
@@ -187,8 +199,10 @@ function schema_to_json(treedb)
187
199
  delete jn_col.flag;
188
200
  }
189
201
  let hook = col_hook(col_record);
190
- if(hook) {
202
+ if(hook && !is_empty_value(hook)) {
191
203
  jn_col.hook = hook;
204
+ } else {
205
+ delete jn_col.hook;
192
206
  }
193
207
  let e = col_enum(col_record);
194
208
  if(e.length > 0) {
@@ -216,3 +216,50 @@ describe("the characters that cannot be written as themselves", () => {
216
216
  .toBe(header);
217
217
  });
218
218
  });
219
+
220
+ describe("the empty collections the store answers with", () => {
221
+ /* Found by running the editor against a real yuno: every `blob`
222
+ column that was never set came back as `{}` and was exported as
223
+ one, so a 4-topic schema printed 1229 lines of empty objects —
224
+ and `'hook': {}` in a literal is a hook the treedb builds and
225
+ nothing writes. */
226
+ const model = build_schema_model({
227
+ treedbs: [{id: "treedb_x", schema_version: 1, _geometry: {}}],
228
+ topics: [{id: "treedb_x.t", value: "t", order: 1, pkey: "id",
229
+ topic_version: 1, _geometry: {}, pkey2s: {},
230
+ treedbs: ["treedbs^treedb_x^topics"]}],
231
+ cols: [{id: "treedb_x.t.id", value: "id", order: 1, header: "Id",
232
+ type: "string", flag: ["persistent"],
233
+ enum: {}, hook: {}, default: {}, template: {}, properties: {},
234
+ _geometry: {}, pkey2s: {},
235
+ topics: ["topics^treedb_x.t^cols"]}],
236
+ });
237
+ const json = schema_to_json(model.treedbs[0]);
238
+
239
+ test("an unset blob is left out, not exported as an empty object", () => {
240
+ for(const key of ["enum", "hook", "default", "template", "properties", "pkey2s"]) {
241
+ expect(key in json.topics[0].cols.id).toBe(false);
242
+ }
243
+ });
244
+
245
+ test("the record's own _geometry is storage and does not travel", () => {
246
+ expect("_geometry" in json.topics[0]).toBe(false);
247
+ expect("_geometry" in json.topics[0].cols.id).toBe(false);
248
+ });
249
+
250
+ test("a column NAMED _geometry is a real column and stays", () => {
251
+ /* The .c literals declare one; only the FIELD is storage. */
252
+ const with_geo = build_schema_model({
253
+ treedbs: [{id: "treedb_x", schema_version: 1}],
254
+ topics: [{id: "treedb_x.t", value: "t", treedbs: ["treedbs^treedb_x^topics"]}],
255
+ cols: [{id: "treedb_x.t._geometry", value: "_geometry", header: "Geometry",
256
+ type: "blob", topics: ["topics^treedb_x.t^cols"]}],
257
+ });
258
+ expect(Object.keys(schema_to_json(with_geo.treedbs[0]).topics[0].cols))
259
+ .toEqual(["_geometry"]);
260
+ });
261
+
262
+ test("what is left still round-trips", () => {
263
+ expect(load_like_the_yuno(schema_to_c(model.treedbs[0]))).toEqual(json);
264
+ });
265
+ });
@@ -0,0 +1,60 @@
1
+ /***********************************************************************
2
+ * schema_write_options.js
3
+ *
4
+ * The OPTIONS a schema write travels with, and why each one.
5
+ *
6
+ * Three words decide whether a write does what it says, and all
7
+ * three are easy to get wrong in a way that succeeds:
8
+ *
9
+ * `create` a node that is not there yet is written by
10
+ * `update-node` with this, not by `create-node`. The
11
+ * two are not the same call: only the update path
12
+ * carries `autolink`, and without a link a new column
13
+ * belongs to no topic.
14
+ *
15
+ * `autolink` turns the fkey carried IN THE RECORD into a link.
16
+ * It rewrites the node's links from the fkey fields
17
+ * the record has — so on a PARTIAL update, which
18
+ * carries none, it reads that as "no parents" and
19
+ * UNLINKS the node. Raising a topic's version with it
20
+ * on detached that topic from its treedb, and the
21
+ * write returned success. It goes with a create,
22
+ * where the record carries its fkey, and with nothing
23
+ * else.
24
+ *
25
+ * `force` a delete of a node something points at. A column is
26
+ * pointed at by the topic that owns it, which is not
27
+ * a reason to refuse.
28
+ *
29
+ * Pure and tested, because the cost of getting it wrong is a
30
+ * store that has to be repaired by hand, and nothing in the
31
+ * answer says it happened.
32
+ *
33
+ * Copyright (c) 2026, ArtGins.
34
+ * All Rights Reserved.
35
+ ***********************************************************************/
36
+
37
+ /***************************************************************
38
+ * write_command(op) -> the treedb command that performs it
39
+ ***************************************************************/
40
+ function write_command(op)
41
+ {
42
+ return (op === "delete") ? "delete-node" : "update-node";
43
+ }
44
+
45
+ /***************************************************************
46
+ * write_options(op) -> the options it travels with
47
+ ***************************************************************/
48
+ function write_options(op)
49
+ {
50
+ if(op === "delete") {
51
+ return {force: true};
52
+ }
53
+ if(op === "create") {
54
+ return {list_dict: true, create: true, autolink: true};
55
+ }
56
+ return {list_dict: true};
57
+ }
58
+
59
+
60
+ export {write_command, write_options};
@@ -0,0 +1,65 @@
1
+ /***********************************************************************
2
+ * schema_write_options.test.js
3
+ *
4
+ * The three words that decide whether a schema write does what it
5
+ * says, pinned.
6
+ *
7
+ * This file exists because of one afternoon: `autolink` on a
8
+ * partial update detached a topic from its treedb — the version
9
+ * was raised, the write answered success, and the topic vanished
10
+ * from the schema. Nothing in the answer said so.
11
+ ***********************************************************************/
12
+ import { describe, test, expect } from "vitest";
13
+ import { write_command, write_options } from "./schema_write_options.js";
14
+
15
+
16
+ describe("which command performs which write", () => {
17
+ test("a create goes through update-node, NOT create-node", () => {
18
+ /* Only the update path carries `autolink`, and without a link a
19
+ new column belongs to no topic. */
20
+ expect(write_command("create")).toBe("update-node");
21
+ });
22
+
23
+ test("an update goes through update-node", () => {
24
+ expect(write_command("update")).toBe("update-node");
25
+ });
26
+
27
+ test("a delete goes through delete-node", () => {
28
+ expect(write_command("delete")).toBe("delete-node");
29
+ });
30
+ });
31
+
32
+ describe("autolink goes with a create and with nothing else", () => {
33
+ test("a create asks for it — its record carries the fkey", () => {
34
+ const o = write_options("create");
35
+ expect(o.autolink).toBe(true);
36
+ expect(o.create).toBe(true);
37
+ });
38
+
39
+ test("an update NEVER asks for it", () => {
40
+ /* The editor writes only what changed, so an update carries no
41
+ fkey — and autolink reads an absent fkey as "no parents". */
42
+ expect(write_options("update").autolink).toBeUndefined();
43
+ expect(write_options("update").create).toBeUndefined();
44
+ });
45
+
46
+ test("a delete never asks for it either", () => {
47
+ expect(write_options("delete").autolink).toBeUndefined();
48
+ });
49
+ });
50
+
51
+ describe("the rest", () => {
52
+ test("a delete forces: a column is pointed at by its own topic", () => {
53
+ expect(write_options("delete")).toEqual({force: true});
54
+ });
55
+
56
+ test("reads come back as dicts, so a write answers in the same shape", () => {
57
+ expect(write_options("create").list_dict).toBe(true);
58
+ expect(write_options("update").list_dict).toBe(true);
59
+ });
60
+
61
+ test("an unknown op is treated as an update — the safe one", () => {
62
+ expect(write_options("")).toEqual({list_dict: true});
63
+ expect(write_options(undefined).autolink).toBeUndefined();
64
+ });
65
+ });
package/src/yui_icons.css CHANGED
@@ -349,3 +349,17 @@
349
349
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M192 0C192 0 48 176 48 304a144 144 0 1 0 288 0C336 176 192 0 192 0z'/%3E%3C/svg%3E");
350
350
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M192 0C192 0 48 176 48 304a144 144 0 1 0 288 0C336 176 192 0 192 0z'/%3E%3C/svg%3E");
351
351
  }
352
+
353
+ /* The drag handle of a reorderable row, and the icon of a treedb: both
354
+ * are wanted by the schema editor and neither was in the set. A missing
355
+ * glyph renders as a solid black square, so a mask is added rather than
356
+ * a class referenced on hope. */
357
+ .yi-grip-vertical::before {
358
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath d='M40 352l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zm192 0l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 192l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zm192 0l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM232 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40z'/%3E%3C/svg%3E");
359
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath d='M40 352l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zm192 0l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 192l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zm192 0l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM232 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40z'/%3E%3C/svg%3E");
360
+ }
361
+
362
+ .yi-database::before {
363
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M448 80l0 48c0 44.2-100.3 80-224 80S0 172.2 0 128L0 80C0 35.8 100.3 0 224 0S448 35.8 448 80zM393.2 214.7c20.8-7.4 39.9-16.9 54.8-28.6L448 288c0 44.2-100.3 80-224 80S0 332.2 0 288L0 186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3zM0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6l0 85.9c0 44.2-100.3 80-224 80S0 476.2 0 432l0-85.9z'/%3E%3C/svg%3E");
364
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M448 80l0 48c0 44.2-100.3 80-224 80S0 172.2 0 128L0 80C0 35.8 100.3 0 224 0S448 35.8 448 80zM393.2 214.7c20.8-7.4 39.9-16.9 54.8-28.6L448 288c0 44.2-100.3 80-224 80S0 332.2 0 288L0 186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3zM0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6l0 85.9c0 44.2-100.3 80-224 80S0 476.2 0 432l0-85.9z'/%3E%3C/svg%3E");
365
+ }