@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.
- package/README.md +20 -7
- package/dist/gobj-ui.cjs.js +248 -84
- package/dist/gobj-ui.es.js +248 -84
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +124 -36
- package/src/c_yui_treedb_schema.js +17 -7
- package/src/schema_descs.js +14 -6
- package/src/schema_descs.test.js +25 -0
- package/src/schema_import.js +17 -6
- package/src/schema_import.test.js +21 -0
- package/src/schema_model.js +26 -0
- package/src/schema_model.test.js +25 -0
- package/src/schema_to_c.js +22 -8
- package/src/schema_to_c.test.js +47 -0
- package/src/schema_write_options.js +60 -0
- package/src/schema_write_options.test.js +65 -0
- package/src/yui_icons.css +14 -0
package/src/schema_to_c.js
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
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
|
|
156
|
+
if(is_empty_value(value)) {
|
|
153
157
|
continue;
|
|
154
158
|
}
|
|
155
|
-
|
|
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
|
|
186
|
+
if(is_empty_value(value)) {
|
|
179
187
|
continue;
|
|
180
188
|
}
|
|
181
|
-
|
|
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) {
|
package/src/schema_to_c.test.js
CHANGED
|
@@ -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
|
+
}
|