@yuneta/gobj-ui 6.0.0 → 6.1.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 +95 -2
- package/dist/gobj-ui.cjs.js +5374 -476
- package/dist/gobj-ui.es.js +5374 -477
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/c_yui_schema_editor.css +133 -0
- package/src/c_yui_schema_editor.js +3217 -0
- package/src/c_yui_treedb_schema.js +22 -3
- package/src/schema_descs.js +164 -0
- package/src/schema_descs.test.js +145 -0
- package/src/schema_flags.js +237 -0
- package/src/schema_flags.test.js +133 -0
- package/src/schema_import.js +413 -0
- package/src/schema_import.test.js +234 -0
- package/src/schema_model.js +512 -0
- package/src/schema_model.test.js +233 -0
- package/src/schema_to_c.js +367 -0
- package/src/schema_to_c.test.js +218 -0
- package/src/schema_validate.js +289 -0
- package/src/schema_validate.test.js +265 -0
|
@@ -0,0 +1,3217 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* c_yui_schema_editor.js
|
|
3
|
+
*
|
|
4
|
+
* C_YUI_SCHEMA_EDITOR — a treedb's schema edited AS A SCHEMA.
|
|
5
|
+
*
|
|
6
|
+
* WHAT IT IS FOR. Every schema a yuno holds lives in its
|
|
7
|
+
* `treedb_system_schema`, stored as data in three flat topics
|
|
8
|
+
* linked by fkeys: `treedbs` -> `topics` -> `cols`. Opened with
|
|
9
|
+
* the ordinary topic editor — which is how it was edited until
|
|
10
|
+
* now — adding one column to one topic means finding it in a
|
|
11
|
+
* table holding every column of every topic of every treedb the
|
|
12
|
+
* yuno has, composing the parent fkey by hand, and remembering to
|
|
13
|
+
* raise a `topic_version` that nothing asks about. The storage is
|
|
14
|
+
* right; it is not a screen.
|
|
15
|
+
*
|
|
16
|
+
* So this view puts the schema back together and edits THAT:
|
|
17
|
+
* treedb -> topics -> columns, each in its declared `order`,
|
|
18
|
+
* with the storage composed underneath — the qualified id, the
|
|
19
|
+
* fkey, the order of a new sibling, the version that publishes
|
|
20
|
+
* the change.
|
|
21
|
+
*
|
|
22
|
+
* THE VERSION IS THE POINT, not a detail. A topic whose columns
|
|
23
|
+
* changed and whose `topic_version` did not is a topic whose
|
|
24
|
+
* persisted `topic_cols.json` masks the whole edit: the restart
|
|
25
|
+
* succeeds and nothing moved. Nothing in the store says so, and
|
|
26
|
+
* the log that would is on the node. So every write here raises
|
|
27
|
+
* the version of the topic it touched, and the toolbar says which
|
|
28
|
+
* topics are waiting for a restart.
|
|
29
|
+
*
|
|
30
|
+
* WHAT ELSE IS HERE, and why each is here and not somewhere else:
|
|
31
|
+
*
|
|
32
|
+
* - the flags of a column as checkboxes with what they do
|
|
33
|
+
* (schema_flags.js): `flag` is the field that decides the most
|
|
34
|
+
* and explains the least.
|
|
35
|
+
* - the schema DRAWN (schema_descs.js + C_YUI_TREEDB_SCHEMA).
|
|
36
|
+
* The diagram of a treedb opened here is otherwise the META
|
|
37
|
+
* schema — treedbs/topics/cols, the same three cards on every
|
|
38
|
+
* yuno — never the schema the operator came to look at.
|
|
39
|
+
* - what the treedb would refuse, before the restart that finds
|
|
40
|
+
* out (schema_validate.js). Applying a schema is restarting the
|
|
41
|
+
* yuno that owns it.
|
|
42
|
+
* - the schema as its C literal (schema_to_c.js), because an
|
|
43
|
+
* edit made here works and lives nowhere the next build knows
|
|
44
|
+
* about; and the way back in, as a reviewed plan
|
|
45
|
+
* (schema_import.js).
|
|
46
|
+
*
|
|
47
|
+
* STATES, because each is a different screen and a different set
|
|
48
|
+
* of legal actions — an action arriving in the wrong one fails
|
|
49
|
+
* loudly and names its sender, which is the whole point of
|
|
50
|
+
* routing a click through the machine:
|
|
51
|
+
* ST_IDLE no transport yet, or hidden
|
|
52
|
+
* ST_LOADING the three `nodes` requests in flight
|
|
53
|
+
* ST_EMPTY the yuno's system schema holds no treedb
|
|
54
|
+
* ST_TREEDBS the treedbs of this yuno
|
|
55
|
+
* ST_TOPICS the topics of one treedb
|
|
56
|
+
* ST_DIAGRAM that treedb drawn
|
|
57
|
+
* ST_COLUMNS the columns of one topic
|
|
58
|
+
* ST_SAVING a write, or a plan of them, in flight
|
|
59
|
+
*
|
|
60
|
+
* THE URL CARRIES THE POSITION (ROUTING.md): the tail this view
|
|
61
|
+
* owns is `<treedb>[/<topic>]` or `<treedb>/diagram`. It is
|
|
62
|
+
* applied from the host's EV_SHOW and mirrored back with
|
|
63
|
+
* EV_POSITION_CHANGED; this gclass navigates nothing itself.
|
|
64
|
+
*
|
|
65
|
+
* TRANSPORT: the `gobj_remote_yuno` every backend-talking view of
|
|
66
|
+
* this library declares. It answers `nodes` / `create-node` /
|
|
67
|
+
* `update-node` / `delete-node` on the treedb service, and it may
|
|
68
|
+
* be a routing adapter two hops away (the agent console's
|
|
69
|
+
* C_AGENT_TREEDB_LINK) — this view cannot tell, and must not.
|
|
70
|
+
*
|
|
71
|
+
* Copyright (c) 2026, ArtGins.
|
|
72
|
+
* All Rights Reserved.
|
|
73
|
+
***********************************************************************/
|
|
74
|
+
import {
|
|
75
|
+
SDATA,
|
|
76
|
+
SDATA_END,
|
|
77
|
+
data_type_t,
|
|
78
|
+
event_flag_t,
|
|
79
|
+
gclass_create,
|
|
80
|
+
gclass_find_by_name,
|
|
81
|
+
log_error,
|
|
82
|
+
log_warning,
|
|
83
|
+
gobj_read_attr,
|
|
84
|
+
gobj_read_str_attr,
|
|
85
|
+
gobj_read_bool_attr,
|
|
86
|
+
gobj_read_pointer_attr,
|
|
87
|
+
gobj_write_attr,
|
|
88
|
+
gobj_name,
|
|
89
|
+
gobj_short_name,
|
|
90
|
+
gobj_parent,
|
|
91
|
+
gobj_command,
|
|
92
|
+
gobj_create_pure_child,
|
|
93
|
+
gobj_start,
|
|
94
|
+
gobj_stop,
|
|
95
|
+
gobj_destroy,
|
|
96
|
+
gobj_is_destroying,
|
|
97
|
+
gobj_send_event,
|
|
98
|
+
gobj_publish_event,
|
|
99
|
+
gobj_subscribe_event,
|
|
100
|
+
gobj_unsubscribe_event,
|
|
101
|
+
gobj_current_state,
|
|
102
|
+
gobj_change_state,
|
|
103
|
+
createElement2,
|
|
104
|
+
refresh_language,
|
|
105
|
+
clean_name,
|
|
106
|
+
msg_iev_get_stack,
|
|
107
|
+
kw_get_str,
|
|
108
|
+
kw_get_dict,
|
|
109
|
+
is_object,
|
|
110
|
+
empty_string,
|
|
111
|
+
} from "@yuneta/gobj-js";
|
|
112
|
+
|
|
113
|
+
import "./c_yui_schema_editor.css";
|
|
114
|
+
|
|
115
|
+
import {
|
|
116
|
+
build_schema_model,
|
|
117
|
+
find_treedb,
|
|
118
|
+
find_topic,
|
|
119
|
+
find_col,
|
|
120
|
+
col_flags,
|
|
121
|
+
col_hook,
|
|
122
|
+
col_enum,
|
|
123
|
+
fkey_ref,
|
|
124
|
+
next_order,
|
|
125
|
+
moved_orders,
|
|
126
|
+
} from "./schema_model.js";
|
|
127
|
+
import {validate_schema, validate_model, has_errors} from "./schema_validate.js";
|
|
128
|
+
import {topic_descs} from "./schema_descs.js";
|
|
129
|
+
import {schema_to_json, schema_to_c} from "./schema_to_c.js";
|
|
130
|
+
import {plan_import, plan_deletes} from "./schema_import.js";
|
|
131
|
+
import {grouped_flags, toggle_flag} from "./schema_flags.js";
|
|
132
|
+
import {COL_TYPES} from "./schema_validate.js";
|
|
133
|
+
|
|
134
|
+
import {yui_shell_of} from "./c_yui_shell.js";
|
|
135
|
+
import {
|
|
136
|
+
yui_shell_show_modal,
|
|
137
|
+
yui_shell_show_error,
|
|
138
|
+
yui_shell_show_info,
|
|
139
|
+
yui_shell_confirm_danger,
|
|
140
|
+
} from "./shell_modals.js";
|
|
141
|
+
|
|
142
|
+
import {t} from "i18next";
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/***************************************************************
|
|
146
|
+
* Constants
|
|
147
|
+
***************************************************************/
|
|
148
|
+
const GCLASS_NAME = "C_YUI_SCHEMA_EDITOR";
|
|
149
|
+
|
|
150
|
+
/* The three topics a schema is stored in. Named once: everything
|
|
151
|
+
* below addresses them by these constants and nothing composes the
|
|
152
|
+
* strings again. */
|
|
153
|
+
const T_TREEDBS = "treedbs";
|
|
154
|
+
const T_TOPICS = "topics";
|
|
155
|
+
const T_COLS = "cols";
|
|
156
|
+
|
|
157
|
+
/* The hook each level is reached through, which is what a new child's
|
|
158
|
+
* fkey has to name. Getting it wrong links the record to nothing
|
|
159
|
+
* while the write still succeeds. */
|
|
160
|
+
const HOOK_TOPICS = "topics";
|
|
161
|
+
const HOOK_COLS = "cols";
|
|
162
|
+
|
|
163
|
+
/* The tail segment that opens the drawing instead of a topic. Reserved:
|
|
164
|
+
* a topic with this name is not reachable by its own name, the same
|
|
165
|
+
* bargain the treedb views already make with `schema` and `graph`. */
|
|
166
|
+
const DIAGRAM_SEG = "diagram";
|
|
167
|
+
|
|
168
|
+
/* Marker of OUR requests: the transport may be shared with other
|
|
169
|
+
* panels (the agent console publishes every answer to every one of
|
|
170
|
+
* them), and each filters on its own purpose. */
|
|
171
|
+
const PURPOSE = "schema_editor";
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
/***************************************************************
|
|
175
|
+
* Attrs
|
|
176
|
+
***************************************************************/
|
|
177
|
+
const attrs_table = [
|
|
178
|
+
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
179
|
+
SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Transport: the treedb's service, or an adapter that reaches it"),
|
|
180
|
+
SDATA(data_type_t.DTP_STRING, "treedb_name", 0, "", "Service name of the system-schema treedb (the one holding the schemas as data)"),
|
|
181
|
+
SDATA(data_type_t.DTP_BOOLEAN, "readonly", 0, false, "This yuno is not the MASTER of the treedb's tranger, so it refuses every write: mount without the write affordances instead of turning each click into an error"),
|
|
182
|
+
SDATA(data_type_t.DTP_STRING, "base_route", 0, "", "This view's route: the position under it is <treedb>[/<topic>] (host-supplied; ROUTING.md)"),
|
|
183
|
+
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement"),
|
|
184
|
+
SDATA_END()
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
let PRIVATE_DATA = {
|
|
188
|
+
/* What the store answered, and what it means */
|
|
189
|
+
records: null, /* {treedbs, topics, cols} as they arrived */
|
|
190
|
+
model: null, /* build_schema_model() of the above */
|
|
191
|
+
pending: 0, /* `nodes` requests still in flight */
|
|
192
|
+
load_error: "",
|
|
193
|
+
|
|
194
|
+
/* Where the operator is. The url carries it; these mirror it. */
|
|
195
|
+
treedb_id: "",
|
|
196
|
+
topic_name: "",
|
|
197
|
+
diagram: false,
|
|
198
|
+
|
|
199
|
+
/* What this session changed, which is what decides whether a
|
|
200
|
+
* version still has to move. */
|
|
201
|
+
baseline: null, /* {topic id: topic_version} at load time */
|
|
202
|
+
written: null, /* {topic id: true} */
|
|
203
|
+
|
|
204
|
+
/* Children and overlays */
|
|
205
|
+
diagram_gobj: null,
|
|
206
|
+
dialog: null,
|
|
207
|
+
|
|
208
|
+
/* A write, or a plan of them, in flight */
|
|
209
|
+
save_queue: null,
|
|
210
|
+
save_return: "",
|
|
211
|
+
save_wrote: false,
|
|
212
|
+
connected: false,
|
|
213
|
+
|
|
214
|
+
/* A position that arrived while a write was in flight */
|
|
215
|
+
pending_seg: null,
|
|
216
|
+
|
|
217
|
+
/* The import dialog's plan and the two nodes it draws into */
|
|
218
|
+
import_plan: null,
|
|
219
|
+
import_pane: null,
|
|
220
|
+
import_run: null,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
let __gclass__ = null;
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
/******************************
|
|
229
|
+
* Framework Methods
|
|
230
|
+
******************************/
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
/***************************************************************
|
|
236
|
+
* Framework Method: Create
|
|
237
|
+
***************************************************************/
|
|
238
|
+
function mt_create(gobj)
|
|
239
|
+
{
|
|
240
|
+
let priv = gobj.priv;
|
|
241
|
+
|
|
242
|
+
priv.baseline = {};
|
|
243
|
+
priv.written = {};
|
|
244
|
+
priv.save_queue = [];
|
|
245
|
+
|
|
246
|
+
build_ui(gobj);
|
|
247
|
+
|
|
248
|
+
/*
|
|
249
|
+
* CHILD subscription model
|
|
250
|
+
*/
|
|
251
|
+
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
252
|
+
if(!subscriber) {
|
|
253
|
+
subscriber = gobj_parent(gobj);
|
|
254
|
+
}
|
|
255
|
+
gobj_subscribe_event(gobj, null, {}, subscriber);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/***************************************************************
|
|
259
|
+
* Framework Method: Start
|
|
260
|
+
***************************************************************/
|
|
261
|
+
function mt_start(gobj)
|
|
262
|
+
{
|
|
263
|
+
let shell = yui_shell_of(gobj);
|
|
264
|
+
|
|
265
|
+
/* What a WIDGET draws is reachable by no i18n attribute, and this
|
|
266
|
+
* view draws all of its rows itself: it re-renders on the shell's
|
|
267
|
+
* language event rather than listening to i18next directly. */
|
|
268
|
+
if(shell) {
|
|
269
|
+
gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
|
|
270
|
+
}
|
|
271
|
+
request_model(gobj);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/***************************************************************
|
|
275
|
+
* Framework Method: Stop
|
|
276
|
+
***************************************************************/
|
|
277
|
+
function mt_stop(gobj)
|
|
278
|
+
{
|
|
279
|
+
let shell = yui_shell_of(gobj);
|
|
280
|
+
|
|
281
|
+
if(shell) {
|
|
282
|
+
gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
|
|
283
|
+
}
|
|
284
|
+
close_dialog(gobj);
|
|
285
|
+
destroy_diagram(gobj);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/***************************************************************
|
|
289
|
+
* Framework Method: Destroy
|
|
290
|
+
***************************************************************/
|
|
291
|
+
function mt_destroy(gobj)
|
|
292
|
+
{
|
|
293
|
+
destroy_ui(gobj);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
/***************************
|
|
300
|
+
* Local Methods
|
|
301
|
+
***************************/
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
/***************************************************************
|
|
307
|
+
* The frame: a toolbar that never changes place, a notice line
|
|
308
|
+
* for what the whole view has to say, and one body the screens
|
|
309
|
+
* take turns in. The screens are re-rendered, the frame is not.
|
|
310
|
+
***************************************************************/
|
|
311
|
+
function build_ui(gobj)
|
|
312
|
+
{
|
|
313
|
+
let $container = createElement2(
|
|
314
|
+
["div", {class: `${GCLASS_NAME} SCHEMA_CARD view-card`,
|
|
315
|
+
style: "height:100%; display:flex; flex-direction:column; min-height:0;"}, [
|
|
316
|
+
["div", {class: "SCHEMA_TOOLBAR is-flex is-align-items-center",
|
|
317
|
+
style: "gap:.25rem; padding:.25rem;"}, []],
|
|
318
|
+
["div", {class: "SCHEMA_NOTICE p-4 has-text-grey is-hidden"}, []],
|
|
319
|
+
["div", {class: "SCHEMA_BODY", style: "flex:1 1 auto; min-height:0; overflow:auto;"}, []]
|
|
320
|
+
]]
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
gobj_write_attr(gobj, "$container", $container);
|
|
324
|
+
refresh_language($container, t);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/***************************************************************
|
|
328
|
+
* Destroy UI
|
|
329
|
+
***************************************************************/
|
|
330
|
+
function destroy_ui(gobj)
|
|
331
|
+
{
|
|
332
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
333
|
+
|
|
334
|
+
if($container) {
|
|
335
|
+
if($container.parentNode) {
|
|
336
|
+
$container.parentNode.removeChild($container);
|
|
337
|
+
}
|
|
338
|
+
gobj_write_attr(gobj, "$container", null);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/***************************************************************
|
|
343
|
+
* Attributes, minus the ones that are not there.
|
|
344
|
+
*
|
|
345
|
+
* createElement2() sets every key it is given with
|
|
346
|
+
* setAttribute(), so a key whose value is `undefined` renders
|
|
347
|
+
* the STRING "undefined" — and `disabled="undefined"` is a
|
|
348
|
+
* disabled button, `checked="undefined"` a checked box. Every
|
|
349
|
+
* conditional attribute in this file goes through here.
|
|
350
|
+
***************************************************************/
|
|
351
|
+
function el_attrs(attrs)
|
|
352
|
+
{
|
|
353
|
+
for(let key of Object.keys(attrs)) {
|
|
354
|
+
if(attrs[key] === undefined) {
|
|
355
|
+
delete attrs[key];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return attrs;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/***************************************************************
|
|
362
|
+
* The body stops answering while a write is in flight.
|
|
363
|
+
***************************************************************/
|
|
364
|
+
function set_busy(gobj, busy)
|
|
365
|
+
{
|
|
366
|
+
let $body = $of(gobj, ".SCHEMA_BODY");
|
|
367
|
+
|
|
368
|
+
if($body) {
|
|
369
|
+
$body.classList.toggle("SCHEMA_BUSY", !!busy);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/***************************************************************
|
|
374
|
+
* Shorthands for the three panes of the frame.
|
|
375
|
+
***************************************************************/
|
|
376
|
+
function $of(gobj, selector)
|
|
377
|
+
{
|
|
378
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
379
|
+
|
|
380
|
+
return $container ? $container.querySelector(selector) : null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function clear($el)
|
|
384
|
+
{
|
|
385
|
+
if(!$el) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
while($el.firstChild) {
|
|
389
|
+
$el.removeChild($el.firstChild);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/***************************************************************
|
|
394
|
+
* Is the store writable from here? A read-only mount is a
|
|
395
|
+
* property of the STORE (this yuno does not master the tranger),
|
|
396
|
+
* and it wins over every button.
|
|
397
|
+
***************************************************************/
|
|
398
|
+
function is_readonly(gobj)
|
|
399
|
+
{
|
|
400
|
+
return gobj_read_bool_attr(gobj, "readonly");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/***************************************************************
|
|
404
|
+
* A write this view refuses to attempt. Called from the actions
|
|
405
|
+
* themselves and not from the code that draws the buttons: a
|
|
406
|
+
* hidden button is not a refused action, and an event arriving
|
|
407
|
+
* anyway — from a keyboard, from a stale screen — must be
|
|
408
|
+
* answered.
|
|
409
|
+
***************************************************************/
|
|
410
|
+
function refuse_if_readonly(gobj, event)
|
|
411
|
+
{
|
|
412
|
+
if(!is_readonly(gobj)) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
log_warning(`${gobj_short_name(gobj)}: ${event} refused, the treedb is read-only`);
|
|
416
|
+
yui_shell_show_error(yui_shell_of(gobj), "the treedb is read-only", {t: t});
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
/***************************
|
|
424
|
+
* The model
|
|
425
|
+
***************************/
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
/***************************************************************
|
|
431
|
+
* One request per meta-topic. Three round trips and the whole
|
|
432
|
+
* schema set of the yuno is here: everything below — the
|
|
433
|
+
* drawing, the validation, the export — is computed from these
|
|
434
|
+
* and asks the backend nothing more.
|
|
435
|
+
***************************************************************/
|
|
436
|
+
function request_model(gobj)
|
|
437
|
+
{
|
|
438
|
+
let priv = gobj.priv;
|
|
439
|
+
let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
440
|
+
|
|
441
|
+
if(!remote) {
|
|
442
|
+
return -1; /* not mounted on a transport yet: mt_start retries via EV_TRANSPORT_STATE */
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
priv.records = {treedbs: [], topics: [], cols: []};
|
|
446
|
+
priv.load_error = "";
|
|
447
|
+
priv.pending = 0;
|
|
448
|
+
gobj_change_state(gobj, "ST_LOADING");
|
|
449
|
+
show_notice(gobj, "loading the schemas");
|
|
450
|
+
|
|
451
|
+
for(let topic_name of [T_TREEDBS, T_TOPICS, T_COLS]) {
|
|
452
|
+
if(remote_command(gobj, "nodes", {
|
|
453
|
+
topic_name: topic_name,
|
|
454
|
+
options: {list_dict: true}
|
|
455
|
+
}) === 0) {
|
|
456
|
+
priv.pending++;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if(priv.pending === 0) {
|
|
460
|
+
priv.load_error = t("cannot reach the treedb");
|
|
461
|
+
gobj_change_state(gobj, "ST_IDLE");
|
|
462
|
+
show_notice(gobj, "cannot reach the treedb");
|
|
463
|
+
return -1;
|
|
464
|
+
}
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/***************************************************************
|
|
469
|
+
* Every request this view makes goes through here, so the
|
|
470
|
+
* service name, the purpose marker and the echo of what was
|
|
471
|
+
* asked are written ONCE.
|
|
472
|
+
*
|
|
473
|
+
* `__md_command__` travels out and comes back: it is how an
|
|
474
|
+
* answer says which topic it is about, since the command is
|
|
475
|
+
* `nodes` for all three.
|
|
476
|
+
***************************************************************/
|
|
477
|
+
function remote_command(gobj, command, kw)
|
|
478
|
+
{
|
|
479
|
+
let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
480
|
+
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
481
|
+
|
|
482
|
+
if(!remote) {
|
|
483
|
+
log_error(`${gobj_short_name(gobj)}: no transport for '${command}'`);
|
|
484
|
+
return -1;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
let full_kw = Object.assign({
|
|
488
|
+
service: treedb_name,
|
|
489
|
+
treedb_name: treedb_name
|
|
490
|
+
}, kw || {});
|
|
491
|
+
full_kw.__md_command__ = Object.assign({
|
|
492
|
+
purpose: PURPOSE,
|
|
493
|
+
command: command
|
|
494
|
+
}, kw && kw.topic_name ? {topic_name: kw.topic_name} : {});
|
|
495
|
+
|
|
496
|
+
let ret = gobj_command(remote, command, full_kw, gobj);
|
|
497
|
+
if(ret) {
|
|
498
|
+
log_error(ret);
|
|
499
|
+
return -1;
|
|
500
|
+
}
|
|
501
|
+
return 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/***************************************************************
|
|
505
|
+
* The three answers are in: build the schema and remember the
|
|
506
|
+
* versions it arrived with. The baseline is what "this topic
|
|
507
|
+
* still needs its version raised" is measured against, so it is
|
|
508
|
+
* taken here and never touched again until the next load.
|
|
509
|
+
***************************************************************/
|
|
510
|
+
function build_model(gobj)
|
|
511
|
+
{
|
|
512
|
+
let priv = gobj.priv;
|
|
513
|
+
|
|
514
|
+
priv.model = build_schema_model(priv.records);
|
|
515
|
+
priv.baseline = {};
|
|
516
|
+
for(let treedb of priv.model.treedbs) {
|
|
517
|
+
for(let topic of treedb.topics) {
|
|
518
|
+
priv.baseline[topic.id] = topic.topic_version;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/***************************************************************
|
|
524
|
+
* Say what the whole store would be refused for.
|
|
525
|
+
*
|
|
526
|
+
* Not for this view — it has a button for that — but for
|
|
527
|
+
* WHOEVER APPLIES. Applying a schema is restarting the yuno that
|
|
528
|
+
* owns it, and the restart is driven from a tab above this one
|
|
529
|
+
* that has no way to know what was edited. So the count travels
|
|
530
|
+
* up on every settle, and the confirmation up there can say what
|
|
531
|
+
* it is about to restart onto.
|
|
532
|
+
***************************************************************/
|
|
533
|
+
function publish_check(gobj)
|
|
534
|
+
{
|
|
535
|
+
let priv = gobj.priv;
|
|
536
|
+
|
|
537
|
+
if(!priv.model) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
let findings = validate_model(priv.model, {
|
|
541
|
+
written_topics: Object.keys(priv.written),
|
|
542
|
+
baseline: priv.baseline
|
|
543
|
+
});
|
|
544
|
+
gobj_publish_event(gobj, "EV_SCHEMA_CHECKED", {
|
|
545
|
+
treedb_name: gobj_read_str_attr(gobj, "treedb_name"),
|
|
546
|
+
errors: findings.filter(f => f.severity === "error").length,
|
|
547
|
+
warnings: findings.filter(f => f.severity === "warning").length,
|
|
548
|
+
/* Plain JSON, and capped: the kw of an event is dumped by the
|
|
549
|
+
* machine trace, and a schema with two hundred findings would
|
|
550
|
+
* put all of them in it. */
|
|
551
|
+
first: findings.slice(0, 5).map((f) => {
|
|
552
|
+
return {severity: f.severity, code: f.code, topic: f.topic,
|
|
553
|
+
col: f.col, detail: f.detail};
|
|
554
|
+
})
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/***************************************************************
|
|
559
|
+
* What the store has to be asked again for. A write answers
|
|
560
|
+
* with the record it wrote, so the model is patched in place
|
|
561
|
+
* rather than reloaded: reloading three topics after every
|
|
562
|
+
* keystroke of an edit is three round trips per column.
|
|
563
|
+
***************************************************************/
|
|
564
|
+
function patch_record(gobj, topic_name, record)
|
|
565
|
+
{
|
|
566
|
+
let priv = gobj.priv;
|
|
567
|
+
|
|
568
|
+
if(!priv.records || !record || typeof record.id !== "string") {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
let list = priv.records[topic_name];
|
|
572
|
+
if(!Array.isArray(list)) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
let at = -1;
|
|
576
|
+
for(let i = 0; i < list.length; i++) {
|
|
577
|
+
if(list[i] && list[i].id === record.id) {
|
|
578
|
+
at = i;
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if(at >= 0) {
|
|
583
|
+
list[at] = Object.assign({}, list[at], record);
|
|
584
|
+
} else {
|
|
585
|
+
list.push(record);
|
|
586
|
+
}
|
|
587
|
+
build_model(gobj);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function forget_record(gobj, topic_name, id)
|
|
591
|
+
{
|
|
592
|
+
let priv = gobj.priv;
|
|
593
|
+
|
|
594
|
+
if(!priv.records || !Array.isArray(priv.records[topic_name])) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
priv.records[topic_name] = priv.records[topic_name].filter((r) => {
|
|
598
|
+
return !r || r.id !== id;
|
|
599
|
+
});
|
|
600
|
+
build_model(gobj);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/***************************************************************
|
|
604
|
+
* Where the operator is, resolved against the model that is
|
|
605
|
+
* actually loaded. A deep link to a treedb or a topic that is
|
|
606
|
+
* no longer there lands one level up instead of on an empty
|
|
607
|
+
* screen.
|
|
608
|
+
***************************************************************/
|
|
609
|
+
function current_treedb(gobj)
|
|
610
|
+
{
|
|
611
|
+
let priv = gobj.priv;
|
|
612
|
+
|
|
613
|
+
return priv.model ? find_treedb(priv.model, priv.treedb_id) : null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function current_topic(gobj)
|
|
617
|
+
{
|
|
618
|
+
let priv = gobj.priv;
|
|
619
|
+
|
|
620
|
+
return priv.model ? find_topic(priv.model, priv.treedb_id, priv.topic_name) : null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
/***************************
|
|
627
|
+
* The screens
|
|
628
|
+
***************************/
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
/***************************************************************
|
|
634
|
+
* One line for what the whole view has to say: loading, empty,
|
|
635
|
+
* or the reason nothing loaded. Carries its i18n key, so it
|
|
636
|
+
* follows a language change like any other text.
|
|
637
|
+
***************************************************************/
|
|
638
|
+
function show_notice(gobj, key, detail)
|
|
639
|
+
{
|
|
640
|
+
let $notice = $of(gobj, ".SCHEMA_NOTICE");
|
|
641
|
+
|
|
642
|
+
if(!$notice) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
clear($notice);
|
|
646
|
+
if(!key) {
|
|
647
|
+
$notice.classList.add("is-hidden");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
$notice.classList.remove("is-hidden");
|
|
651
|
+
$notice.appendChild(createElement2(
|
|
652
|
+
["span", {class: "SCHEMA_NOTICE_TEXT", i18n: key}, t(key)]
|
|
653
|
+
));
|
|
654
|
+
if(detail) {
|
|
655
|
+
$notice.appendChild(createElement2(
|
|
656
|
+
["span", {class: "SCHEMA_NOTICE_DETAIL has-text-grey ml-2"}, `${detail}`]
|
|
657
|
+
));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/***************************************************************
|
|
662
|
+
* A button of the toolbar. Every one carries an icon and a
|
|
663
|
+
* label; the label hides on a phone because this toolbar holds
|
|
664
|
+
* more than two buttons at its widest.
|
|
665
|
+
***************************************************************/
|
|
666
|
+
function toolbar_button(logical, icon, key, event, disabled)
|
|
667
|
+
{
|
|
668
|
+
return ["button", el_attrs({class: `${logical} button is-small`,
|
|
669
|
+
type: "button",
|
|
670
|
+
disabled: disabled ? "disabled" : undefined,
|
|
671
|
+
title: t(key), "aria-label": t(key),
|
|
672
|
+
"data-i18n-title": key, "data-i18n-aria-label": key}), [
|
|
673
|
+
["span", {class: "icon"}, [["i", {class: icon}]]],
|
|
674
|
+
["span", {class: "is-hidden-mobile", i18n: key}, t(key)]
|
|
675
|
+
], {
|
|
676
|
+
click: (evt) => {
|
|
677
|
+
evt.stopPropagation();
|
|
678
|
+
gobj_send_event(evt.currentTarget.__gobj__, event, {}, evt.currentTarget.__gobj__);
|
|
679
|
+
}
|
|
680
|
+
}];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/***************************************************************
|
|
684
|
+
* The toolbar: where you are on the left, what you can do on
|
|
685
|
+
* the right. Rebuilt with the screen, because both halves
|
|
686
|
+
* depend on it.
|
|
687
|
+
***************************************************************/
|
|
688
|
+
function render_toolbar(gobj)
|
|
689
|
+
{
|
|
690
|
+
let priv = gobj.priv;
|
|
691
|
+
let $toolbar = $of(gobj, ".SCHEMA_TOOLBAR");
|
|
692
|
+
|
|
693
|
+
if(!$toolbar) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
clear($toolbar);
|
|
697
|
+
|
|
698
|
+
let state = gobj_current_state(gobj);
|
|
699
|
+
let readonly = is_readonly(gobj);
|
|
700
|
+
let has_model = !!priv.model;
|
|
701
|
+
|
|
702
|
+
/*----------------------------------------------*
|
|
703
|
+
* Left: back, then the position as a trail.
|
|
704
|
+
*----------------------------------------------*/
|
|
705
|
+
let $left = [];
|
|
706
|
+
if(priv.treedb_id) {
|
|
707
|
+
$left.push(["button", {class: "SCHEMA_BACK button is-small",
|
|
708
|
+
type: "button",
|
|
709
|
+
title: t("back"), "aria-label": t("back"),
|
|
710
|
+
"data-i18n-title": "back", "data-i18n-aria-label": "back"}, [
|
|
711
|
+
["span", {class: "icon"}, [["i", {class: "yi-arrow-left"}]]]
|
|
712
|
+
], {
|
|
713
|
+
click: (evt) => {
|
|
714
|
+
evt.stopPropagation();
|
|
715
|
+
gobj_send_event(gobj, "EV_BACK", {}, gobj);
|
|
716
|
+
}
|
|
717
|
+
}]);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
let $trail = [
|
|
721
|
+
["span", {class: "SCHEMA_CRUMB_ROOT", i18n: "schemas"}, t("schemas")]
|
|
722
|
+
];
|
|
723
|
+
if(priv.treedb_id) {
|
|
724
|
+
$trail.push(["span", {class: "SCHEMA_CRUMB_SEP"}, "/"]);
|
|
725
|
+
$trail.push(["span", {class: "SCHEMA_CRUMB_TREEDB"}, priv.treedb_id]);
|
|
726
|
+
}
|
|
727
|
+
if(priv.topic_name) {
|
|
728
|
+
$trail.push(["span", {class: "SCHEMA_CRUMB_SEP"}, "/"]);
|
|
729
|
+
$trail.push(["span", {class: "SCHEMA_CRUMB_TOPIC"}, priv.topic_name]);
|
|
730
|
+
}
|
|
731
|
+
if(priv.diagram) {
|
|
732
|
+
$trail.push(["span", {class: "SCHEMA_CRUMB_SEP"}, "/"]);
|
|
733
|
+
$trail.push(["span", {class: "SCHEMA_CRUMB_DIAGRAM", i18n: "diagram"}, t("diagram")]);
|
|
734
|
+
}
|
|
735
|
+
$left.push(["nav", {class: "SCHEMA_CRUMBS is-size-7 has-text-grey ml-1"}, $trail]);
|
|
736
|
+
|
|
737
|
+
/*----------------------------------------------*
|
|
738
|
+
* Right: what is legal HERE. A button that is not
|
|
739
|
+
* legal is left out; a button that is legal and
|
|
740
|
+
* refused says why when it is pressed.
|
|
741
|
+
*----------------------------------------------*/
|
|
742
|
+
let $right = [];
|
|
743
|
+
if(priv.treedb_id && has_model) {
|
|
744
|
+
$right.push(toolbar_button("SCHEMA_DIAGRAM_BTN", "yi-hexagon-nodes",
|
|
745
|
+
priv.diagram ? "topics" : "diagram", "EV_TOGGLE_DIAGRAM", false));
|
|
746
|
+
$right.push(toolbar_button("SCHEMA_VALIDATE_BTN", "yi-circle-check",
|
|
747
|
+
"check", "EV_VALIDATE", false));
|
|
748
|
+
$right.push(toolbar_button("SCHEMA_EXPORT_BTN", "yi-file-export",
|
|
749
|
+
"export", "EV_EXPORT", false));
|
|
750
|
+
if(!readonly) {
|
|
751
|
+
$right.push(toolbar_button("SCHEMA_IMPORT_BTN", "yi-file-import",
|
|
752
|
+
"import", "EV_IMPORT", false));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if(priv.topic_name && !readonly && !priv.diagram) {
|
|
756
|
+
$right.push(toolbar_button("SCHEMA_ADD_COL_BTN", "yi-plus",
|
|
757
|
+
"new column", "EV_ADD_COLUMN", false));
|
|
758
|
+
}
|
|
759
|
+
if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram) {
|
|
760
|
+
$right.push(toolbar_button("SCHEMA_ADD_TOPIC_BTN", "yi-plus",
|
|
761
|
+
"new topic", "EV_ADD_TOPIC", false));
|
|
762
|
+
}
|
|
763
|
+
$right.push(toolbar_button("SCHEMA_REFRESH_BTN", "yi-arrows-rotate",
|
|
764
|
+
"refresh", "EV_REFRESH", state === "ST_LOADING"));
|
|
765
|
+
|
|
766
|
+
let $bar = createElement2(
|
|
767
|
+
["div", {class: "SCHEMA_TOOLBAR_INNER is-flex is-align-items-center",
|
|
768
|
+
style: "gap:.25rem; width:100%;"}, [
|
|
769
|
+
["div", {class: "SCHEMA_TOOLBAR_START is-flex is-align-items-center",
|
|
770
|
+
style: "gap:.25rem; min-width:0;"}, $left],
|
|
771
|
+
["div", {class: "SCHEMA_TOOLBAR_END is-flex is-align-items-center",
|
|
772
|
+
style: "gap:.25rem; margin-left:auto;"}, $right]
|
|
773
|
+
]]
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
/* The click handlers above reach the gobj through the button they
|
|
777
|
+
* are on: a kw may not carry one (the machine trace dumps it), and
|
|
778
|
+
* a closure per button would keep this gclass alive after it is
|
|
779
|
+
* destroyed. */
|
|
780
|
+
for(let $button of $bar.querySelectorAll("button")) {
|
|
781
|
+
$button.__gobj__ = gobj;
|
|
782
|
+
if(state === "ST_SAVING") {
|
|
783
|
+
$button.disabled = true;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
$toolbar.appendChild($bar);
|
|
787
|
+
refresh_language($toolbar, t);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/***************************************************************
|
|
791
|
+
* Draw whatever the current state says. One place decides which
|
|
792
|
+
* screen is on, so a state change and a re-render cannot
|
|
793
|
+
* disagree.
|
|
794
|
+
***************************************************************/
|
|
795
|
+
function render(gobj)
|
|
796
|
+
{
|
|
797
|
+
let priv = gobj.priv;
|
|
798
|
+
let state = gobj_current_state(gobj);
|
|
799
|
+
let $body = $of(gobj, ".SCHEMA_BODY");
|
|
800
|
+
|
|
801
|
+
if(!$body) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
render_toolbar(gobj);
|
|
805
|
+
|
|
806
|
+
if(state === "ST_LOADING") {
|
|
807
|
+
show_notice(gobj, "loading the schemas");
|
|
808
|
+
clear($body);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if(state === "ST_IDLE") {
|
|
812
|
+
show_notice(gobj, priv.load_error ? "cannot load the schemas" : "not connected",
|
|
813
|
+
priv.load_error);
|
|
814
|
+
clear($body);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if(state === "ST_EMPTY") {
|
|
818
|
+
show_notice(gobj, "this yuno stores no schema");
|
|
819
|
+
clear($body);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if(state === "ST_SAVING") {
|
|
823
|
+
return; /* the screen under it stays; end_writes redraws it */
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
show_notice(gobj, "");
|
|
827
|
+
if(state === "ST_DIAGRAM") {
|
|
828
|
+
render_diagram(gobj);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
destroy_diagram(gobj);
|
|
832
|
+
clear($body);
|
|
833
|
+
if(state === "ST_COLUMNS") {
|
|
834
|
+
render_columns(gobj, $body);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if(state === "ST_TOPICS") {
|
|
838
|
+
render_topics(gobj, $body);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
render_treedbs(gobj, $body);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/***************************************************************
|
|
845
|
+
* A card that opens something, with its own logical name. The
|
|
846
|
+
* cards grid is the shell's, so a schema card and a nav card
|
|
847
|
+
* look alike on purpose.
|
|
848
|
+
***************************************************************/
|
|
849
|
+
function open_card(gobj, logical, title, icon, tags, event, kw)
|
|
850
|
+
{
|
|
851
|
+
let $tags = tags.map((tag) => {
|
|
852
|
+
return ["span", {class: `${logical}_TAG tag ${tag.style || "is-light"}`},
|
|
853
|
+
tag.i18n
|
|
854
|
+
? [["span", {i18n: tag.i18n}, t(tag.i18n)],
|
|
855
|
+
["span", {class: "ml-1"}, `${tag.text}`]]
|
|
856
|
+
: `${tag.text}`];
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
return ["div", {class: `${logical} yui-nav-card card`, role: "button", tabindex: "0"}, [
|
|
860
|
+
["div", {class: `${logical}_BODY card-content p-3`}, [
|
|
861
|
+
["p", {class: `${logical}_TITLE is-flex is-align-items-center`,
|
|
862
|
+
style: "gap:.4rem;"}, [
|
|
863
|
+
["span", {class: "icon"}, [["i", {class: icon}]]],
|
|
864
|
+
["span", {class: `${logical}_NAME has-text-weight-semibold`}, `${title}`]
|
|
865
|
+
]],
|
|
866
|
+
["div", {class: `${logical}_TAGS tags mt-2`, style: "gap:.25rem;"}, $tags]
|
|
867
|
+
]]
|
|
868
|
+
], {
|
|
869
|
+
click: (evt) => {
|
|
870
|
+
evt.stopPropagation();
|
|
871
|
+
gobj_send_event(gobj, event, kw, gobj);
|
|
872
|
+
},
|
|
873
|
+
keydown: (evt) => {
|
|
874
|
+
if(evt.key !== "Enter" && evt.key !== " ") {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
evt.preventDefault();
|
|
878
|
+
gobj_send_event(gobj, event, kw, gobj);
|
|
879
|
+
}
|
|
880
|
+
}];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/***************************************************************
|
|
884
|
+
* ST_TREEDBS — the treedbs this yuno keeps a schema for.
|
|
885
|
+
*
|
|
886
|
+
* The badge that matters is the version pair: `schema_version`
|
|
887
|
+
* above `c_schema_version` is the shape of an operator edit AND
|
|
888
|
+
* the shape of a plain re-projection, so it is shown and not
|
|
889
|
+
* interpreted — `diff-schema`, in the tab above this view, is
|
|
890
|
+
* what tells the two apart.
|
|
891
|
+
***************************************************************/
|
|
892
|
+
function render_treedbs(gobj, $body)
|
|
893
|
+
{
|
|
894
|
+
let priv = gobj.priv;
|
|
895
|
+
let $cards = [];
|
|
896
|
+
|
|
897
|
+
for(let treedb of priv.model.treedbs) {
|
|
898
|
+
let tags = [
|
|
899
|
+
{i18n: "topics", text: treedb.topics.length},
|
|
900
|
+
{i18n: "version", text: treedb.schema_version === undefined
|
|
901
|
+
? "?" : treedb.schema_version}
|
|
902
|
+
];
|
|
903
|
+
if(treedb.c_schema_version !== undefined &&
|
|
904
|
+
String(treedb.c_schema_version) !== String(treedb.schema_version)
|
|
905
|
+
) {
|
|
906
|
+
tags.push({i18n: "from c", text: treedb.c_schema_version, style: "is-warning is-light"});
|
|
907
|
+
}
|
|
908
|
+
$cards.push(open_card(gobj, "SCHEMA_TREEDB_CARD", treedb.id, "yi-database",
|
|
909
|
+
tags, "EV_SELECT_TREEDB", {treedb: treedb.id}));
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if(priv.model.orphan_topics.length > 0 || priv.model.orphan_cols.length > 0) {
|
|
913
|
+
/* A leftover of a deletion belongs to no treedb and would be
|
|
914
|
+
* invisible in a screen that only draws the tree. Saying it
|
|
915
|
+
* here is the only place it can be said. */
|
|
916
|
+
$cards.push(open_card(gobj, "SCHEMA_ORPHAN_CARD", t("orphans"), "yi-triangle-exclamation",
|
|
917
|
+
[{i18n: "topics", text: priv.model.orphan_topics.length},
|
|
918
|
+
{i18n: "columns", text: priv.model.orphan_cols.length}],
|
|
919
|
+
"EV_SHOW_ORPHANS", {}));
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
let $grid = createElement2(
|
|
923
|
+
["div", {class: "SCHEMA_TREEDBS yui-nav-cards p-3"}, $cards]
|
|
924
|
+
);
|
|
925
|
+
$body.appendChild($grid);
|
|
926
|
+
refresh_language($grid, t);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/***************************************************************
|
|
930
|
+
* ST_TOPICS — the topics of one treedb, in schema order.
|
|
931
|
+
*
|
|
932
|
+
* A row per topic rather than a card: the operator is comparing
|
|
933
|
+
* versions and column counts down a column, which a grid of
|
|
934
|
+
* cards makes them read across.
|
|
935
|
+
***************************************************************/
|
|
936
|
+
function render_topics(gobj, $body)
|
|
937
|
+
{
|
|
938
|
+
let priv = gobj.priv;
|
|
939
|
+
let treedb = current_treedb(gobj);
|
|
940
|
+
|
|
941
|
+
if(!treedb) {
|
|
942
|
+
show_notice(gobj, "that treedb is not here any more", priv.treedb_id);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let readonly = is_readonly(gobj);
|
|
947
|
+
let $rows = [];
|
|
948
|
+
for(let topic of treedb.topics) {
|
|
949
|
+
let pending = priv.written[topic.id] &&
|
|
950
|
+
String(priv.baseline[topic.id]) === String(topic.topic_version);
|
|
951
|
+
|
|
952
|
+
let $actions = [];
|
|
953
|
+
if(!readonly) {
|
|
954
|
+
$actions.push(row_icon(gobj, "SCHEMA_TOPIC_EDIT", "yi-pen", "edit topic",
|
|
955
|
+
"EV_EDIT_TOPIC", {topic: topic.name}));
|
|
956
|
+
$actions.push(row_icon(gobj, "SCHEMA_TOPIC_DELETE", "yi-trash", "delete topic",
|
|
957
|
+
"EV_DELETE_TOPIC", {topic: topic.name}));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
$rows.push(["tr", {class: "SCHEMA_TOPIC_ROW", role: "button", tabindex: "0"}, [
|
|
961
|
+
["td", {class: "SCHEMA_TOPIC_NAME"}, [
|
|
962
|
+
["span", {class: "has-text-weight-semibold"}, `${topic.name}`],
|
|
963
|
+
topic.system_topic
|
|
964
|
+
? ["span", {class: "SCHEMA_TOPIC_SYSTEM tag is-light ml-2",
|
|
965
|
+
i18n: "system"}, t("system")]
|
|
966
|
+
: ["span", {}, ""]
|
|
967
|
+
]],
|
|
968
|
+
["td", {class: "SCHEMA_TOPIC_PKEY"}, [["code", {}, `${topic.pkey || "id"}`]]],
|
|
969
|
+
["td", {class: "SCHEMA_TOPIC_COLS has-text-right"}, `${topic.cols.length}`],
|
|
970
|
+
["td", {class: "SCHEMA_TOPIC_VERSION"}, [
|
|
971
|
+
["span", {class: `tag ${pending ? "is-warning" : "is-info"} is-light`},
|
|
972
|
+
`${topic.topic_version === undefined ? "?" : topic.topic_version}`]
|
|
973
|
+
]],
|
|
974
|
+
["td", {class: "SCHEMA_TOPIC_ACTIONS has-text-right"}, $actions]
|
|
975
|
+
], {
|
|
976
|
+
click: (evt) => {
|
|
977
|
+
if(evt.target.closest(".SCHEMA_TOPIC_ACTIONS")) {
|
|
978
|
+
return; /* a row action, not the row */
|
|
979
|
+
}
|
|
980
|
+
gobj_send_event(gobj, "EV_SELECT_TOPIC", {topic: topic.name}, gobj);
|
|
981
|
+
},
|
|
982
|
+
keydown: (evt) => {
|
|
983
|
+
if(evt.key !== "Enter" && evt.key !== " ") {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
evt.preventDefault();
|
|
987
|
+
gobj_send_event(gobj, "EV_SELECT_TOPIC", {topic: topic.name}, gobj);
|
|
988
|
+
}
|
|
989
|
+
}]);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
let $table = createElement2(
|
|
993
|
+
["div", {class: "SCHEMA_TOPICS p-3", style: "overflow-x:auto;"}, [
|
|
994
|
+
["table", {class: "SCHEMA_TOPICS_TABLE table is-fullwidth is-narrow is-hoverable"}, [
|
|
995
|
+
["thead", {}, [
|
|
996
|
+
["tr", {}, [
|
|
997
|
+
["th", {i18n: "topic"}, t("topic")],
|
|
998
|
+
["th", {i18n: "pkey"}, t("pkey")],
|
|
999
|
+
["th", {class: "has-text-right", i18n: "columns"}, t("columns")],
|
|
1000
|
+
["th", {i18n: "version"}, t("version")],
|
|
1001
|
+
["th", {}, ""]
|
|
1002
|
+
]]
|
|
1003
|
+
]],
|
|
1004
|
+
["tbody", {}, $rows]
|
|
1005
|
+
]]
|
|
1006
|
+
]]
|
|
1007
|
+
);
|
|
1008
|
+
$body.appendChild($table);
|
|
1009
|
+
refresh_language($table, t);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/***************************************************************
|
|
1013
|
+
* A small icon button inside a row. Icon only, with the label
|
|
1014
|
+
* in `title`/`aria-label`: a row is the one place where the
|
|
1015
|
+
* labels do not fit at 360px.
|
|
1016
|
+
***************************************************************/
|
|
1017
|
+
function row_icon(gobj, logical, icon, key, event, kw)
|
|
1018
|
+
{
|
|
1019
|
+
return ["button", {class: `${logical} button is-small is-ghost`,
|
|
1020
|
+
type: "button",
|
|
1021
|
+
title: t(key), "aria-label": t(key),
|
|
1022
|
+
"data-i18n-title": key, "data-i18n-aria-label": key}, [
|
|
1023
|
+
["span", {class: "icon is-small"}, [["i", {class: icon}]]]
|
|
1024
|
+
], {
|
|
1025
|
+
click: (evt) => {
|
|
1026
|
+
evt.stopPropagation();
|
|
1027
|
+
gobj_send_event(gobj, event, kw, gobj);
|
|
1028
|
+
}
|
|
1029
|
+
}];
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/***************************************************************
|
|
1033
|
+
* ST_COLUMNS — the columns of one topic, in schema order.
|
|
1034
|
+
*
|
|
1035
|
+
* The order is a FIELD (`order`), so the rows are draggable and
|
|
1036
|
+
* a drop writes the rows whose place actually changed — a drag
|
|
1037
|
+
* in a forty-column topic is two or three writes, not forty.
|
|
1038
|
+
*
|
|
1039
|
+
* What each row shows is what a schema is read for: the name,
|
|
1040
|
+
* what it holds, and what it links to. The rest of a column
|
|
1041
|
+
* definition is one click away, in the form.
|
|
1042
|
+
***************************************************************/
|
|
1043
|
+
function render_columns(gobj, $body)
|
|
1044
|
+
{
|
|
1045
|
+
let priv = gobj.priv;
|
|
1046
|
+
let topic = current_topic(gobj);
|
|
1047
|
+
|
|
1048
|
+
if(!topic) {
|
|
1049
|
+
show_notice(gobj, "that topic is not here any more", priv.topic_name);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
let readonly = is_readonly(gobj);
|
|
1054
|
+
let $wrap = createElement2(["div", {class: "SCHEMA_COLUMNS p-3"}, []]);
|
|
1055
|
+
|
|
1056
|
+
/*----------------------------------------------*
|
|
1057
|
+
* The version banner: the one thing that makes
|
|
1058
|
+
* an edit succeed and change nothing.
|
|
1059
|
+
*----------------------------------------------*/
|
|
1060
|
+
let $banner = version_banner(gobj, topic);
|
|
1061
|
+
if($banner) {
|
|
1062
|
+
$wrap.appendChild($banner);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
let $rows = [];
|
|
1066
|
+
for(let i = 0; i < topic.cols.length; i++) {
|
|
1067
|
+
$rows.push(column_row(gobj, topic, topic.cols[i], i, readonly));
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
let $table = createElement2(
|
|
1071
|
+
["div", {class: "SCHEMA_COLUMNS_WRAP", style: "overflow-x:auto;"}, [
|
|
1072
|
+
["table", {class: "SCHEMA_COLUMNS_TABLE table is-fullwidth is-narrow is-hoverable"}, [
|
|
1073
|
+
["thead", {}, [
|
|
1074
|
+
["tr", {}, [
|
|
1075
|
+
["th", {class: "SCHEMA_COL_HANDLE_TH"}, ""],
|
|
1076
|
+
["th", {i18n: "column"}, t("column")],
|
|
1077
|
+
["th", {i18n: "header"}, t("header")],
|
|
1078
|
+
["th", {i18n: "type"}, t("type")],
|
|
1079
|
+
["th", {i18n: "flags"}, t("flags")],
|
|
1080
|
+
["th", {i18n: "link"}, t("link")],
|
|
1081
|
+
["th", {}, ""]
|
|
1082
|
+
]]
|
|
1083
|
+
]],
|
|
1084
|
+
["tbody", {class: "SCHEMA_COLUMNS_BODY"}, $rows]
|
|
1085
|
+
]]
|
|
1086
|
+
]]
|
|
1087
|
+
);
|
|
1088
|
+
$wrap.appendChild($table);
|
|
1089
|
+
|
|
1090
|
+
if(topic.cols.length === 0) {
|
|
1091
|
+
$wrap.appendChild(createElement2(
|
|
1092
|
+
["p", {class: "SCHEMA_COLUMNS_EMPTY has-text-grey p-3",
|
|
1093
|
+
i18n: "this topic has no column"}, t("this topic has no column")]
|
|
1094
|
+
));
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
$body.appendChild($wrap);
|
|
1098
|
+
refresh_language($wrap, t);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/***************************************************************
|
|
1102
|
+
* "This topic changed and its version did not."
|
|
1103
|
+
*
|
|
1104
|
+
* Only shown when it is TRUE — the topic was written in this
|
|
1105
|
+
* session and its `topic_version` still reads what it read when
|
|
1106
|
+
* the model loaded. Every write this view makes raises it, so
|
|
1107
|
+
* the banner is for the case where something else did not: an
|
|
1108
|
+
* edit made from the raw tables, or a bump undone by hand.
|
|
1109
|
+
***************************************************************/
|
|
1110
|
+
function version_banner(gobj, topic)
|
|
1111
|
+
{
|
|
1112
|
+
let priv = gobj.priv;
|
|
1113
|
+
|
|
1114
|
+
if(!priv.written[topic.id]) {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
if(String(priv.baseline[topic.id]) !== String(topic.topic_version)) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
if(is_readonly(gobj)) {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
let $banner = createElement2(
|
|
1125
|
+
["div", {class: "SCHEMA_VERSION_BANNER notification is-warning is-light p-3 mb-3 " +
|
|
1126
|
+
"is-flex is-align-items-center", style: "gap:.5rem;"}, [
|
|
1127
|
+
["span", {class: "icon"}, [["i", {class: "yi-triangle-exclamation"}]]],
|
|
1128
|
+
["span", {class: "SCHEMA_VERSION_TEXT is-size-7", i18n: "version not raised"},
|
|
1129
|
+
t("version not raised")],
|
|
1130
|
+
["button", {class: "SCHEMA_VERSION_BUMP button is-small is-warning",
|
|
1131
|
+
type: "button",
|
|
1132
|
+
style: "margin-left:auto;",
|
|
1133
|
+
title: t("raise the version"), "aria-label": t("raise the version"),
|
|
1134
|
+
"data-i18n-title": "raise the version",
|
|
1135
|
+
"data-i18n-aria-label": "raise the version"}, [
|
|
1136
|
+
["span", {class: "icon"}, [["i", {class: "yi-arrow-up"}]]],
|
|
1137
|
+
["span", {i18n: "raise"}, t("raise")]
|
|
1138
|
+
]]
|
|
1139
|
+
]]
|
|
1140
|
+
);
|
|
1141
|
+
let $bump = $banner.querySelector(".SCHEMA_VERSION_BUMP");
|
|
1142
|
+
$bump.addEventListener("click", (evt) => {
|
|
1143
|
+
evt.stopPropagation();
|
|
1144
|
+
gobj_send_event(gobj, "EV_BUMP_VERSION", {topic: topic.name}, gobj);
|
|
1145
|
+
});
|
|
1146
|
+
return $banner;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/***************************************************************
|
|
1150
|
+
* One column, as a row. The drag events carry INDEXES in the
|
|
1151
|
+
* dataset and the drop turns them into an event: the kw of a
|
|
1152
|
+
* Yuneta event is plain JSON, so a DOM node never travels in
|
|
1153
|
+
* one.
|
|
1154
|
+
***************************************************************/
|
|
1155
|
+
function column_row(gobj, topic, col, index, readonly)
|
|
1156
|
+
{
|
|
1157
|
+
let record = col.record || {};
|
|
1158
|
+
let flags = col_flags(record);
|
|
1159
|
+
let hook = col_hook(record);
|
|
1160
|
+
let is_pkey = (col.name === (topic.pkey || "id"));
|
|
1161
|
+
|
|
1162
|
+
let $flags = flags.map((flag) => {
|
|
1163
|
+
return ["span", {class: `SCHEMA_COL_FLAG tag is-light ${flag_style(flag)}`}, `${flag}`];
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
let link = "";
|
|
1167
|
+
if(hook) {
|
|
1168
|
+
link = "→ " + Object.keys(hook).join(", ");
|
|
1169
|
+
} else if(flags.indexOf("fkey") >= 0) {
|
|
1170
|
+
link = "↖";
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
let $actions = [];
|
|
1174
|
+
if(!readonly) {
|
|
1175
|
+
$actions.push(row_icon(gobj, "SCHEMA_COL_EDIT", "yi-pen", "edit column",
|
|
1176
|
+
"EV_EDIT_COLUMN", {col: col.name}));
|
|
1177
|
+
$actions.push(row_icon(gobj, "SCHEMA_COL_DUPLICATE", "yi-copy", "duplicate column",
|
|
1178
|
+
"EV_DUPLICATE_COLUMN", {col: col.name}));
|
|
1179
|
+
$actions.push(row_icon(gobj, "SCHEMA_COL_DELETE", "yi-trash", "delete column",
|
|
1180
|
+
"EV_DELETE_COLUMN", {col: col.name}));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
let $row = createElement2(
|
|
1184
|
+
["tr", {class: "SCHEMA_COL_ROW", "data-index": `${index}`,
|
|
1185
|
+
draggable: readonly ? "false" : "true"}, [
|
|
1186
|
+
["td", {class: "SCHEMA_COL_HANDLE"},
|
|
1187
|
+
readonly ? [] : [["span", {class: "icon is-small has-text-grey"},
|
|
1188
|
+
[["i", {class: "yi-grip-vertical"}]]]]],
|
|
1189
|
+
["td", {class: "SCHEMA_COL_NAME"}, [
|
|
1190
|
+
["code", {}, `${col.name}`],
|
|
1191
|
+
is_pkey
|
|
1192
|
+
? ["span", {class: "SCHEMA_COL_PKEY tag is-info is-light ml-2",
|
|
1193
|
+
i18n: "pkey"}, t("pkey")]
|
|
1194
|
+
: ["span", {}, ""]
|
|
1195
|
+
]],
|
|
1196
|
+
["td", {class: "SCHEMA_COL_HEADER"}, `${record.header || ""}`],
|
|
1197
|
+
["td", {class: "SCHEMA_COL_TYPE"}, [["code", {}, `${record.type || ""}`]]],
|
|
1198
|
+
["td", {class: "SCHEMA_COL_FLAGS"}, [
|
|
1199
|
+
["div", {class: "tags", style: "gap:.2rem;"}, $flags]
|
|
1200
|
+
]],
|
|
1201
|
+
["td", {class: "SCHEMA_COL_LINK"}, `${link}`],
|
|
1202
|
+
["td", {class: "SCHEMA_COL_ACTIONS has-text-right"}, $actions]
|
|
1203
|
+
]]
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
if(!readonly) {
|
|
1207
|
+
wire_row_drag(gobj, $row);
|
|
1208
|
+
}
|
|
1209
|
+
$row.addEventListener("dblclick", (evt) => {
|
|
1210
|
+
if(readonly || evt.target.closest(".SCHEMA_COL_ACTIONS")) {
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
gobj_send_event(gobj, "EV_EDIT_COLUMN", {col: col.name}, gobj);
|
|
1214
|
+
});
|
|
1215
|
+
return $row;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/***************************************************************
|
|
1219
|
+
* Which flags are worth a colour: the ones that change what
|
|
1220
|
+
* the record IS, rather than how it is drawn.
|
|
1221
|
+
***************************************************************/
|
|
1222
|
+
function flag_style(flag)
|
|
1223
|
+
{
|
|
1224
|
+
if(flag === "hook" || flag === "fkey") {
|
|
1225
|
+
return "is-link";
|
|
1226
|
+
}
|
|
1227
|
+
if(flag === "required" || flag === "notnull") {
|
|
1228
|
+
return "is-danger";
|
|
1229
|
+
}
|
|
1230
|
+
if(flag === "persistent") {
|
|
1231
|
+
return "is-success";
|
|
1232
|
+
}
|
|
1233
|
+
return "";
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/***************************************************************
|
|
1237
|
+
* Dragging a row to a new place. The DOM events are the
|
|
1238
|
+
* browser's — an operating system as far as this app is
|
|
1239
|
+
* concerned — so their only job is to turn a drop into
|
|
1240
|
+
* EV_MOVE_COLUMN, which is where the work happens.
|
|
1241
|
+
***************************************************************/
|
|
1242
|
+
function wire_row_drag(gobj, $row)
|
|
1243
|
+
{
|
|
1244
|
+
$row.addEventListener("dragstart", (evt) => {
|
|
1245
|
+
evt.dataTransfer.effectAllowed = "move";
|
|
1246
|
+
evt.dataTransfer.setData("text/plain", $row.dataset.index);
|
|
1247
|
+
$row.classList.add("SCHEMA_COL_DRAGGING");
|
|
1248
|
+
});
|
|
1249
|
+
$row.addEventListener("dragend", () => {
|
|
1250
|
+
$row.classList.remove("SCHEMA_COL_DRAGGING");
|
|
1251
|
+
let $body = $row.parentNode;
|
|
1252
|
+
if($body) {
|
|
1253
|
+
for(let $other of $body.querySelectorAll(".SCHEMA_COL_DROP_BEFORE, .SCHEMA_COL_DROP_AFTER")) {
|
|
1254
|
+
$other.classList.remove("SCHEMA_COL_DROP_BEFORE", "SCHEMA_COL_DROP_AFTER");
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
$row.addEventListener("dragover", (evt) => {
|
|
1259
|
+
evt.preventDefault();
|
|
1260
|
+
evt.dataTransfer.dropEffect = "move";
|
|
1261
|
+
let rect = $row.getBoundingClientRect();
|
|
1262
|
+
let before = (evt.clientY - rect.top) < (rect.height / 2);
|
|
1263
|
+
$row.classList.toggle("SCHEMA_COL_DROP_BEFORE", before);
|
|
1264
|
+
$row.classList.toggle("SCHEMA_COL_DROP_AFTER", !before);
|
|
1265
|
+
});
|
|
1266
|
+
$row.addEventListener("dragleave", () => {
|
|
1267
|
+
$row.classList.remove("SCHEMA_COL_DROP_BEFORE", "SCHEMA_COL_DROP_AFTER");
|
|
1268
|
+
});
|
|
1269
|
+
$row.addEventListener("drop", (evt) => {
|
|
1270
|
+
evt.preventDefault();
|
|
1271
|
+
evt.stopPropagation();
|
|
1272
|
+
let from = parseInt(evt.dataTransfer.getData("text/plain"), 10);
|
|
1273
|
+
let to = parseInt($row.dataset.index, 10);
|
|
1274
|
+
let rect = $row.getBoundingClientRect();
|
|
1275
|
+
if((evt.clientY - rect.top) >= (rect.height / 2)) {
|
|
1276
|
+
to = to + 1;
|
|
1277
|
+
}
|
|
1278
|
+
$row.classList.remove("SCHEMA_COL_DROP_BEFORE", "SCHEMA_COL_DROP_AFTER");
|
|
1279
|
+
if(!isFinite(from) || !isFinite(to)) {
|
|
1280
|
+
log_error(`${gobj_short_name(gobj)}: drop with no row index`);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
gobj_send_event(gobj, "EV_MOVE_COLUMN", {from: from, to: to}, gobj);
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/***************************************************************
|
|
1288
|
+
* ST_DIAGRAM — the treedb drawn from the records being edited.
|
|
1289
|
+
*
|
|
1290
|
+
* Built lazily and shown, never created hidden: G6 measures its
|
|
1291
|
+
* canvas the first time it is painted and would size it to a
|
|
1292
|
+
* zero rect.
|
|
1293
|
+
***************************************************************/
|
|
1294
|
+
function render_diagram(gobj)
|
|
1295
|
+
{
|
|
1296
|
+
let priv = gobj.priv;
|
|
1297
|
+
let $body = $of(gobj, ".SCHEMA_BODY");
|
|
1298
|
+
let treedb = current_treedb(gobj);
|
|
1299
|
+
|
|
1300
|
+
if(!$body || !treedb) {
|
|
1301
|
+
show_notice(gobj, "that treedb is not here any more", priv.treedb_id);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if(!gclass_find_by_name("C_YUI_TREEDB_SCHEMA")) {
|
|
1305
|
+
log_error(`${gobj_short_name(gobj)}: C_YUI_TREEDB_SCHEMA not registered by the app`);
|
|
1306
|
+
show_notice(gobj, "the schema drawing is not available");
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
let descs = topic_descs(treedb);
|
|
1311
|
+
if(priv.diagram_gobj) {
|
|
1312
|
+
gobj_write_attr(priv.diagram_gobj, "descs", descs);
|
|
1313
|
+
gobj_send_event(priv.diagram_gobj, "EV_SHOW", {}, gobj);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
clear($body);
|
|
1318
|
+
let $pane = createElement2(
|
|
1319
|
+
["div", {class: "SCHEMA_DIAGRAM", style: "height:100%; min-height:0; overflow:hidden;"}, []]
|
|
1320
|
+
);
|
|
1321
|
+
$body.appendChild($pane);
|
|
1322
|
+
|
|
1323
|
+
let diagram = gobj_create_pure_child(
|
|
1324
|
+
"diagram_" + clean_name(gobj_name(gobj)),
|
|
1325
|
+
"C_YUI_TREEDB_SCHEMA",
|
|
1326
|
+
{
|
|
1327
|
+
subscriber: gobj,
|
|
1328
|
+
descs: descs,
|
|
1329
|
+
/* No `node_route`: this diagram lives inside the editor and
|
|
1330
|
+
* a node click is answered here, not by a hash the shell
|
|
1331
|
+
* would have to own. */
|
|
1332
|
+
node_route: "",
|
|
1333
|
+
system: true
|
|
1334
|
+
},
|
|
1335
|
+
gobj
|
|
1336
|
+
);
|
|
1337
|
+
if(!diagram) {
|
|
1338
|
+
log_error(`${gobj_short_name(gobj)}: cannot create the schema drawing`);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
priv.diagram_gobj = diagram;
|
|
1342
|
+
let $d = gobj_read_attr(diagram, "$container");
|
|
1343
|
+
if($d) {
|
|
1344
|
+
$pane.appendChild($d);
|
|
1345
|
+
}
|
|
1346
|
+
gobj_start(diagram);
|
|
1347
|
+
gobj_send_event(diagram, "EV_SHOW", {}, gobj);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function destroy_diagram(gobj)
|
|
1351
|
+
{
|
|
1352
|
+
let priv = gobj.priv;
|
|
1353
|
+
|
|
1354
|
+
if(!priv.diagram_gobj) {
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
gobj_stop(priv.diagram_gobj);
|
|
1358
|
+
gobj_destroy(priv.diagram_gobj);
|
|
1359
|
+
priv.diagram_gobj = null;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
/***************************
|
|
1366
|
+
* The forms
|
|
1367
|
+
***************************/
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
/***************************************************************
|
|
1373
|
+
* One overlay at a time, closed from one place: a dialog left
|
|
1374
|
+
* behind by a re-render is a screen the operator can still type
|
|
1375
|
+
* into and nothing will read.
|
|
1376
|
+
***************************************************************/
|
|
1377
|
+
function close_dialog(gobj)
|
|
1378
|
+
{
|
|
1379
|
+
let priv = gobj.priv;
|
|
1380
|
+
|
|
1381
|
+
if(!priv.dialog) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
let dialog = priv.dialog;
|
|
1385
|
+
priv.dialog = null;
|
|
1386
|
+
try {
|
|
1387
|
+
dialog.close();
|
|
1388
|
+
} catch(e) {
|
|
1389
|
+
log_warning(`${gobj_short_name(gobj)}: closing a dialog threw: ${e}`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function open_dialog(gobj, $content, title, logical, title_prefix)
|
|
1394
|
+
{
|
|
1395
|
+
let priv = gobj.priv;
|
|
1396
|
+
|
|
1397
|
+
close_dialog(gobj);
|
|
1398
|
+
priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
|
|
1399
|
+
dialog: true,
|
|
1400
|
+
title: title,
|
|
1401
|
+
title_prefix: title_prefix || "",
|
|
1402
|
+
logical_class: logical,
|
|
1403
|
+
t: t,
|
|
1404
|
+
on_close: () => {
|
|
1405
|
+
priv.dialog = null;
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
refresh_language($content, t);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/***************************************************************
|
|
1412
|
+
* A labelled field. The label carries its key and the input
|
|
1413
|
+
* carries its name, so reading the form back is one query and
|
|
1414
|
+
* a language change does not have to know the form exists.
|
|
1415
|
+
***************************************************************/
|
|
1416
|
+
function field(logical, name, label, $control, help)
|
|
1417
|
+
{
|
|
1418
|
+
return ["div", {class: `${logical} field mb-3`}, [
|
|
1419
|
+
["label", {class: "label is-small", i18n: label}, t(label)],
|
|
1420
|
+
["div", {class: "control"}, [$control]],
|
|
1421
|
+
help
|
|
1422
|
+
? ["p", {class: "help", i18n: help}, t(help)]
|
|
1423
|
+
: ["span", {}, ""]
|
|
1424
|
+
]];
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function text_input(name, value, placeholder, disabled)
|
|
1428
|
+
{
|
|
1429
|
+
return ["input", el_attrs({class: "input is-small", type: "text",
|
|
1430
|
+
"data-name": name,
|
|
1431
|
+
value: value === undefined || value === null ? "" : `${value}`,
|
|
1432
|
+
placeholder: placeholder || "",
|
|
1433
|
+
disabled: disabled ? "disabled" : undefined})];
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function number_input(name, value)
|
|
1437
|
+
{
|
|
1438
|
+
return ["input", {class: "input is-small", type: "number",
|
|
1439
|
+
"data-name": name,
|
|
1440
|
+
value: (value === undefined || value === null || value === "")
|
|
1441
|
+
? "" : `${value}`}];
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function select_input(name, value, options)
|
|
1445
|
+
{
|
|
1446
|
+
let $options = options.map((option) => {
|
|
1447
|
+
return ["option", el_attrs({value: `${option}`,
|
|
1448
|
+
selected: `${option}` === `${value}` ? "selected" : undefined}),
|
|
1449
|
+
`${option}`];
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
return ["div", {class: "select is-small is-fullwidth"}, [
|
|
1453
|
+
["select", {"data-name": name}, $options]
|
|
1454
|
+
]];
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function textarea_input(name, value, rows, placeholder)
|
|
1458
|
+
{
|
|
1459
|
+
return ["textarea", {class: "textarea is-small", "data-name": name,
|
|
1460
|
+
rows: `${rows || 3}`,
|
|
1461
|
+
placeholder: placeholder || ""},
|
|
1462
|
+
value === undefined || value === null ? "" : `${value}`];
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/***************************************************************
|
|
1466
|
+
* Read a form back: every control carries its own name, so
|
|
1467
|
+
* this walks the dialog once and never knows which form it is.
|
|
1468
|
+
***************************************************************/
|
|
1469
|
+
function read_form($root)
|
|
1470
|
+
{
|
|
1471
|
+
let out = {};
|
|
1472
|
+
|
|
1473
|
+
for(let $control of $root.querySelectorAll("[data-name]")) {
|
|
1474
|
+
let name = $control.dataset.name;
|
|
1475
|
+
if($control.type === "checkbox") {
|
|
1476
|
+
out[name] = $control.checked;
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
out[name] = $control.value;
|
|
1480
|
+
}
|
|
1481
|
+
return out;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/***************************************************************
|
|
1485
|
+
* The flags of a column, as checkboxes that say what they do.
|
|
1486
|
+
*
|
|
1487
|
+
* Grouped the way they act, greyed when they are meaningless on
|
|
1488
|
+
* the chosen type, and never filtered: a flag already set on a
|
|
1489
|
+
* column of another type has to stay visible or the next save
|
|
1490
|
+
* drops it silently. Turning one on may turn another off —
|
|
1491
|
+
* `hook` and `fkey` are the two ends of one link — and
|
|
1492
|
+
* toggle_flag() is what knows that.
|
|
1493
|
+
***************************************************************/
|
|
1494
|
+
function flags_field(gobj, type, current)
|
|
1495
|
+
{
|
|
1496
|
+
let $groups = [];
|
|
1497
|
+
|
|
1498
|
+
for(let group of grouped_flags(type, current)) {
|
|
1499
|
+
let $boxes = group.flags.map((flag) => {
|
|
1500
|
+
return ["label", el_attrs({class: "SCHEMA_FLAG checkbox is-size-7 " +
|
|
1501
|
+
(flag.meaningful ? "" : "SCHEMA_FLAG_DIM has-text-grey-light"),
|
|
1502
|
+
title: flag.desc ? t(flag.desc) : "",
|
|
1503
|
+
"data-i18n-title": flag.desc || undefined}), [
|
|
1504
|
+
["input", el_attrs({type: "checkbox", class: "SCHEMA_FLAG_BOX",
|
|
1505
|
+
"data-flag": flag.name,
|
|
1506
|
+
checked: flag.on ? "checked" : undefined})],
|
|
1507
|
+
["span", {class: "SCHEMA_FLAG_NAME ml-1"}, `${flag.name}`]
|
|
1508
|
+
]];
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
$groups.push(["div", {class: "SCHEMA_FLAG_GROUP mb-2"}, [
|
|
1512
|
+
["p", {class: "SCHEMA_FLAG_GROUP_NAME is-size-7 has-text-weight-semibold has-text-grey",
|
|
1513
|
+
i18n: group.group}, t(group.group)],
|
|
1514
|
+
["div", {class: "SCHEMA_FLAG_LIST"}, $boxes]
|
|
1515
|
+
]]);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
return ["div", {class: "SCHEMA_FLAGS"}, $groups];
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/***************************************************************
|
|
1522
|
+
* The flags a dialog currently holds, in the order they were
|
|
1523
|
+
* drawn.
|
|
1524
|
+
***************************************************************/
|
|
1525
|
+
function read_flags($root)
|
|
1526
|
+
{
|
|
1527
|
+
let flags = [];
|
|
1528
|
+
|
|
1529
|
+
for(let $box of $root.querySelectorAll(".SCHEMA_FLAG_BOX")) {
|
|
1530
|
+
if($box.checked) {
|
|
1531
|
+
flags.push($box.dataset.flag);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return flags;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/***************************************************************
|
|
1538
|
+
* The column form, for a new column and for an existing one.
|
|
1539
|
+
*
|
|
1540
|
+
* A NAME IS NOT EDITABLE HERE, and the form says why: the store
|
|
1541
|
+
* keys a column by its qualified id — treedb, topic and name —
|
|
1542
|
+
* so renaming one is creating another and deleting this one.
|
|
1543
|
+
* Offering a field that silently does something else is worse
|
|
1544
|
+
* than not offering it.
|
|
1545
|
+
***************************************************************/
|
|
1546
|
+
function open_column_form(gobj, topic, col, prefill)
|
|
1547
|
+
{
|
|
1548
|
+
let priv = gobj.priv;
|
|
1549
|
+
/* A DUPLICATE is a creation whose fields start filled: the name must
|
|
1550
|
+
* be typed (the store keys a column by its whole path) and everything
|
|
1551
|
+
* else is the column it was copied from. */
|
|
1552
|
+
let record = prefill || (col && col.record) || {};
|
|
1553
|
+
let creating = !col;
|
|
1554
|
+
let type = record.type || "string";
|
|
1555
|
+
let hook = col_hook(record) || {};
|
|
1556
|
+
let hook_topic = Object.keys(hook)[0] || "";
|
|
1557
|
+
let hook_col = hook_topic ? hook[hook_topic] : "";
|
|
1558
|
+
let treedb = current_treedb(gobj);
|
|
1559
|
+
let sibling_topics = treedb ? treedb.topics.map(x => x.name) : [];
|
|
1560
|
+
|
|
1561
|
+
let $form = createElement2(
|
|
1562
|
+
["div", {class: "SCHEMA_COL_FORM box"}, [
|
|
1563
|
+
field("SCHEMA_COL_FORM_NAME", "value", "column",
|
|
1564
|
+
text_input("value", creating ? (prefill ? prefill.__name__ || "" : "") : col.name,
|
|
1565
|
+
t("column name"), !creating),
|
|
1566
|
+
creating ? "" : "a column is keyed by its whole path: renaming it is creating another one"),
|
|
1567
|
+
field("SCHEMA_COL_FORM_HEADER", "header", "header",
|
|
1568
|
+
text_input("header", record.header, "")),
|
|
1569
|
+
field("SCHEMA_COL_FORM_TYPE", "type", "type",
|
|
1570
|
+
select_input("type", type, COL_TYPES)),
|
|
1571
|
+
field("SCHEMA_COL_FORM_FILLSPACE", "fillspace", "fillspace",
|
|
1572
|
+
number_input("fillspace", record.fillspace)),
|
|
1573
|
+
["div", {class: "SCHEMA_COL_FORM_FLAGS field mb-3"}, [
|
|
1574
|
+
["label", {class: "label is-small", i18n: "flags"}, t("flags")],
|
|
1575
|
+
flags_field(gobj, type, col_flags(record))
|
|
1576
|
+
]],
|
|
1577
|
+
["div", {class: "SCHEMA_COL_FORM_HOOK field mb-3"}, [
|
|
1578
|
+
["label", {class: "label is-small", i18n: "hook"}, t("hook")],
|
|
1579
|
+
["div", {class: "is-flex", style: "gap:.4rem;"}, [
|
|
1580
|
+
select_input("hook_topic", hook_topic, [""].concat(sibling_topics)),
|
|
1581
|
+
text_input("hook_col", hook_col, t("the fkey column of the child"))
|
|
1582
|
+
]],
|
|
1583
|
+
["p", {class: "help", i18n: "a hook names the child topic and the column of the child that points back"},
|
|
1584
|
+
t("a hook names the child topic and the column of the child that points back")]
|
|
1585
|
+
]],
|
|
1586
|
+
field("SCHEMA_COL_FORM_ENUM", "enum", "enum",
|
|
1587
|
+
textarea_input("enum", col_enum(record).join("\n"), 3, t("one value per line")),
|
|
1588
|
+
"only read when the column is flagged enum"),
|
|
1589
|
+
field("SCHEMA_COL_FORM_DEFAULT", "default", "default",
|
|
1590
|
+
text_input("default", stringify_field(record.default), "")),
|
|
1591
|
+
field("SCHEMA_COL_FORM_PLACEHOLDER", "placeholder", "placeholder",
|
|
1592
|
+
text_input("placeholder", record.placeholder, "")),
|
|
1593
|
+
field("SCHEMA_COL_FORM_DESCRIPTION", "description", "description",
|
|
1594
|
+
text_input("description", record.description, "")),
|
|
1595
|
+
["div", {class: "SCHEMA_COL_FORM_ACTIONS is-flex mt-4",
|
|
1596
|
+
style: "gap:.5rem; justify-content:flex-end;"}, [
|
|
1597
|
+
["button", {class: "SCHEMA_COL_FORM_CANCEL button is-small", type: "button"},
|
|
1598
|
+
[["span", {i18n: "cancel"}, t("cancel")]]],
|
|
1599
|
+
["button", {class: "SCHEMA_COL_FORM_SAVE button is-small is-primary", type: "button"},
|
|
1600
|
+
[["span", {class: "icon"}, [["i", {class: "yi-floppy-disk"}]]],
|
|
1601
|
+
["span", {i18n: "save"}, t("save")]]]
|
|
1602
|
+
]]
|
|
1603
|
+
]]
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1606
|
+
/* The type decides which flags are meaningful, so changing it
|
|
1607
|
+
* redraws them — keeping whatever is checked, including a flag the
|
|
1608
|
+
* new type does not use. */
|
|
1609
|
+
let $type = $form.querySelector('[data-name="type"]');
|
|
1610
|
+
$type.addEventListener("change", () => {
|
|
1611
|
+
let $holder = $form.querySelector(".SCHEMA_FLAGS");
|
|
1612
|
+
let current = read_flags($form);
|
|
1613
|
+
let $new = createElement2(flags_field(gobj, $type.value, current));
|
|
1614
|
+
$holder.parentNode.replaceChild($new, $holder);
|
|
1615
|
+
refresh_language($new, t);
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
/* A flag that excludes another is answered here and not on save,
|
|
1619
|
+
* so the form never shows a state the store would refuse. */
|
|
1620
|
+
$form.addEventListener("change", (evt) => {
|
|
1621
|
+
let $box = evt.target.closest(".SCHEMA_FLAG_BOX");
|
|
1622
|
+
if(!$box) {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
let wanted = toggle_flag(read_flags($form), $box.dataset.flag, $box.checked);
|
|
1626
|
+
for(let $other of $form.querySelectorAll(".SCHEMA_FLAG_BOX")) {
|
|
1627
|
+
$other.checked = wanted.indexOf($other.dataset.flag) >= 0;
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
$form.querySelector(".SCHEMA_COL_FORM_CANCEL").addEventListener("click", (evt) => {
|
|
1632
|
+
evt.stopPropagation();
|
|
1633
|
+
close_dialog(gobj);
|
|
1634
|
+
});
|
|
1635
|
+
$form.querySelector(".SCHEMA_COL_FORM_SAVE").addEventListener("click", (evt) => {
|
|
1636
|
+
evt.stopPropagation();
|
|
1637
|
+
let values = read_form($form);
|
|
1638
|
+
values.flag = read_flags($form);
|
|
1639
|
+
gobj_send_event(gobj, "EV_SAVE_COLUMN", {
|
|
1640
|
+
topic: topic.name,
|
|
1641
|
+
col: creating ? "" : col.name,
|
|
1642
|
+
creating: creating,
|
|
1643
|
+
values: values
|
|
1644
|
+
}, gobj);
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
open_dialog(gobj, $form, creating ? "new column" : "edit column",
|
|
1648
|
+
"SCHEMA_COL_FORM_DIALOG", creating ? topic.name : `${topic.name}.${col.name}`);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/***************************************************************
|
|
1652
|
+
* A field whose stored form may be the JSON text of a value,
|
|
1653
|
+
* shown as text either way.
|
|
1654
|
+
***************************************************************/
|
|
1655
|
+
function stringify_field(value)
|
|
1656
|
+
{
|
|
1657
|
+
if(value === undefined || value === null) {
|
|
1658
|
+
return "";
|
|
1659
|
+
}
|
|
1660
|
+
if(typeof value === "object") {
|
|
1661
|
+
return JSON.stringify(value);
|
|
1662
|
+
}
|
|
1663
|
+
return `${value}`;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/***************************************************************
|
|
1667
|
+
* The topic form. `pkey` is a SELECT of the columns the topic
|
|
1668
|
+
* actually has: a pkey naming no column is a topic that does
|
|
1669
|
+
* not open, and it is the first thing the validator looks for.
|
|
1670
|
+
***************************************************************/
|
|
1671
|
+
function open_topic_form(gobj, treedb, topic)
|
|
1672
|
+
{
|
|
1673
|
+
let record = (topic && topic.record) || {};
|
|
1674
|
+
let creating = !topic;
|
|
1675
|
+
let col_names = topic ? topic.cols.map(c => c.name) : [];
|
|
1676
|
+
|
|
1677
|
+
let $form = createElement2(
|
|
1678
|
+
["div", {class: "SCHEMA_TOPIC_FORM box"}, [
|
|
1679
|
+
field("SCHEMA_TOPIC_FORM_NAME", "value", "topic",
|
|
1680
|
+
text_input("value", creating ? "" : topic.name, t("topic name"), !creating),
|
|
1681
|
+
creating ? "" : "a topic is keyed by its whole path: renaming it is creating another one"),
|
|
1682
|
+
field("SCHEMA_TOPIC_FORM_PKEY", "pkey", "pkey",
|
|
1683
|
+
creating
|
|
1684
|
+
? text_input("pkey", "id", "id")
|
|
1685
|
+
: select_input("pkey", record.pkey || "id",
|
|
1686
|
+
col_names.length > 0 ? col_names : [record.pkey || "id"])),
|
|
1687
|
+
field("SCHEMA_TOPIC_FORM_PKEY2S", "pkey2s", "secondary keys",
|
|
1688
|
+
text_input("pkey2s", stringify_field(record.pkey2s), ""),
|
|
1689
|
+
"the column a record is KNOWN by when its key is composed"),
|
|
1690
|
+
field("SCHEMA_TOPIC_FORM_SYSTEM_FLAG", "system_flag", "system flag",
|
|
1691
|
+
text_input("system_flag", record.system_flag || "sf_string_key", "")),
|
|
1692
|
+
field("SCHEMA_TOPIC_FORM_TKEY", "tkey", "time key",
|
|
1693
|
+
text_input("tkey", record.tkey, "")),
|
|
1694
|
+
field("SCHEMA_TOPIC_FORM_VERSION", "topic_version", "version",
|
|
1695
|
+
number_input("topic_version",
|
|
1696
|
+
record.topic_version === undefined ? 1 : record.topic_version),
|
|
1697
|
+
"raising it is what publishes a change of the columns"),
|
|
1698
|
+
["div", {class: "SCHEMA_TOPIC_FORM_SYSTEM field mb-3"}, [
|
|
1699
|
+
["label", {class: "checkbox is-size-7"}, [
|
|
1700
|
+
["input", el_attrs({type: "checkbox", "data-name": "system_topic",
|
|
1701
|
+
checked: record.system_topic ? "checked" : undefined})],
|
|
1702
|
+
["span", {class: "ml-1", i18n: "system topic"}, t("system topic")]
|
|
1703
|
+
]]
|
|
1704
|
+
]],
|
|
1705
|
+
["div", {class: "SCHEMA_TOPIC_FORM_ACTIONS is-flex mt-4",
|
|
1706
|
+
style: "gap:.5rem; justify-content:flex-end;"}, [
|
|
1707
|
+
["button", {class: "SCHEMA_TOPIC_FORM_CANCEL button is-small", type: "button"},
|
|
1708
|
+
[["span", {i18n: "cancel"}, t("cancel")]]],
|
|
1709
|
+
["button", {class: "SCHEMA_TOPIC_FORM_SAVE button is-small is-primary",
|
|
1710
|
+
type: "button"},
|
|
1711
|
+
[["span", {class: "icon"}, [["i", {class: "yi-floppy-disk"}]]],
|
|
1712
|
+
["span", {i18n: "save"}, t("save")]]]
|
|
1713
|
+
]]
|
|
1714
|
+
]]
|
|
1715
|
+
);
|
|
1716
|
+
|
|
1717
|
+
$form.querySelector(".SCHEMA_TOPIC_FORM_CANCEL").addEventListener("click", (evt) => {
|
|
1718
|
+
evt.stopPropagation();
|
|
1719
|
+
close_dialog(gobj);
|
|
1720
|
+
});
|
|
1721
|
+
$form.querySelector(".SCHEMA_TOPIC_FORM_SAVE").addEventListener("click", (evt) => {
|
|
1722
|
+
evt.stopPropagation();
|
|
1723
|
+
gobj_send_event(gobj, "EV_SAVE_TOPIC", {
|
|
1724
|
+
topic: creating ? "" : topic.name,
|
|
1725
|
+
creating: creating,
|
|
1726
|
+
values: read_form($form)
|
|
1727
|
+
}, gobj);
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
open_dialog(gobj, $form, creating ? "new topic" : "edit topic",
|
|
1731
|
+
"SCHEMA_TOPIC_FORM_DIALOG", creating ? treedb.id : `${treedb.id}.${topic.name}`);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/***************************************************************
|
|
1735
|
+
* What the treedb would refuse, as a table.
|
|
1736
|
+
*
|
|
1737
|
+
* Every `code` is an i18n key and carries it, so the report
|
|
1738
|
+
* follows a language change; the detail beside it is DATA (a
|
|
1739
|
+
* column name, a type) and never goes through t().
|
|
1740
|
+
***************************************************************/
|
|
1741
|
+
function open_report(gobj, findings, treedb_id)
|
|
1742
|
+
{
|
|
1743
|
+
let $rows = findings.map((finding) => {
|
|
1744
|
+
return ["tr", {class: `SCHEMA_REPORT_ROW SCHEMA_REPORT_${finding.severity.toUpperCase()}`}, [
|
|
1745
|
+
["td", {class: "SCHEMA_REPORT_SEVERITY"}, [
|
|
1746
|
+
["span", {class: `tag ${finding.severity === "error" ? "is-danger" : "is-warning"}` +
|
|
1747
|
+
" is-light", i18n: finding.severity}, t(finding.severity)]
|
|
1748
|
+
]],
|
|
1749
|
+
["td", {class: "SCHEMA_REPORT_WHAT", i18n: finding.code}, t(finding.code)],
|
|
1750
|
+
["td", {class: "SCHEMA_REPORT_TOPIC"}, `${finding.topic || ""}`],
|
|
1751
|
+
["td", {class: "SCHEMA_REPORT_COL"}, `${finding.col || ""}`],
|
|
1752
|
+
["td", {class: "SCHEMA_REPORT_DETAIL"}, [["code", {}, `${finding.detail || ""}`]]]
|
|
1753
|
+
]];
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
let $content = createElement2(
|
|
1757
|
+
["div", {class: "SCHEMA_REPORT box"}, [
|
|
1758
|
+
findings.length === 0
|
|
1759
|
+
? ["p", {class: "SCHEMA_REPORT_CLEAN", i18n: "nothing to report"},
|
|
1760
|
+
t("nothing to report")]
|
|
1761
|
+
: ["div", {class: "SCHEMA_REPORT_TABLE_WRAP", style: "overflow-x:auto;"}, [
|
|
1762
|
+
["table", {class: "SCHEMA_REPORT_TABLE table is-narrow is-fullwidth is-size-7"}, [
|
|
1763
|
+
["thead", {}, [
|
|
1764
|
+
["tr", {}, [
|
|
1765
|
+
["th", {i18n: "severity"}, t("severity")],
|
|
1766
|
+
["th", {i18n: "finding"}, t("finding")],
|
|
1767
|
+
["th", {i18n: "topic"}, t("topic")],
|
|
1768
|
+
["th", {i18n: "column"}, t("column")],
|
|
1769
|
+
["th", {i18n: "detail"}, t("detail")]
|
|
1770
|
+
]]
|
|
1771
|
+
]],
|
|
1772
|
+
["tbody", {}, $rows]
|
|
1773
|
+
]]
|
|
1774
|
+
]]
|
|
1775
|
+
]]
|
|
1776
|
+
);
|
|
1777
|
+
|
|
1778
|
+
open_dialog(gobj, $content, "check", "SCHEMA_REPORT_DIALOG", treedb_id);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/***************************************************************
|
|
1782
|
+
* The schema as its C literal, and as JSON.
|
|
1783
|
+
*
|
|
1784
|
+
* This is the half `diff-schema` cannot give: it says the two
|
|
1785
|
+
* halves drifted, and this is the edit in the form the SOURCE
|
|
1786
|
+
* can hold. The text is selectable and copyable — never offered
|
|
1787
|
+
* as a download, which a published page cannot start anyway.
|
|
1788
|
+
***************************************************************/
|
|
1789
|
+
function open_export(gobj, treedb)
|
|
1790
|
+
{
|
|
1791
|
+
let c_text = schema_to_c(treedb);
|
|
1792
|
+
let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
|
|
1793
|
+
|
|
1794
|
+
let $content = createElement2(
|
|
1795
|
+
["div", {class: "SCHEMA_EXPORT box"}, [
|
|
1796
|
+
["div", {class: "SCHEMA_EXPORT_TABS tabs is-small is-toggle mb-2"}, [
|
|
1797
|
+
["ul", {}, [
|
|
1798
|
+
["li", {class: "SCHEMA_EXPORT_TAB is-active", "data-pane": "c"},
|
|
1799
|
+
[["a", {}, [["span", {}, "C"]]]]],
|
|
1800
|
+
["li", {class: "SCHEMA_EXPORT_TAB", "data-pane": "json"},
|
|
1801
|
+
[["a", {}, [["span", {}, "JSON"]]]]]
|
|
1802
|
+
]]
|
|
1803
|
+
]],
|
|
1804
|
+
["p", {class: "SCHEMA_EXPORT_HELP help mb-2",
|
|
1805
|
+
i18n: "an edit made here works and lives in no source: this is what to paste back"},
|
|
1806
|
+
t("an edit made here works and lives in no source: this is what to paste back")],
|
|
1807
|
+
["textarea", {class: "SCHEMA_EXPORT_TEXT textarea is-small is-family-monospace",
|
|
1808
|
+
rows: "18", readonly: "readonly", spellcheck: "false"}, c_text],
|
|
1809
|
+
["div", {class: "SCHEMA_EXPORT_ACTIONS is-flex mt-3",
|
|
1810
|
+
style: "gap:.5rem; justify-content:flex-end;"}, [
|
|
1811
|
+
["button", {class: "SCHEMA_EXPORT_COPY button is-small", type: "button"}, [
|
|
1812
|
+
["span", {class: "icon"}, [["i", {class: "yi-copy"}]]],
|
|
1813
|
+
["span", {i18n: "copy"}, t("copy")]
|
|
1814
|
+
]]
|
|
1815
|
+
]]
|
|
1816
|
+
]]
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
let $text = $content.querySelector(".SCHEMA_EXPORT_TEXT");
|
|
1820
|
+
for(let $tab of $content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
|
|
1821
|
+
$tab.addEventListener("click", (evt) => {
|
|
1822
|
+
evt.stopPropagation();
|
|
1823
|
+
for(let $other of $content.querySelectorAll(".SCHEMA_EXPORT_TAB")) {
|
|
1824
|
+
$other.classList.toggle("is-active", $other === $tab);
|
|
1825
|
+
}
|
|
1826
|
+
$text.value = ($tab.dataset.pane === "c") ? c_text : json_text;
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
$content.querySelector(".SCHEMA_EXPORT_COPY").addEventListener("click", (evt) => {
|
|
1830
|
+
evt.stopPropagation();
|
|
1831
|
+
copy_text(gobj, $text);
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
open_dialog(gobj, $content, "export", "SCHEMA_EXPORT_DIALOG", treedb.id);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/***************************************************************
|
|
1838
|
+
* Copy through the clipboard when the page is allowed one, and
|
|
1839
|
+
* through a selection when it is not: an artifact or an http
|
|
1840
|
+
* origin has no navigator.clipboard, and a copy button that
|
|
1841
|
+
* does nothing is worse than one that selects the text.
|
|
1842
|
+
***************************************************************/
|
|
1843
|
+
function copy_text(gobj, $text)
|
|
1844
|
+
{
|
|
1845
|
+
let ok = () => {
|
|
1846
|
+
yui_shell_show_info(yui_shell_of(gobj), "copied", {t: t});
|
|
1847
|
+
};
|
|
1848
|
+
|
|
1849
|
+
$text.select();
|
|
1850
|
+
if(navigator.clipboard && navigator.clipboard.writeText) {
|
|
1851
|
+
navigator.clipboard.writeText($text.value).then(ok, (e) => {
|
|
1852
|
+
log_warning(`${gobj_short_name(gobj)}: clipboard refused: ${e}`);
|
|
1853
|
+
});
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
log_warning(`${gobj_short_name(gobj)}: no clipboard here, the text is selected instead`);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/***************************************************************
|
|
1860
|
+
* Import: paste a schema, see the plan, then run it.
|
|
1861
|
+
*
|
|
1862
|
+
* Two steps and not one, because this is the only operation in
|
|
1863
|
+
* the editor that can DELETE a column. What is shown is the
|
|
1864
|
+
* plan itself, so what is confirmed is what runs.
|
|
1865
|
+
***************************************************************/
|
|
1866
|
+
function open_import(gobj, treedb)
|
|
1867
|
+
{
|
|
1868
|
+
let $content = createElement2(
|
|
1869
|
+
["div", {class: "SCHEMA_IMPORT box"}, [
|
|
1870
|
+
["p", {class: "SCHEMA_IMPORT_HELP help mb-2",
|
|
1871
|
+
i18n: "paste a schema as json: the plan is shown before anything is written"},
|
|
1872
|
+
t("paste a schema as json: the plan is shown before anything is written")],
|
|
1873
|
+
["textarea", {class: "SCHEMA_IMPORT_TEXT textarea is-small is-family-monospace",
|
|
1874
|
+
rows: "12", spellcheck: "false",
|
|
1875
|
+
placeholder: '{"id": "...", "schema_version": "1", "topics": []}'}, ""],
|
|
1876
|
+
["label", {class: "SCHEMA_IMPORT_PRUNE checkbox is-size-7 mt-2"}, [
|
|
1877
|
+
["input", {type: "checkbox", class: "SCHEMA_IMPORT_PRUNE_BOX", checked: "checked"}],
|
|
1878
|
+
["span", {class: "ml-1", i18n: "delete what the pasted schema does not declare"},
|
|
1879
|
+
t("delete what the pasted schema does not declare")]
|
|
1880
|
+
]],
|
|
1881
|
+
["div", {class: "SCHEMA_IMPORT_PLAN mt-3"}, []],
|
|
1882
|
+
["div", {class: "SCHEMA_IMPORT_ACTIONS is-flex mt-3",
|
|
1883
|
+
style: "gap:.5rem; justify-content:flex-end;"}, [
|
|
1884
|
+
["button", {class: "SCHEMA_IMPORT_CANCEL button is-small", type: "button"},
|
|
1885
|
+
[["span", {i18n: "cancel"}, t("cancel")]]],
|
|
1886
|
+
["button", {class: "SCHEMA_IMPORT_PREVIEW button is-small", type: "button"},
|
|
1887
|
+
[["span", {class: "icon"}, [["i", {class: "yi-eye"}]]],
|
|
1888
|
+
["span", {i18n: "preview"}, t("preview")]]],
|
|
1889
|
+
["button", {class: "SCHEMA_IMPORT_RUN button is-small is-warning",
|
|
1890
|
+
type: "button", disabled: "disabled"},
|
|
1891
|
+
[["span", {class: "icon"}, [["i", {class: "yi-file-import"}]]],
|
|
1892
|
+
["span", {i18n: "import"}, t("import")]]]
|
|
1893
|
+
]]
|
|
1894
|
+
]]
|
|
1895
|
+
);
|
|
1896
|
+
|
|
1897
|
+
let $text = $content.querySelector(".SCHEMA_IMPORT_TEXT");
|
|
1898
|
+
let $prune = $content.querySelector(".SCHEMA_IMPORT_PRUNE_BOX");
|
|
1899
|
+
let $plan_pane = $content.querySelector(".SCHEMA_IMPORT_PLAN");
|
|
1900
|
+
let $run = $content.querySelector(".SCHEMA_IMPORT_RUN");
|
|
1901
|
+
|
|
1902
|
+
$content.querySelector(".SCHEMA_IMPORT_CANCEL").addEventListener("click", (evt) => {
|
|
1903
|
+
evt.stopPropagation();
|
|
1904
|
+
close_dialog(gobj);
|
|
1905
|
+
});
|
|
1906
|
+
$content.querySelector(".SCHEMA_IMPORT_PREVIEW").addEventListener("click", (evt) => {
|
|
1907
|
+
evt.stopPropagation();
|
|
1908
|
+
gobj_send_event(gobj, "EV_PREVIEW_IMPORT", {
|
|
1909
|
+
text: $text.value,
|
|
1910
|
+
prune: $prune.checked
|
|
1911
|
+
}, gobj);
|
|
1912
|
+
});
|
|
1913
|
+
$run.addEventListener("click", (evt) => {
|
|
1914
|
+
evt.stopPropagation();
|
|
1915
|
+
gobj_send_event(gobj, "EV_APPLY_IMPORT", {}, gobj);
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
gobj.priv.import_pane = $plan_pane;
|
|
1919
|
+
gobj.priv.import_run = $run;
|
|
1920
|
+
open_dialog(gobj, $content, "import", "SCHEMA_IMPORT_DIALOG", treedb.id);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/***************************************************************
|
|
1924
|
+
* The plan, drawn where the paste is: one line per write, the
|
|
1925
|
+
* deletions apart and counted, because they are what the
|
|
1926
|
+
* operator is being asked about.
|
|
1927
|
+
***************************************************************/
|
|
1928
|
+
function render_import_plan(gobj, plan)
|
|
1929
|
+
{
|
|
1930
|
+
let priv = gobj.priv;
|
|
1931
|
+
let $pane = priv.import_pane;
|
|
1932
|
+
|
|
1933
|
+
if(!$pane) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
clear($pane);
|
|
1937
|
+
|
|
1938
|
+
if(plan.conflicts.length > 0) {
|
|
1939
|
+
$pane.appendChild(createElement2(
|
|
1940
|
+
["div", {class: "SCHEMA_IMPORT_CONFLICTS notification is-danger is-light p-3"},
|
|
1941
|
+
plan.conflicts.map((conflict) => {
|
|
1942
|
+
return ["p", {class: "SCHEMA_IMPORT_CONFLICT is-size-7", i18n: conflict.code},
|
|
1943
|
+
t(conflict.code)];
|
|
1944
|
+
})]
|
|
1945
|
+
));
|
|
1946
|
+
if(priv.import_run) {
|
|
1947
|
+
priv.import_run.disabled = true;
|
|
1948
|
+
}
|
|
1949
|
+
refresh_language($pane, t);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
let deletes = plan_deletes(plan);
|
|
1954
|
+
let $lines = plan.writes.map((write) => {
|
|
1955
|
+
let what = write.what.col
|
|
1956
|
+
? `${write.what.topic}.${write.what.col}`
|
|
1957
|
+
: (write.what.topic || write.topic_name);
|
|
1958
|
+
return ["li", {class: `SCHEMA_IMPORT_LINE SCHEMA_IMPORT_${write.op.toUpperCase()}`}, [
|
|
1959
|
+
["span", {class: `tag is-light ${write.op === "delete" ? "is-danger" : "is-info"}`,
|
|
1960
|
+
i18n: write.op}, t(write.op)],
|
|
1961
|
+
["code", {class: "ml-2"}, `${what}`]
|
|
1962
|
+
]];
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
$pane.appendChild(createElement2(
|
|
1966
|
+
["div", {class: "SCHEMA_IMPORT_SUMMARY"}, [
|
|
1967
|
+
["p", {class: "SCHEMA_IMPORT_COUNT is-size-7 has-text-grey mb-2"}, [
|
|
1968
|
+
["span", {i18n: "writes"}, t("writes")],
|
|
1969
|
+
["span", {class: "ml-1"}, `${plan.writes.length}`],
|
|
1970
|
+
["span", {class: "ml-3", i18n: "deletions"}, t("deletions")],
|
|
1971
|
+
["span", {class: "ml-1 has-text-weight-bold"}, `${deletes.length}`]
|
|
1972
|
+
]],
|
|
1973
|
+
["ul", {class: "SCHEMA_IMPORT_LINES is-size-7",
|
|
1974
|
+
style: "max-height:14rem; overflow:auto;"}, $lines]
|
|
1975
|
+
]]
|
|
1976
|
+
));
|
|
1977
|
+
if(priv.import_run) {
|
|
1978
|
+
priv.import_run.disabled = (plan.writes.length === 0);
|
|
1979
|
+
}
|
|
1980
|
+
refresh_language($pane, t);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/***************************************************************
|
|
1984
|
+
* What belongs to nothing: a topic whose treedb is gone, a
|
|
1985
|
+
* column whose topic is gone. Leftovers of a deletion, and the
|
|
1986
|
+
* only screen they can be seen on.
|
|
1987
|
+
***************************************************************/
|
|
1988
|
+
function open_orphans(gobj)
|
|
1989
|
+
{
|
|
1990
|
+
let priv = gobj.priv;
|
|
1991
|
+
let readonly = is_readonly(gobj);
|
|
1992
|
+
|
|
1993
|
+
let row = (kind, entry) => {
|
|
1994
|
+
return ["tr", {class: "SCHEMA_ORPHAN_ROW"}, [
|
|
1995
|
+
["td", {class: "SCHEMA_ORPHAN_KIND", i18n: kind}, t(kind)],
|
|
1996
|
+
["td", {class: "SCHEMA_ORPHAN_ID"}, [["code", {}, `${entry.id}`]]],
|
|
1997
|
+
["td", {class: "SCHEMA_ORPHAN_ACTIONS has-text-right"},
|
|
1998
|
+
readonly ? [] : [row_icon(gobj, "SCHEMA_ORPHAN_DELETE", "yi-trash", "delete",
|
|
1999
|
+
"EV_DELETE_ORPHAN", {kind: kind, id: entry.id})]]
|
|
2000
|
+
]];
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
let $rows = priv.model.orphan_topics.map(x => row("topic", x))
|
|
2004
|
+
.concat(priv.model.orphan_cols.map(x => row("column", x)));
|
|
2005
|
+
|
|
2006
|
+
let $content = createElement2(
|
|
2007
|
+
["div", {class: "SCHEMA_ORPHANS box"}, [
|
|
2008
|
+
["p", {class: "SCHEMA_ORPHANS_HELP help mb-2",
|
|
2009
|
+
i18n: "these belong to no treedb or to no topic: leftovers of a deletion"},
|
|
2010
|
+
t("these belong to no treedb or to no topic: leftovers of a deletion")],
|
|
2011
|
+
["table", {class: "SCHEMA_ORPHANS_TABLE table is-narrow is-fullwidth is-size-7"}, [
|
|
2012
|
+
["tbody", {}, $rows]
|
|
2013
|
+
]]
|
|
2014
|
+
]]
|
|
2015
|
+
);
|
|
2016
|
+
|
|
2017
|
+
open_dialog(gobj, $content, "orphans", "SCHEMA_ORPHANS_DIALOG");
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
|
|
2022
|
+
|
|
2023
|
+
/***************************
|
|
2024
|
+
* Writing
|
|
2025
|
+
***************************/
|
|
2026
|
+
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
/***************************************************************
|
|
2031
|
+
* THE TWO VERSIONS A SCHEMA EDIT HAS TO RAISE, and why both.
|
|
2032
|
+
*
|
|
2033
|
+
* `topic_version` is what publishes a change of a topic's
|
|
2034
|
+
* columns: leave it and the persisted `topic_cols.json` masks
|
|
2035
|
+
* the edit, the restart succeeds and nothing moved.
|
|
2036
|
+
*
|
|
2037
|
+
* `schema_version` is what publishes the schema as a whole —
|
|
2038
|
+
* "the stored one wins on ties, and the incoming one has to be
|
|
2039
|
+
* strictly newer to take over" (c_treedb.c). It is safe to
|
|
2040
|
+
* raise: re-projection from C compares `c_schema_version`, the
|
|
2041
|
+
* version of the LITERAL, precisely so that an edit made here
|
|
2042
|
+
* survives every start until a newer literal arrives.
|
|
2043
|
+
*
|
|
2044
|
+
* So every write that changes a schema carries both, and the
|
|
2045
|
+
* operator is never asked to remember either.
|
|
2046
|
+
***************************************************************/
|
|
2047
|
+
function version_writes(gobj, topic)
|
|
2048
|
+
{
|
|
2049
|
+
let treedb = current_treedb(gobj);
|
|
2050
|
+
let writes = [];
|
|
2051
|
+
|
|
2052
|
+
if(topic) {
|
|
2053
|
+
let version = Number(topic.topic_version);
|
|
2054
|
+
if(!isFinite(version)) {
|
|
2055
|
+
version = 0;
|
|
2056
|
+
}
|
|
2057
|
+
writes.push({
|
|
2058
|
+
op: "update", topic_name: T_TOPICS,
|
|
2059
|
+
record: {id: topic.id, topic_version: version + 1}
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
if(treedb) {
|
|
2063
|
+
let version = Number(treedb.schema_version);
|
|
2064
|
+
if(!isFinite(version)) {
|
|
2065
|
+
version = 0;
|
|
2066
|
+
}
|
|
2067
|
+
writes.push({
|
|
2068
|
+
op: "update", topic_name: T_TREEDBS,
|
|
2069
|
+
record: {id: treedb.id, schema_version: version + 1}
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
return writes;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
/***************************************************************
|
|
2076
|
+
* Run a list of writes, one at a time, and say so while it
|
|
2077
|
+
* runs. One at a time because each answer patches the model the
|
|
2078
|
+
* next one is computed against, and because a treedb that
|
|
2079
|
+
* refuses the third of five must not be left having done two
|
|
2080
|
+
* and been asked for two more.
|
|
2081
|
+
***************************************************************/
|
|
2082
|
+
function queue_writes(gobj, writes)
|
|
2083
|
+
{
|
|
2084
|
+
let priv = gobj.priv;
|
|
2085
|
+
|
|
2086
|
+
if(!Array.isArray(writes) || writes.length === 0) {
|
|
2087
|
+
return 0;
|
|
2088
|
+
}
|
|
2089
|
+
if(gobj_current_state(gobj) === "ST_SAVING") {
|
|
2090
|
+
log_warning(`${gobj_short_name(gobj)}: a write is already in flight`);
|
|
2091
|
+
return -1;
|
|
2092
|
+
}
|
|
2093
|
+
priv.save_queue = writes.slice();
|
|
2094
|
+
priv.save_return = gobj_current_state(gobj);
|
|
2095
|
+
priv.save_wrote = false;
|
|
2096
|
+
gobj_change_state(gobj, "ST_SAVING");
|
|
2097
|
+
show_notice(gobj, "saving");
|
|
2098
|
+
/* The screen stays up and stops answering. Not to be tidy: every
|
|
2099
|
+
* action of a screen is computed against the model, and the answer
|
|
2100
|
+
* still in the air is about to change it — so a click during a save
|
|
2101
|
+
* would be an action decided on a schema that no longer exists. */
|
|
2102
|
+
set_busy(gobj, true);
|
|
2103
|
+
render_toolbar(gobj);
|
|
2104
|
+
return run_next_write(gobj);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
function run_next_write(gobj)
|
|
2108
|
+
{
|
|
2109
|
+
let priv = gobj.priv;
|
|
2110
|
+
|
|
2111
|
+
if(priv.save_queue.length === 0) {
|
|
2112
|
+
return end_writes(gobj, "");
|
|
2113
|
+
}
|
|
2114
|
+
let write = priv.save_queue[0];
|
|
2115
|
+
let command = (write.op === "create") ? "create-node"
|
|
2116
|
+
: (write.op === "delete") ? "delete-node" : "update-node";
|
|
2117
|
+
|
|
2118
|
+
if(remote_command(gobj, command, {
|
|
2119
|
+
topic_name: write.topic_name,
|
|
2120
|
+
record: write.record,
|
|
2121
|
+
options: {},
|
|
2122
|
+
record_id: write.record.id || ""
|
|
2123
|
+
}) < 0) {
|
|
2124
|
+
return end_writes(gobj, t("cannot reach the treedb"));
|
|
2125
|
+
}
|
|
2126
|
+
return 0;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/***************************************************************
|
|
2130
|
+
* The queue is done, or gave up. Either way the model is what
|
|
2131
|
+
* the answers made it, so the screen is redrawn from it and the
|
|
2132
|
+
* host is told once — the tab above owns the pending flag and
|
|
2133
|
+
* the Apply button, and a write per event would blink it.
|
|
2134
|
+
***************************************************************/
|
|
2135
|
+
function end_writes(gobj, error)
|
|
2136
|
+
{
|
|
2137
|
+
let priv = gobj.priv;
|
|
2138
|
+
|
|
2139
|
+
priv.save_queue = [];
|
|
2140
|
+
gobj_change_state(gobj, priv.save_return || "ST_TREEDBS");
|
|
2141
|
+
priv.save_return = "";
|
|
2142
|
+
show_notice(gobj, "");
|
|
2143
|
+
set_busy(gobj, false);
|
|
2144
|
+
|
|
2145
|
+
/* A position that arrived while the write was in flight: applied
|
|
2146
|
+
* now, so a Back pressed mid-save is not simply lost. */
|
|
2147
|
+
if(priv.pending_seg !== null) {
|
|
2148
|
+
let seg = priv.pending_seg;
|
|
2149
|
+
priv.pending_seg = null;
|
|
2150
|
+
if(!error) {
|
|
2151
|
+
apply_seg(gobj, seg);
|
|
2152
|
+
if(priv.save_wrote) {
|
|
2153
|
+
gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
|
|
2154
|
+
treedb_name: gobj_read_str_attr(gobj, "treedb_name")
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
priv.save_wrote = false;
|
|
2158
|
+
return 0;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
if(error) {
|
|
2163
|
+
yui_shell_show_error(yui_shell_of(gobj), error, {t: t});
|
|
2164
|
+
}
|
|
2165
|
+
if(priv.save_wrote) {
|
|
2166
|
+
gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
|
|
2167
|
+
treedb_name: gobj_read_str_attr(gobj, "treedb_name")
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
priv.save_wrote = false;
|
|
2171
|
+
render(gobj);
|
|
2172
|
+
publish_check(gobj);
|
|
2173
|
+
return error ? -1 : 0;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/***************************************************************
|
|
2177
|
+
* The record a column form produces.
|
|
2178
|
+
*
|
|
2179
|
+
* Everything the store needs and the operator did not type is
|
|
2180
|
+
* composed here: the fkey to the parent topic, the place among
|
|
2181
|
+
* its siblings, and the fields that are only meaningful when a
|
|
2182
|
+
* flag says so.
|
|
2183
|
+
***************************************************************/
|
|
2184
|
+
function column_record(gobj, topic, col, values)
|
|
2185
|
+
{
|
|
2186
|
+
let creating = !col;
|
|
2187
|
+
let flags = Array.isArray(values.flag) ? values.flag : [];
|
|
2188
|
+
let record = {};
|
|
2189
|
+
|
|
2190
|
+
if(creating) {
|
|
2191
|
+
record.value = String(values.value || "").trim();
|
|
2192
|
+
record.topics = [fkey_ref(T_TOPICS, topic.id, HOOK_COLS)];
|
|
2193
|
+
record.order = next_order(topic.cols);
|
|
2194
|
+
} else {
|
|
2195
|
+
record.id = col.id;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
record.header = values.header || "";
|
|
2199
|
+
record.type = values.type || "string";
|
|
2200
|
+
record.flag = flags;
|
|
2201
|
+
record.fillspace = values.fillspace === "" ? "" : Number(values.fillspace);
|
|
2202
|
+
record.placeholder = values.placeholder || "";
|
|
2203
|
+
record.description = values.description || "";
|
|
2204
|
+
record.default = values.default || "";
|
|
2205
|
+
|
|
2206
|
+
/* A hook is a mapping and the form asks for its two halves; an
|
|
2207
|
+
* incomplete pair is no mapping, not half of one. */
|
|
2208
|
+
if(values.hook_topic && values.hook_col) {
|
|
2209
|
+
record.hook = {[values.hook_topic]: values.hook_col};
|
|
2210
|
+
} else {
|
|
2211
|
+
record.hook = "";
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
let enum_lines = String(values.enum || "")
|
|
2215
|
+
.split("\n").map(x => x.trim()).filter(x => x.length > 0);
|
|
2216
|
+
record.enum = enum_lines.length > 0 ? enum_lines : "";
|
|
2217
|
+
|
|
2218
|
+
return record;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
/***************************************************************
|
|
2222
|
+
* The record a topic form produces.
|
|
2223
|
+
***************************************************************/
|
|
2224
|
+
function topic_record(gobj, treedb, topic, values)
|
|
2225
|
+
{
|
|
2226
|
+
let creating = !topic;
|
|
2227
|
+
let record = {};
|
|
2228
|
+
|
|
2229
|
+
if(creating) {
|
|
2230
|
+
record.value = String(values.value || "").trim();
|
|
2231
|
+
record.treedbs = [fkey_ref(T_TREEDBS, treedb.id, HOOK_TOPICS)];
|
|
2232
|
+
record.order = next_order(treedb.topics);
|
|
2233
|
+
} else {
|
|
2234
|
+
record.id = topic.id;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
record.pkey = values.pkey || "id";
|
|
2238
|
+
record.pkey2s = values.pkey2s || "";
|
|
2239
|
+
record.system_flag = values.system_flag || "sf_string_key";
|
|
2240
|
+
record.tkey = values.tkey || "";
|
|
2241
|
+
record.topic_version = values.topic_version === "" ? 1 : Number(values.topic_version);
|
|
2242
|
+
record.system_topic = !!values.system_topic;
|
|
2243
|
+
|
|
2244
|
+
return record;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
/***************************
|
|
2251
|
+
* Positions
|
|
2252
|
+
***************************/
|
|
2253
|
+
|
|
2254
|
+
|
|
2255
|
+
|
|
2256
|
+
|
|
2257
|
+
/***************************************************************
|
|
2258
|
+
* Put the position in the model's terms and in the url's.
|
|
2259
|
+
*
|
|
2260
|
+
* The state IS the screen, so moving is a state change and
|
|
2261
|
+
* nothing else decides what is drawn. The url is mirrored
|
|
2262
|
+
* outwards — this view navigates nothing itself, the host owns
|
|
2263
|
+
* the route it hangs from (ROUTING.md).
|
|
2264
|
+
***************************************************************/
|
|
2265
|
+
function go(gobj, treedb_id, topic_name, diagram, mirror)
|
|
2266
|
+
{
|
|
2267
|
+
let priv = gobj.priv;
|
|
2268
|
+
|
|
2269
|
+
priv.treedb_id = treedb_id || "";
|
|
2270
|
+
priv.topic_name = topic_name || "";
|
|
2271
|
+
priv.diagram = !!diagram;
|
|
2272
|
+
|
|
2273
|
+
let state = "ST_TREEDBS";
|
|
2274
|
+
if(!priv.model) {
|
|
2275
|
+
state = "ST_LOADING";
|
|
2276
|
+
} else if(priv.model.treedbs.length === 0) {
|
|
2277
|
+
state = "ST_EMPTY";
|
|
2278
|
+
} else if(priv.treedb_id && priv.diagram) {
|
|
2279
|
+
state = "ST_DIAGRAM";
|
|
2280
|
+
} else if(priv.treedb_id && priv.topic_name) {
|
|
2281
|
+
state = "ST_COLUMNS";
|
|
2282
|
+
} else if(priv.treedb_id) {
|
|
2283
|
+
state = "ST_TOPICS";
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
gobj_change_state(gobj, state);
|
|
2287
|
+
render(gobj);
|
|
2288
|
+
publish_check(gobj);
|
|
2289
|
+
|
|
2290
|
+
if(mirror !== false) {
|
|
2291
|
+
gobj_publish_event(gobj, "EV_POSITION_CHANGED", {subpath: position_seg(gobj)});
|
|
2292
|
+
}
|
|
2293
|
+
return 0;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/***************************************************************
|
|
2297
|
+
* The tail of the url this view owns.
|
|
2298
|
+
***************************************************************/
|
|
2299
|
+
function position_seg(gobj)
|
|
2300
|
+
{
|
|
2301
|
+
let priv = gobj.priv;
|
|
2302
|
+
|
|
2303
|
+
if(!priv.treedb_id) {
|
|
2304
|
+
return "";
|
|
2305
|
+
}
|
|
2306
|
+
if(priv.diagram) {
|
|
2307
|
+
return `${priv.treedb_id}/${DIAGRAM_SEG}`;
|
|
2308
|
+
}
|
|
2309
|
+
if(priv.topic_name) {
|
|
2310
|
+
return `${priv.treedb_id}/${priv.topic_name}`;
|
|
2311
|
+
}
|
|
2312
|
+
return priv.treedb_id;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
/***************************************************************
|
|
2316
|
+
* ...and the same in reverse. A tail naming a treedb or a topic
|
|
2317
|
+
* the store does not have lands one level up rather than on an
|
|
2318
|
+
* empty screen: a shared link outlives the schema it points at.
|
|
2319
|
+
***************************************************************/
|
|
2320
|
+
function apply_seg(gobj, seg)
|
|
2321
|
+
{
|
|
2322
|
+
let parts = String(seg || "").split("/").filter(x => x.length > 0);
|
|
2323
|
+
let treedb_id = parts[0] || "";
|
|
2324
|
+
let tail = parts[1] || "";
|
|
2325
|
+
|
|
2326
|
+
if(!treedb_id) {
|
|
2327
|
+
return go(gobj, "", "", false, false);
|
|
2328
|
+
}
|
|
2329
|
+
if(tail === DIAGRAM_SEG) {
|
|
2330
|
+
return go(gobj, treedb_id, "", true, false);
|
|
2331
|
+
}
|
|
2332
|
+
return go(gobj, treedb_id, tail, false, false);
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
|
|
2336
|
+
|
|
2337
|
+
|
|
2338
|
+
/***************************
|
|
2339
|
+
* Actions
|
|
2340
|
+
***************************/
|
|
2341
|
+
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
/***************************************************************
|
|
2346
|
+
* An answer from the treedb.
|
|
2347
|
+
*
|
|
2348
|
+
* Which request it answers is read from the command stack, not
|
|
2349
|
+
* guessed: three of them are `nodes` and only the echoed
|
|
2350
|
+
* `topic_name` tells them apart.
|
|
2351
|
+
***************************************************************/
|
|
2352
|
+
function ac_mt_command_answer(gobj, event, kw, src)
|
|
2353
|
+
{
|
|
2354
|
+
let priv = gobj.priv;
|
|
2355
|
+
let result = kw.result;
|
|
2356
|
+
let comment = kw.comment;
|
|
2357
|
+
let data = kw.data;
|
|
2358
|
+
|
|
2359
|
+
let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
|
|
2360
|
+
let command = kw_get_str(gobj, __command__, "command", "", 0);
|
|
2361
|
+
let kw_command = kw_get_dict(gobj, __command__, "kw", {}, 0);
|
|
2362
|
+
let md = kw_get_dict(gobj, kw_command, "__md_command__", {}, 0);
|
|
2363
|
+
|
|
2364
|
+
if(kw_get_str(gobj, md, "purpose", "", 0) !== PURPOSE) {
|
|
2365
|
+
return 0; /* another panel's answer on a shared transport */
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
if(command === "nodes") {
|
|
2369
|
+
let topic_name = kw_get_str(gobj, md, "topic_name", "", 0);
|
|
2370
|
+
priv.pending--;
|
|
2371
|
+
if(result < 0) {
|
|
2372
|
+
priv.load_error = comment || t("cannot load the schemas");
|
|
2373
|
+
} else if(priv.records && Array.isArray(priv.records[topic_name])) {
|
|
2374
|
+
priv.records[topic_name] = Array.isArray(data) ? data : [];
|
|
2375
|
+
}
|
|
2376
|
+
if(priv.pending > 0) {
|
|
2377
|
+
return 0;
|
|
2378
|
+
}
|
|
2379
|
+
if(priv.load_error) {
|
|
2380
|
+
priv.model = null;
|
|
2381
|
+
gobj_change_state(gobj, "ST_IDLE");
|
|
2382
|
+
render(gobj);
|
|
2383
|
+
return 0;
|
|
2384
|
+
}
|
|
2385
|
+
build_model(gobj);
|
|
2386
|
+
/* Back to where the operator was: the load may be a refresh, or
|
|
2387
|
+
* the answer to a deep link that arrived before the model. */
|
|
2388
|
+
return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
if(command === "create-node" || command === "update-node" || command === "delete-node") {
|
|
2392
|
+
let topic_name = kw_get_str(gobj, kw_command, "topic_name", "", 0);
|
|
2393
|
+
if(result < 0) {
|
|
2394
|
+
return end_writes(gobj, comment || t("the treedb refused the write"));
|
|
2395
|
+
}
|
|
2396
|
+
priv.save_wrote = true;
|
|
2397
|
+
if(command === "delete-node") {
|
|
2398
|
+
forget_record(gobj, topic_name,
|
|
2399
|
+
kw_get_str(gobj, kw_command, "record_id", "", 0));
|
|
2400
|
+
} else {
|
|
2401
|
+
let record = is_object(data) ? data
|
|
2402
|
+
: (Array.isArray(data) && is_object(data[0]) ? data[0] : null);
|
|
2403
|
+
if(record) {
|
|
2404
|
+
patch_record(gobj, topic_name, record);
|
|
2405
|
+
} else {
|
|
2406
|
+
/* The treedb wrote it and did not describe it back: ask
|
|
2407
|
+
* again rather than draw a model that has drifted. */
|
|
2408
|
+
log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record`);
|
|
2409
|
+
priv.save_queue = [];
|
|
2410
|
+
request_model(gobj);
|
|
2411
|
+
return 0;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
if(topic_name === T_TOPICS || topic_name === T_COLS) {
|
|
2415
|
+
let touched = kw_get_dict(gobj, kw_command, "record", {}, 0);
|
|
2416
|
+
mark_written(gobj, topic_name, touched);
|
|
2417
|
+
}
|
|
2418
|
+
priv.save_queue.shift();
|
|
2419
|
+
return run_next_write(gobj);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
log_error(`${gobj_short_name(gobj)}: unknown command answered: ${command}`);
|
|
2423
|
+
return 0;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
/***************************************************************
|
|
2427
|
+
* Remember which topic this write belongs to, which is what
|
|
2428
|
+
* "its version still has to move" is asked about.
|
|
2429
|
+
***************************************************************/
|
|
2430
|
+
function mark_written(gobj, topic_name, record)
|
|
2431
|
+
{
|
|
2432
|
+
let priv = gobj.priv;
|
|
2433
|
+
let topic = current_topic(gobj);
|
|
2434
|
+
|
|
2435
|
+
if(topic_name === T_TOPICS && record && record.id) {
|
|
2436
|
+
priv.written[record.id] = true;
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
if(topic) {
|
|
2440
|
+
priv.written[topic.id] = true;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
/***************************************************************
|
|
2445
|
+
* The host says where we are (ROUTING.md §5): everything under
|
|
2446
|
+
* this view's route is ours.
|
|
2447
|
+
***************************************************************/
|
|
2448
|
+
function ac_show(gobj, event, kw, src)
|
|
2449
|
+
{
|
|
2450
|
+
let priv = gobj.priv;
|
|
2451
|
+
let seg = (kw && kw.subpath) || "";
|
|
2452
|
+
|
|
2453
|
+
if(gobj_current_state(gobj) === "ST_SAVING") {
|
|
2454
|
+
priv.pending_seg = seg;
|
|
2455
|
+
return 0; /* applied when the write lands (end_writes) */
|
|
2456
|
+
}
|
|
2457
|
+
return apply_seg(gobj, seg);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/***************************************************************
|
|
2461
|
+
* Hidden by the host: drop the overlays. A dialog outlives the
|
|
2462
|
+
* screen it was opened from otherwise, and answers into it land
|
|
2463
|
+
* nowhere.
|
|
2464
|
+
***************************************************************/
|
|
2465
|
+
function ac_hide(gobj, event, kw, src)
|
|
2466
|
+
{
|
|
2467
|
+
close_dialog(gobj);
|
|
2468
|
+
return 0;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/***************************************************************
|
|
2472
|
+
* The session came up or went down. The model stays either way
|
|
2473
|
+
* — a schema does not change under you — but a first load may
|
|
2474
|
+
* have had no transport to run on.
|
|
2475
|
+
***************************************************************/
|
|
2476
|
+
function ac_transport_state(gobj, event, kw, src)
|
|
2477
|
+
{
|
|
2478
|
+
let priv = gobj.priv;
|
|
2479
|
+
|
|
2480
|
+
priv.connected = !!(kw && kw.connected);
|
|
2481
|
+
if(priv.connected && !priv.model && gobj_current_state(gobj) !== "ST_LOADING") {
|
|
2482
|
+
return request_model(gobj);
|
|
2483
|
+
}
|
|
2484
|
+
render_toolbar(gobj);
|
|
2485
|
+
return 0;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
/***************************************************************
|
|
2489
|
+
* The shell switched language. What this view draws is drawn by
|
|
2490
|
+
* itself, so it redraws: an i18n attribute reaches the chrome,
|
|
2491
|
+
* never a row this gclass built.
|
|
2492
|
+
***************************************************************/
|
|
2493
|
+
function ac_language_changed(gobj, event, kw, src)
|
|
2494
|
+
{
|
|
2495
|
+
render(gobj);
|
|
2496
|
+
return 0;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
function ac_refresh(gobj, event, kw, src)
|
|
2500
|
+
{
|
|
2501
|
+
return request_model(gobj);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
/***************************************************************
|
|
2505
|
+
* Moving about.
|
|
2506
|
+
***************************************************************/
|
|
2507
|
+
function ac_select_treedb(gobj, event, kw, src)
|
|
2508
|
+
{
|
|
2509
|
+
return go(gobj, kw.treedb, "", false, true);
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function ac_select_topic(gobj, event, kw, src)
|
|
2513
|
+
{
|
|
2514
|
+
return go(gobj, gobj.priv.treedb_id, kw.topic, false, true);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
function ac_back(gobj, event, kw, src)
|
|
2518
|
+
{
|
|
2519
|
+
let priv = gobj.priv;
|
|
2520
|
+
|
|
2521
|
+
if(priv.diagram) {
|
|
2522
|
+
return go(gobj, priv.treedb_id, "", false, true);
|
|
2523
|
+
}
|
|
2524
|
+
if(priv.topic_name) {
|
|
2525
|
+
return go(gobj, priv.treedb_id, "", false, true);
|
|
2526
|
+
}
|
|
2527
|
+
return go(gobj, "", "", false, true);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
function ac_toggle_diagram(gobj, event, kw, src)
|
|
2531
|
+
{
|
|
2532
|
+
let priv = gobj.priv;
|
|
2533
|
+
|
|
2534
|
+
return go(gobj, priv.treedb_id, priv.topic_name, !priv.diagram, true);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
/***************************************************************
|
|
2538
|
+
* A topic clicked in the drawing. The diagram is a CHILD here
|
|
2539
|
+
* and publishes to us, so the event is declared in this FSM —
|
|
2540
|
+
* and it means the same thing a click on a row means.
|
|
2541
|
+
***************************************************************/
|
|
2542
|
+
function ac_node_click(gobj, event, kw, src)
|
|
2543
|
+
{
|
|
2544
|
+
let topic = kw && kw.topic;
|
|
2545
|
+
|
|
2546
|
+
if(!topic) {
|
|
2547
|
+
return 0;
|
|
2548
|
+
}
|
|
2549
|
+
return go(gobj, gobj.priv.treedb_id, topic, false, true);
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
function ac_show_orphans(gobj, event, kw, src)
|
|
2553
|
+
{
|
|
2554
|
+
open_orphans(gobj);
|
|
2555
|
+
return 0;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
/***************************************************************
|
|
2559
|
+
* Ask, and turn the answer into an event.
|
|
2560
|
+
*
|
|
2561
|
+
* `kw` carries an IDENTITY and never an object: the machine
|
|
2562
|
+
* trace dumps it, and a gobj or a DOM node in there is a
|
|
2563
|
+
* circular structure that breaks the very trace the FSM exists
|
|
2564
|
+
* to feed.
|
|
2565
|
+
***************************************************************/
|
|
2566
|
+
function confirm_then(gobj, message, detail, kw)
|
|
2567
|
+
{
|
|
2568
|
+
yui_shell_confirm_danger(yui_shell_of(gobj), message, {
|
|
2569
|
+
t: t,
|
|
2570
|
+
detail: detail
|
|
2571
|
+
}).then((yes) => {
|
|
2572
|
+
if(!yes || gobj_is_destroying(gobj)) {
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
gobj_send_event(gobj, "EV_CONFIRMED", kw, gobj);
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
/***************************************************************
|
|
2580
|
+
* Editing a column.
|
|
2581
|
+
***************************************************************/
|
|
2582
|
+
function ac_add_column(gobj, event, kw, src)
|
|
2583
|
+
{
|
|
2584
|
+
let topic = current_topic(gobj);
|
|
2585
|
+
|
|
2586
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2587
|
+
return -1;
|
|
2588
|
+
}
|
|
2589
|
+
if(!topic) {
|
|
2590
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no topic open`);
|
|
2591
|
+
return -1;
|
|
2592
|
+
}
|
|
2593
|
+
open_column_form(gobj, topic, null);
|
|
2594
|
+
return 0;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
function ac_edit_column(gobj, event, kw, src)
|
|
2598
|
+
{
|
|
2599
|
+
let priv = gobj.priv;
|
|
2600
|
+
let topic = current_topic(gobj);
|
|
2601
|
+
let col = topic ? find_col(priv.model, priv.treedb_id, topic.name, kw.col) : null;
|
|
2602
|
+
|
|
2603
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2604
|
+
return -1;
|
|
2605
|
+
}
|
|
2606
|
+
if(!col) {
|
|
2607
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.col}`);
|
|
2608
|
+
return -1;
|
|
2609
|
+
}
|
|
2610
|
+
open_column_form(gobj, topic, col);
|
|
2611
|
+
return 0;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
/***************************************************************
|
|
2615
|
+
* Duplicating a column: the whole definition, a name that says
|
|
2616
|
+
* it is a copy, and the form open on it. A schema is written by
|
|
2617
|
+
* variations on a column far more often than from nothing.
|
|
2618
|
+
***************************************************************/
|
|
2619
|
+
function ac_duplicate_column(gobj, event, kw, src)
|
|
2620
|
+
{
|
|
2621
|
+
let priv = gobj.priv;
|
|
2622
|
+
let topic = current_topic(gobj);
|
|
2623
|
+
let col = topic ? find_col(priv.model, priv.treedb_id, topic.name, kw.col) : null;
|
|
2624
|
+
|
|
2625
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2626
|
+
return -1;
|
|
2627
|
+
}
|
|
2628
|
+
if(!col) {
|
|
2629
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.col}`);
|
|
2630
|
+
return -1;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
let prefill = Object.assign({}, col.record);
|
|
2634
|
+
delete prefill.id;
|
|
2635
|
+
delete prefill.value;
|
|
2636
|
+
delete prefill.order;
|
|
2637
|
+
delete prefill.topics;
|
|
2638
|
+
prefill.__name__ = `${col.name}_copy`;
|
|
2639
|
+
|
|
2640
|
+
open_column_form(gobj, topic, null, prefill);
|
|
2641
|
+
return 0;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
function ac_save_column(gobj, event, kw, src)
|
|
2645
|
+
{
|
|
2646
|
+
let priv = gobj.priv;
|
|
2647
|
+
let topic = find_topic(priv.model, priv.treedb_id, kw.topic);
|
|
2648
|
+
let col = kw.creating ? null : find_col(priv.model, priv.treedb_id, kw.topic, kw.col);
|
|
2649
|
+
|
|
2650
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2651
|
+
return -1;
|
|
2652
|
+
}
|
|
2653
|
+
if(!topic || (!kw.creating && !col)) {
|
|
2654
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.topic}.${kw.col}`);
|
|
2655
|
+
return -1;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
let values = kw.values || {};
|
|
2659
|
+
if(kw.creating && empty_string(String(values.value || "").trim())) {
|
|
2660
|
+
yui_shell_show_error(yui_shell_of(gobj), "a column needs a name", {t: t});
|
|
2661
|
+
return -1;
|
|
2662
|
+
}
|
|
2663
|
+
if(kw.creating && find_col(priv.model, priv.treedb_id, kw.topic, String(values.value).trim())) {
|
|
2664
|
+
yui_shell_show_error(yui_shell_of(gobj), "that column is already there", {t: t});
|
|
2665
|
+
return -1;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
close_dialog(gobj);
|
|
2669
|
+
return queue_writes(gobj, [{
|
|
2670
|
+
op: kw.creating ? "create" : "update",
|
|
2671
|
+
topic_name: T_COLS,
|
|
2672
|
+
record: column_record(gobj, topic, col, values)
|
|
2673
|
+
}].concat(version_writes(gobj, topic)));
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
function ac_delete_column(gobj, event, kw, src)
|
|
2677
|
+
{
|
|
2678
|
+
let priv = gobj.priv;
|
|
2679
|
+
let topic = current_topic(gobj);
|
|
2680
|
+
let col = topic ? find_col(priv.model, priv.treedb_id, topic.name, kw.col) : null;
|
|
2681
|
+
|
|
2682
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2683
|
+
return -1;
|
|
2684
|
+
}
|
|
2685
|
+
if(!col) {
|
|
2686
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.col}`);
|
|
2687
|
+
return -1;
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
confirm_then(gobj, "delete this column?", `${topic.name}.${col.name}`,
|
|
2691
|
+
{what: "column", topic: topic.name, col: col.name});
|
|
2692
|
+
return 0;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
/***************************************************************
|
|
2696
|
+
* Reordering. `order` is a field, so a drag is a write — of the
|
|
2697
|
+
* rows whose place actually changed, which in a forty-column
|
|
2698
|
+
* topic is two or three of them.
|
|
2699
|
+
***************************************************************/
|
|
2700
|
+
function ac_move_column(gobj, event, kw, src)
|
|
2701
|
+
{
|
|
2702
|
+
let topic = current_topic(gobj);
|
|
2703
|
+
|
|
2704
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2705
|
+
return -1;
|
|
2706
|
+
}
|
|
2707
|
+
if(!topic) {
|
|
2708
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no topic open`);
|
|
2709
|
+
return -1;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
let from = kw.from;
|
|
2713
|
+
let to = kw.to;
|
|
2714
|
+
let list = topic.cols.slice();
|
|
2715
|
+
if(from < 0 || from >= list.length || to < 0 || to > list.length) {
|
|
2716
|
+
log_error(`${gobj_short_name(gobj)}: ${event} out of range: ${from} -> ${to}`);
|
|
2717
|
+
return -1;
|
|
2718
|
+
}
|
|
2719
|
+
let moved = list.splice(from, 1)[0];
|
|
2720
|
+
list.splice(from < to ? to - 1 : to, 0, moved);
|
|
2721
|
+
|
|
2722
|
+
let writes = moved_orders(list).map((order) => {
|
|
2723
|
+
return {op: "update", topic_name: T_COLS,
|
|
2724
|
+
record: {id: order.id, order: order.order}};
|
|
2725
|
+
});
|
|
2726
|
+
if(writes.length === 0) {
|
|
2727
|
+
return 0; /* dropped where it already was */
|
|
2728
|
+
}
|
|
2729
|
+
return queue_writes(gobj, writes.concat(version_writes(gobj, topic)));
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
/***************************************************************
|
|
2733
|
+
* Editing a topic.
|
|
2734
|
+
***************************************************************/
|
|
2735
|
+
function ac_add_topic(gobj, event, kw, src)
|
|
2736
|
+
{
|
|
2737
|
+
let treedb = current_treedb(gobj);
|
|
2738
|
+
|
|
2739
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2740
|
+
return -1;
|
|
2741
|
+
}
|
|
2742
|
+
if(!treedb) {
|
|
2743
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
|
|
2744
|
+
return -1;
|
|
2745
|
+
}
|
|
2746
|
+
open_topic_form(gobj, treedb, null);
|
|
2747
|
+
return 0;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
function ac_edit_topic(gobj, event, kw, src)
|
|
2751
|
+
{
|
|
2752
|
+
let priv = gobj.priv;
|
|
2753
|
+
let treedb = current_treedb(gobj);
|
|
2754
|
+
let topic = treedb ? find_topic(priv.model, priv.treedb_id, kw.topic) : null;
|
|
2755
|
+
|
|
2756
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2757
|
+
return -1;
|
|
2758
|
+
}
|
|
2759
|
+
if(!topic) {
|
|
2760
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
|
|
2761
|
+
return -1;
|
|
2762
|
+
}
|
|
2763
|
+
open_topic_form(gobj, treedb, topic);
|
|
2764
|
+
return 0;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
function ac_save_topic(gobj, event, kw, src)
|
|
2768
|
+
{
|
|
2769
|
+
let priv = gobj.priv;
|
|
2770
|
+
let treedb = current_treedb(gobj);
|
|
2771
|
+
let topic = kw.creating ? null : find_topic(priv.model, priv.treedb_id, kw.topic);
|
|
2772
|
+
|
|
2773
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2774
|
+
return -1;
|
|
2775
|
+
}
|
|
2776
|
+
if(!treedb || (!kw.creating && !topic)) {
|
|
2777
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
|
|
2778
|
+
return -1;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
let values = kw.values || {};
|
|
2782
|
+
if(kw.creating) {
|
|
2783
|
+
let name = String(values.value || "").trim();
|
|
2784
|
+
if(empty_string(name)) {
|
|
2785
|
+
yui_shell_show_error(yui_shell_of(gobj), "a topic needs a name", {t: t});
|
|
2786
|
+
return -1;
|
|
2787
|
+
}
|
|
2788
|
+
if(find_topic(priv.model, priv.treedb_id, name)) {
|
|
2789
|
+
yui_shell_show_error(yui_shell_of(gobj), "that topic is already there", {t: t});
|
|
2790
|
+
return -1;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
close_dialog(gobj);
|
|
2795
|
+
/* The topic record carries the version the form asked for, so it is
|
|
2796
|
+
* not raised again here — only the treedb's. */
|
|
2797
|
+
return queue_writes(gobj, [{
|
|
2798
|
+
op: kw.creating ? "create" : "update",
|
|
2799
|
+
topic_name: T_TOPICS,
|
|
2800
|
+
record: topic_record(gobj, treedb, topic, values)
|
|
2801
|
+
}].concat(version_writes(gobj, null)));
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
function ac_delete_topic(gobj, event, kw, src)
|
|
2805
|
+
{
|
|
2806
|
+
let priv = gobj.priv;
|
|
2807
|
+
let topic = find_topic(priv.model, priv.treedb_id, kw.topic);
|
|
2808
|
+
|
|
2809
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2810
|
+
return -1;
|
|
2811
|
+
}
|
|
2812
|
+
if(!topic) {
|
|
2813
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
|
|
2814
|
+
return -1;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
confirm_then(gobj, "delete this topic and its columns?",
|
|
2818
|
+
`${topic.name} (${topic.cols.length})`, {what: "topic", topic: topic.name});
|
|
2819
|
+
return 0;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
/***************************************************************
|
|
2823
|
+
* Raising a version by hand: what the banner offers when
|
|
2824
|
+
* something else wrote the topic and left its version alone.
|
|
2825
|
+
***************************************************************/
|
|
2826
|
+
function ac_bump_version(gobj, event, kw, src)
|
|
2827
|
+
{
|
|
2828
|
+
let priv = gobj.priv;
|
|
2829
|
+
let topic = find_topic(priv.model, priv.treedb_id, kw.topic);
|
|
2830
|
+
|
|
2831
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2832
|
+
return -1;
|
|
2833
|
+
}
|
|
2834
|
+
if(!topic) {
|
|
2835
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
|
|
2836
|
+
return -1;
|
|
2837
|
+
}
|
|
2838
|
+
return queue_writes(gobj, version_writes(gobj, topic));
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
/***************************************************************
|
|
2842
|
+
* Deleting a leftover.
|
|
2843
|
+
***************************************************************/
|
|
2844
|
+
function ac_delete_orphan(gobj, event, kw, src)
|
|
2845
|
+
{
|
|
2846
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2847
|
+
return -1;
|
|
2848
|
+
}
|
|
2849
|
+
if(empty_string(kw.id)) {
|
|
2850
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names nothing`);
|
|
2851
|
+
return -1;
|
|
2852
|
+
}
|
|
2853
|
+
close_dialog(gobj);
|
|
2854
|
+
return queue_writes(gobj, [{
|
|
2855
|
+
op: "delete",
|
|
2856
|
+
topic_name: kw.kind === "topic" ? T_TOPICS : T_COLS,
|
|
2857
|
+
record: {id: kw.id}
|
|
2858
|
+
}]);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
/***************************************************************
|
|
2862
|
+
* Checking, exporting, importing.
|
|
2863
|
+
***************************************************************/
|
|
2864
|
+
function ac_validate(gobj, event, kw, src)
|
|
2865
|
+
{
|
|
2866
|
+
let priv = gobj.priv;
|
|
2867
|
+
let treedb = current_treedb(gobj);
|
|
2868
|
+
|
|
2869
|
+
if(!treedb) {
|
|
2870
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
|
|
2871
|
+
return -1;
|
|
2872
|
+
}
|
|
2873
|
+
let findings = validate_schema(treedb, {
|
|
2874
|
+
written_topics: Object.keys(priv.written),
|
|
2875
|
+
baseline: priv.baseline
|
|
2876
|
+
});
|
|
2877
|
+
open_report(gobj, findings, treedb.id);
|
|
2878
|
+
if(has_errors(findings)) {
|
|
2879
|
+
log_warning(`${gobj_short_name(gobj)}: ${treedb.id} would be refused as it is`);
|
|
2880
|
+
}
|
|
2881
|
+
return 0;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
function ac_export(gobj, event, kw, src)
|
|
2885
|
+
{
|
|
2886
|
+
let treedb = current_treedb(gobj);
|
|
2887
|
+
|
|
2888
|
+
if(!treedb) {
|
|
2889
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
|
|
2890
|
+
return -1;
|
|
2891
|
+
}
|
|
2892
|
+
open_export(gobj, treedb);
|
|
2893
|
+
return 0;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
function ac_import(gobj, event, kw, src)
|
|
2897
|
+
{
|
|
2898
|
+
let treedb = current_treedb(gobj);
|
|
2899
|
+
|
|
2900
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2901
|
+
return -1;
|
|
2902
|
+
}
|
|
2903
|
+
if(!treedb) {
|
|
2904
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
|
|
2905
|
+
return -1;
|
|
2906
|
+
}
|
|
2907
|
+
open_import(gobj, treedb);
|
|
2908
|
+
return 0;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
/***************************************************************
|
|
2912
|
+
* The paste, read and planned. Nothing is written here: what is
|
|
2913
|
+
* shown is the plan, and the plan is what the next event runs.
|
|
2914
|
+
***************************************************************/
|
|
2915
|
+
function ac_preview_import(gobj, event, kw, src)
|
|
2916
|
+
{
|
|
2917
|
+
let priv = gobj.priv;
|
|
2918
|
+
let treedb = current_treedb(gobj);
|
|
2919
|
+
let incoming = null;
|
|
2920
|
+
|
|
2921
|
+
if(!treedb) {
|
|
2922
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no treedb open`);
|
|
2923
|
+
return -1;
|
|
2924
|
+
}
|
|
2925
|
+
try {
|
|
2926
|
+
incoming = JSON.parse(String(kw.text || ""));
|
|
2927
|
+
} catch(e) {
|
|
2928
|
+
priv.import_plan = null;
|
|
2929
|
+
render_import_plan(gobj, {writes: [], summary: {},
|
|
2930
|
+
conflicts: [{code: "that is not JSON", topic: "", col: ""}]});
|
|
2931
|
+
return -1;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
let plan = plan_import(treedb, incoming, {prune: !!kw.prune});
|
|
2935
|
+
priv.import_plan = plan;
|
|
2936
|
+
render_import_plan(gobj, plan);
|
|
2937
|
+
return 0;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
function ac_apply_import(gobj, event, kw, src)
|
|
2941
|
+
{
|
|
2942
|
+
let priv = gobj.priv;
|
|
2943
|
+
let plan = priv.import_plan;
|
|
2944
|
+
|
|
2945
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2946
|
+
return -1;
|
|
2947
|
+
}
|
|
2948
|
+
if(!plan || plan.writes.length === 0) {
|
|
2949
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no plan to run`);
|
|
2950
|
+
return -1;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
let deletes = plan_deletes(plan);
|
|
2954
|
+
if(deletes.length === 0) {
|
|
2955
|
+
return run_import(gobj);
|
|
2956
|
+
}
|
|
2957
|
+
confirm_then(gobj, "this import deletes part of the schema", `${deletes.length}`,
|
|
2958
|
+
{what: "import"});
|
|
2959
|
+
return 0;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
/***************************************************************
|
|
2963
|
+
* Run the plan the dialog showed. Reached only through
|
|
2964
|
+
* EV_CONFIRMED or from an import with nothing to delete.
|
|
2965
|
+
***************************************************************/
|
|
2966
|
+
function run_import(gobj)
|
|
2967
|
+
{
|
|
2968
|
+
let priv = gobj.priv;
|
|
2969
|
+
let plan = priv.import_plan;
|
|
2970
|
+
|
|
2971
|
+
if(!plan) {
|
|
2972
|
+
log_error(`${gobj_short_name(gobj)}: the import plan is gone`);
|
|
2973
|
+
return -1;
|
|
2974
|
+
}
|
|
2975
|
+
close_dialog(gobj);
|
|
2976
|
+
priv.import_plan = null;
|
|
2977
|
+
return queue_writes(gobj, plan.writes.map((write) => {
|
|
2978
|
+
return {op: write.op, topic_name: write.topic_name, record: write.record};
|
|
2979
|
+
}));
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
/***************************************************************
|
|
2983
|
+
* A confirmation the operator gave.
|
|
2984
|
+
*
|
|
2985
|
+
* The dialog's answer comes back as an EVENT and not as a
|
|
2986
|
+
* continuation: a deletion decided in a modal is an action like
|
|
2987
|
+
* any other, and an action that never crosses the machine
|
|
2988
|
+
* leaves the `machine` trace saying nothing about the one thing
|
|
2989
|
+
* in this view that destroys something.
|
|
2990
|
+
***************************************************************/
|
|
2991
|
+
function ac_confirmed(gobj, event, kw, src)
|
|
2992
|
+
{
|
|
2993
|
+
let priv = gobj.priv;
|
|
2994
|
+
|
|
2995
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2996
|
+
return -1;
|
|
2997
|
+
}
|
|
2998
|
+
if(kw.what === "import") {
|
|
2999
|
+
return run_import(gobj);
|
|
3000
|
+
}
|
|
3001
|
+
if(kw.what === "topic") {
|
|
3002
|
+
let topic = find_topic(priv.model, priv.treedb_id, kw.topic);
|
|
3003
|
+
if(!topic) {
|
|
3004
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
|
|
3005
|
+
return -1;
|
|
3006
|
+
}
|
|
3007
|
+
/* The columns first: a topic deleted with children behind it
|
|
3008
|
+
* leaves them belonging to nothing. */
|
|
3009
|
+
let writes = topic.cols.map((col) => {
|
|
3010
|
+
return {op: "delete", topic_name: T_COLS, record: {id: col.id}};
|
|
3011
|
+
});
|
|
3012
|
+
writes.push({op: "delete", topic_name: T_TOPICS, record: {id: topic.id}});
|
|
3013
|
+
return queue_writes(gobj, writes.concat(version_writes(gobj, null)));
|
|
3014
|
+
}
|
|
3015
|
+
if(kw.what === "column") {
|
|
3016
|
+
let topic = find_topic(priv.model, priv.treedb_id, kw.topic);
|
|
3017
|
+
let col = topic ? find_col(priv.model, priv.treedb_id, kw.topic, kw.col) : null;
|
|
3018
|
+
if(!col) {
|
|
3019
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no column: ${kw.col}`);
|
|
3020
|
+
return -1;
|
|
3021
|
+
}
|
|
3022
|
+
return queue_writes(gobj, [{
|
|
3023
|
+
op: "delete", topic_name: T_COLS, record: {id: col.id}
|
|
3024
|
+
}].concat(version_writes(gobj, topic)));
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
log_error(`${gobj_short_name(gobj)}: ${event} of nothing this view asked about: ${kw.what}`);
|
|
3028
|
+
return -1;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
|
|
3032
|
+
|
|
3033
|
+
|
|
3034
|
+
/***************************************************************
|
|
3035
|
+
* FSM
|
|
3036
|
+
***************************************************************/
|
|
3037
|
+
/*---------------------------------------------*
|
|
3038
|
+
* Global methods table
|
|
3039
|
+
*---------------------------------------------*/
|
|
3040
|
+
const gmt = {
|
|
3041
|
+
mt_create: mt_create,
|
|
3042
|
+
mt_start: mt_start,
|
|
3043
|
+
mt_stop: mt_stop,
|
|
3044
|
+
mt_destroy: mt_destroy
|
|
3045
|
+
};
|
|
3046
|
+
|
|
3047
|
+
/***************************************************************
|
|
3048
|
+
* Create the GClass
|
|
3049
|
+
***************************************************************/
|
|
3050
|
+
function create_gclass(gclass_name)
|
|
3051
|
+
{
|
|
3052
|
+
if(__gclass__) {
|
|
3053
|
+
log_error(`GClass ALREADY created: ${gclass_name}`);
|
|
3054
|
+
return -1;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
/* What every screen answers: where the host says we are, whether
|
|
3058
|
+
* the session is up, the language, and the treedb's answers. */
|
|
3059
|
+
const COMMON = [
|
|
3060
|
+
["EV_MT_COMMAND_ANSWER", ac_mt_command_answer, null],
|
|
3061
|
+
["EV_SHOW", ac_show, null],
|
|
3062
|
+
["EV_HIDE", ac_hide, null],
|
|
3063
|
+
["EV_TRANSPORT_STATE", ac_transport_state, null],
|
|
3064
|
+
["EV_LANGUAGE_CHANGED", ac_language_changed, null],
|
|
3065
|
+
["EV_REFRESH", ac_refresh, null]
|
|
3066
|
+
];
|
|
3067
|
+
|
|
3068
|
+
/* What a treedb is open for: reading it whole, and writing it
|
|
3069
|
+
* back. Legal wherever a treedb is open, which is every screen
|
|
3070
|
+
* below the first one. */
|
|
3071
|
+
const TREEDB_WIDE = [
|
|
3072
|
+
["EV_BACK", ac_back, null],
|
|
3073
|
+
["EV_TOGGLE_DIAGRAM", ac_toggle_diagram, null],
|
|
3074
|
+
["EV_VALIDATE", ac_validate, null],
|
|
3075
|
+
["EV_EXPORT", ac_export, null],
|
|
3076
|
+
["EV_IMPORT", ac_import, null],
|
|
3077
|
+
["EV_PREVIEW_IMPORT", ac_preview_import, null],
|
|
3078
|
+
["EV_APPLY_IMPORT", ac_apply_import, null],
|
|
3079
|
+
["EV_CONFIRMED", ac_confirmed, null]
|
|
3080
|
+
];
|
|
3081
|
+
|
|
3082
|
+
/*---------------------------------------------*
|
|
3083
|
+
* States
|
|
3084
|
+
*
|
|
3085
|
+
* Each is a screen and a set of legal actions. An
|
|
3086
|
+
* event arriving in the wrong one is NOT swallowed
|
|
3087
|
+
* by a no-op action: the framework says so, loudly,
|
|
3088
|
+
* and names the sender — which is the only reason
|
|
3089
|
+
* a click is worth routing through a machine.
|
|
3090
|
+
*---------------------------------------------*/
|
|
3091
|
+
const states = [
|
|
3092
|
+
["ST_IDLE", COMMON.slice()],
|
|
3093
|
+
|
|
3094
|
+
["ST_LOADING", COMMON.slice()],
|
|
3095
|
+
|
|
3096
|
+
["ST_EMPTY", COMMON.slice()],
|
|
3097
|
+
|
|
3098
|
+
["ST_TREEDBS", COMMON.concat([
|
|
3099
|
+
["EV_SELECT_TREEDB", ac_select_treedb, null],
|
|
3100
|
+
["EV_SHOW_ORPHANS", ac_show_orphans, null],
|
|
3101
|
+
["EV_DELETE_ORPHAN", ac_delete_orphan, null],
|
|
3102
|
+
["EV_CONFIRMED", ac_confirmed, null]
|
|
3103
|
+
])],
|
|
3104
|
+
|
|
3105
|
+
["ST_TOPICS", COMMON.concat(TREEDB_WIDE).concat([
|
|
3106
|
+
["EV_SELECT_TOPIC", ac_select_topic, null],
|
|
3107
|
+
["EV_ADD_TOPIC", ac_add_topic, null],
|
|
3108
|
+
["EV_EDIT_TOPIC", ac_edit_topic, null],
|
|
3109
|
+
["EV_DELETE_TOPIC", ac_delete_topic, null],
|
|
3110
|
+
["EV_SAVE_TOPIC", ac_save_topic, null]
|
|
3111
|
+
])],
|
|
3112
|
+
|
|
3113
|
+
["ST_DIAGRAM", COMMON.concat(TREEDB_WIDE).concat([
|
|
3114
|
+
["EV_NODE_CLICK", ac_node_click, null]
|
|
3115
|
+
])],
|
|
3116
|
+
|
|
3117
|
+
["ST_COLUMNS", COMMON.concat(TREEDB_WIDE).concat([
|
|
3118
|
+
["EV_ADD_COLUMN", ac_add_column, null],
|
|
3119
|
+
["EV_EDIT_COLUMN", ac_edit_column, null],
|
|
3120
|
+
["EV_DUPLICATE_COLUMN", ac_duplicate_column, null],
|
|
3121
|
+
["EV_DELETE_COLUMN", ac_delete_column, null],
|
|
3122
|
+
["EV_SAVE_COLUMN", ac_save_column, null],
|
|
3123
|
+
["EV_MOVE_COLUMN", ac_move_column, null],
|
|
3124
|
+
["EV_BUMP_VERSION", ac_bump_version, null]
|
|
3125
|
+
])],
|
|
3126
|
+
|
|
3127
|
+
/* A write, or a plan of them, in flight. Only the answers and
|
|
3128
|
+
* the host are heard: every other action would be computed
|
|
3129
|
+
* against a model the answer still in the air is about to
|
|
3130
|
+
* change. */
|
|
3131
|
+
["ST_SAVING", COMMON.slice()]
|
|
3132
|
+
];
|
|
3133
|
+
|
|
3134
|
+
/*---------------------------------------------*
|
|
3135
|
+
* Events
|
|
3136
|
+
*---------------------------------------------*/
|
|
3137
|
+
const event_types = [
|
|
3138
|
+
/* From the backend. PUBLIC, or the ievent drops it before this
|
|
3139
|
+
* gclass is reached. */
|
|
3140
|
+
["EV_MT_COMMAND_ANSWER", event_flag_t.EVF_PUBLIC_EVENT],
|
|
3141
|
+
|
|
3142
|
+
/* From the host. */
|
|
3143
|
+
["EV_SHOW", 0],
|
|
3144
|
+
["EV_HIDE", 0],
|
|
3145
|
+
["EV_TRANSPORT_STATE", 0],
|
|
3146
|
+
["EV_LANGUAGE_CHANGED", 0],
|
|
3147
|
+
["EV_REFRESH", 0],
|
|
3148
|
+
|
|
3149
|
+
/* From this view's own screens. */
|
|
3150
|
+
["EV_SELECT_TREEDB", 0],
|
|
3151
|
+
["EV_SELECT_TOPIC", 0],
|
|
3152
|
+
["EV_BACK", 0],
|
|
3153
|
+
["EV_TOGGLE_DIAGRAM", 0],
|
|
3154
|
+
["EV_SHOW_ORPHANS", 0],
|
|
3155
|
+
["EV_DELETE_ORPHAN", 0],
|
|
3156
|
+
["EV_ADD_TOPIC", 0],
|
|
3157
|
+
["EV_EDIT_TOPIC", 0],
|
|
3158
|
+
["EV_DELETE_TOPIC", 0],
|
|
3159
|
+
["EV_SAVE_TOPIC", 0],
|
|
3160
|
+
["EV_ADD_COLUMN", 0],
|
|
3161
|
+
["EV_EDIT_COLUMN", 0],
|
|
3162
|
+
["EV_DUPLICATE_COLUMN", 0],
|
|
3163
|
+
["EV_DELETE_COLUMN", 0],
|
|
3164
|
+
["EV_SAVE_COLUMN", 0],
|
|
3165
|
+
["EV_MOVE_COLUMN", 0],
|
|
3166
|
+
["EV_BUMP_VERSION", 0],
|
|
3167
|
+
["EV_VALIDATE", 0],
|
|
3168
|
+
["EV_EXPORT", 0],
|
|
3169
|
+
["EV_IMPORT", 0],
|
|
3170
|
+
["EV_PREVIEW_IMPORT", 0],
|
|
3171
|
+
["EV_APPLY_IMPORT", 0],
|
|
3172
|
+
["EV_CONFIRMED", 0],
|
|
3173
|
+
|
|
3174
|
+
/* From the drawing, which is a child of this gobj. */
|
|
3175
|
+
["EV_NODE_CLICK", 0],
|
|
3176
|
+
|
|
3177
|
+
/* Outwards. The host mirrors the position into the url and
|
|
3178
|
+
* carries the write up to whoever owns the Apply — both are
|
|
3179
|
+
* optional, so neither warns when nobody listens. */
|
|
3180
|
+
["EV_POSITION_CHANGED", event_flag_t.EVF_OUTPUT_EVENT|
|
|
3181
|
+
event_flag_t.EVF_NO_WARN_SUBS],
|
|
3182
|
+
["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT|
|
|
3183
|
+
event_flag_t.EVF_NO_WARN_SUBS],
|
|
3184
|
+
["EV_SCHEMA_CHECKED", event_flag_t.EVF_OUTPUT_EVENT|
|
|
3185
|
+
event_flag_t.EVF_NO_WARN_SUBS]
|
|
3186
|
+
];
|
|
3187
|
+
|
|
3188
|
+
__gclass__ = gclass_create(
|
|
3189
|
+
gclass_name,
|
|
3190
|
+
event_types,
|
|
3191
|
+
states,
|
|
3192
|
+
gmt,
|
|
3193
|
+
0, // lmt,
|
|
3194
|
+
attrs_table,
|
|
3195
|
+
PRIVATE_DATA,
|
|
3196
|
+
0, // authz_table,
|
|
3197
|
+
0, // command_table,
|
|
3198
|
+
0, // s_user_trace_level
|
|
3199
|
+
0 // gclass_flag
|
|
3200
|
+
);
|
|
3201
|
+
|
|
3202
|
+
if(!__gclass__) {
|
|
3203
|
+
return -1;
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
return 0;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
/***************************************************************
|
|
3210
|
+
* Register GClass
|
|
3211
|
+
***************************************************************/
|
|
3212
|
+
function register_c_yui_schema_editor()
|
|
3213
|
+
{
|
|
3214
|
+
return create_gclass(GCLASS_NAME);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
export {register_c_yui_schema_editor};
|