@yuneta/gobj-ui 7.9.1 → 7.10.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 +35 -0
- package/dist/gobj-ui.cjs.js +226 -8
- package/dist/gobj-ui.es.js +226 -8
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +25 -3
- package/src/c_yui_treedb_topic_with_form.js +98 -8
- package/src/delete_impact.js +90 -0
- package/src/delete_impact.test.js +61 -0
package/README.md
CHANGED
|
@@ -491,6 +491,41 @@ The buttons of that toolbar never shrink. When the row runs out of room, the
|
|
|
491
491
|
url is what gives way, cut with an ellipsis, and the whole value stays in the
|
|
492
492
|
`title` and the `aria-label`.
|
|
493
493
|
|
|
494
|
+
### Reading a topic a page at a time
|
|
495
|
+
|
|
496
|
+
`C_YUI_TREEDB_TOPICS` takes **`with_remote_paging`** (off by default) and
|
|
497
|
+
forwards it to every topic table: the table pulls the page it is showing
|
|
498
|
+
instead of the host pushing the whole topic down. It needs the SDK's `nodes`
|
|
499
|
+
with `from` / `limit` (see `YUNO_TREEDB.md` §5.3).
|
|
500
|
+
|
|
501
|
+
**The page size is generous on purpose** (`page_size`, 200). A treedb that
|
|
502
|
+
fits in one page behaves exactly as it did — paginator hidden, every filter
|
|
503
|
+
seeing every row — so nothing that exists today changes. Only a topic that
|
|
504
|
+
does NOT fit pays for paging, and for that one loading it whole was never an
|
|
505
|
+
option.
|
|
506
|
+
|
|
507
|
+
**Safe against a backend that cannot page:** it answers the whole list, which
|
|
508
|
+
`nodes_answer()` reads as one page. That is the truth, and it is why the table
|
|
509
|
+
can ask without knowing what it is talking to.
|
|
510
|
+
|
|
511
|
+
`filterMode: "local"` says the plain truth: the header filters and the search
|
|
512
|
+
box work on the page that is loaded. Same as the tranger browser's Rows card,
|
|
513
|
+
and for the same reason — the alternative is pushing every filter to the
|
|
514
|
+
backend and changing what "search" means.
|
|
515
|
+
|
|
516
|
+
Who does what: the transport belongs to the HOST, so the table asks with
|
|
517
|
+
`EV_REQUEST_PAGE` and the answer comes back as `EV_PAGE_LOADED`, correlated by
|
|
518
|
+
an id echoed in `__md_command__`. **Read that id flat off the command stack**
|
|
519
|
+
(`kw_command.req_id`): `C_IEVENT_CLI` EXTRACTS `__md_command__` and pushes it
|
|
520
|
+
AS the stack's `kw`, so one level deeper is a level too far — and the symptom
|
|
521
|
+
is every request timing out with its answer sitting right there.
|
|
522
|
+
|
|
523
|
+
The promise Tabulator wants is parked in the table (`ajaxRequestFunc` must
|
|
524
|
+
RETURN a promise — it is a data source, not an event), with a watchdog,
|
|
525
|
+
because the link can stay up and an answer still never land. A refresh
|
|
526
|
+
re-pulls the page the reader is on rather than throwing them back to the
|
|
527
|
+
first.
|
|
528
|
+
|
|
494
529
|
### Editing a topic table in place
|
|
495
530
|
|
|
496
531
|
A writable scalar is editable in the table, in edition mode
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -16381,6 +16381,71 @@ function plan_treedb_writes(flags) {
|
|
|
16381
16381
|
};
|
|
16382
16382
|
}
|
|
16383
16383
|
//#endregion
|
|
16384
|
+
//#region src/delete_impact.js
|
|
16385
|
+
/***********************************************************************
|
|
16386
|
+
* delete_impact.js
|
|
16387
|
+
*
|
|
16388
|
+
* What a delete takes with it, read off the record itself.
|
|
16389
|
+
*
|
|
16390
|
+
* A treedb delete is not one thing. The backend refuses a node
|
|
16391
|
+
* that has links — UNLESS the caller sends `force`, and the topic
|
|
16392
|
+
* views do. With `force`:
|
|
16393
|
+
*
|
|
16394
|
+
* children are UNLINKED, not deleted. They survive, loose.
|
|
16395
|
+
* parents lose the node from their hook.
|
|
16396
|
+
*
|
|
16397
|
+
* So "delete this row" can mean "detach eleven records from their
|
|
16398
|
+
* only parent", and the question that used to be asked — `are you
|
|
16399
|
+
* sure` — said none of it. This counts what is at stake so the
|
|
16400
|
+
* question can name it.
|
|
16401
|
+
*
|
|
16402
|
+
* Read from the record the table ALREADY has (`list_dict` fills
|
|
16403
|
+
* the hook and fkey columns), so asking costs no round trip.
|
|
16404
|
+
*
|
|
16405
|
+
* Pure and tested, because the shapes are the fiddly part: a hook
|
|
16406
|
+
* or fkey value arrives as a list of refs, a dict keyed by id, or
|
|
16407
|
+
* a single ref string, depending on the column's declared type.
|
|
16408
|
+
*
|
|
16409
|
+
* Copyright (c) 2026, ArtGins.
|
|
16410
|
+
* All Rights Reserved.
|
|
16411
|
+
***********************************************************************/
|
|
16412
|
+
/***************************************************************
|
|
16413
|
+
* How many things a hook/fkey value holds.
|
|
16414
|
+
***************************************************************/
|
|
16415
|
+
function ref_count(value) {
|
|
16416
|
+
if (Array.isArray(value)) return value.length;
|
|
16417
|
+
if (value && typeof value === "object") return Object.keys(value).length;
|
|
16418
|
+
if (typeof value === "string" && value.length > 0) return 1;
|
|
16419
|
+
return 0;
|
|
16420
|
+
}
|
|
16421
|
+
/***************************************************************
|
|
16422
|
+
* delete_impact(desc, records) -> {records, children, parents}
|
|
16423
|
+
*
|
|
16424
|
+
* `records` is one record or a list of them, so the same answer
|
|
16425
|
+
* serves the one-row delete and the selection.
|
|
16426
|
+
***************************************************************/
|
|
16427
|
+
function delete_impact(desc, records) {
|
|
16428
|
+
let list = Array.isArray(records) ? records : records ? [records] : [];
|
|
16429
|
+
let out = {
|
|
16430
|
+
records: list.length,
|
|
16431
|
+
children: 0,
|
|
16432
|
+
parents: 0
|
|
16433
|
+
};
|
|
16434
|
+
let cols = desc && Array.isArray(desc.cols) ? desc.cols : [];
|
|
16435
|
+
if (!cols.length) return out;
|
|
16436
|
+
for (let record of list) {
|
|
16437
|
+
if (!record) continue;
|
|
16438
|
+
for (let col of cols) {
|
|
16439
|
+
if (!col || !col.id || !Array.isArray(col.flag)) continue;
|
|
16440
|
+
let n = ref_count(record[col.id]);
|
|
16441
|
+
if (n === 0) continue;
|
|
16442
|
+
if (col.flag.indexOf("hook") >= 0) out.children += n;
|
|
16443
|
+
if (col.flag.indexOf("fkey") >= 0) out.parents += n;
|
|
16444
|
+
}
|
|
16445
|
+
}
|
|
16446
|
+
return out;
|
|
16447
|
+
}
|
|
16448
|
+
//#endregion
|
|
16384
16449
|
//#region src/c_yui_treedb_topic_with_form.js
|
|
16385
16450
|
/***********************************************************************
|
|
16386
16451
|
* c_yui_treedb_topic_with_form.js
|
|
@@ -16447,6 +16512,7 @@ var attrs_table$5 = [
|
|
|
16447
16512
|
var PRIVATE_DATA$5 = {
|
|
16448
16513
|
_pending_pages: null,
|
|
16449
16514
|
_page_seq: 0,
|
|
16515
|
+
_page_total: null,
|
|
16450
16516
|
$container: null,
|
|
16451
16517
|
treedb_name: "",
|
|
16452
16518
|
topic_name: "",
|
|
@@ -17215,11 +17281,19 @@ function create_tabulator(gobj) {
|
|
|
17215
17281
|
} catch (e) {
|
|
17216
17282
|
n = 0;
|
|
17217
17283
|
}
|
|
17218
|
-
|
|
17284
|
+
let paged = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_remote_paging");
|
|
17285
|
+
let total = gobj.priv._page_total;
|
|
17286
|
+
if (paged && typeof total === "number" && total > n) $rc.textContent = `${n} / ${total} ${(0, i18next.t)("rows")}`;
|
|
17287
|
+
else $rc.textContent = `${n} ${(0, i18next.t)("rows")}`;
|
|
17219
17288
|
let single_page = true;
|
|
17220
17289
|
try {
|
|
17221
|
-
|
|
17222
|
-
|
|
17290
|
+
if (paged) {
|
|
17291
|
+
let max = tabulator.getPageMax();
|
|
17292
|
+
single_page = max === false || max <= 1;
|
|
17293
|
+
} else {
|
|
17294
|
+
let size = tabulator.getPageSize();
|
|
17295
|
+
single_page = !size || n <= size;
|
|
17296
|
+
}
|
|
17223
17297
|
} catch (e) {
|
|
17224
17298
|
single_page = true;
|
|
17225
17299
|
}
|
|
@@ -18210,7 +18284,7 @@ function ac_delete_rows(gobj, event, kw, src) {
|
|
|
18210
18284
|
});
|
|
18211
18285
|
return 0;
|
|
18212
18286
|
}
|
|
18213
|
-
yui_shell_confirm_yesnocancel(yui_shell_of(gobj),
|
|
18287
|
+
yui_shell_confirm_yesnocancel(yui_shell_of(gobj), build_delete_question(gobj, rows), {
|
|
18214
18288
|
t: i18next.t,
|
|
18215
18289
|
yes_label: "yes",
|
|
18216
18290
|
no_label: "no",
|
|
@@ -18221,7 +18295,7 @@ function ac_delete_rows(gobj, event, kw, src) {
|
|
|
18221
18295
|
record: row
|
|
18222
18296
|
});
|
|
18223
18297
|
});
|
|
18224
|
-
} else yui_shell_confirm_yesnocancel(yui_shell_of(gobj),
|
|
18298
|
+
} else yui_shell_confirm_yesnocancel(yui_shell_of(gobj), build_delete_question(gobj, kw.row), {
|
|
18225
18299
|
t: i18next.t,
|
|
18226
18300
|
yes_label: "yes",
|
|
18227
18301
|
no_label: "no",
|
|
@@ -18392,6 +18466,145 @@ function ac_show_schema(gobj, event, kw, src) {
|
|
|
18392
18466
|
return 0;
|
|
18393
18467
|
}
|
|
18394
18468
|
/************************************************************
|
|
18469
|
+
* The question a delete deserves.
|
|
18470
|
+
*
|
|
18471
|
+
* These views delete with `force`, and `force` on a treedb node does
|
|
18472
|
+
* not only remove it: its children are UNLINKED — they survive, loose
|
|
18473
|
+
* — and it is cleaned off its parents. So "delete this row" can mean
|
|
18474
|
+
* "detach eleven records from their only parent", and `are you sure`
|
|
18475
|
+
* said none of it.
|
|
18476
|
+
*
|
|
18477
|
+
* Built as DOM, not as a string, and that is forced rather than
|
|
18478
|
+
* chosen: the dialog renders its message AS AN I18N KEY, so a
|
|
18479
|
+
* composed sentence could never be one. Each translatable half
|
|
18480
|
+
* carries its own key and the numbers sit between them as data —
|
|
18481
|
+
* which is also the only way the question survives a language switch.
|
|
18482
|
+
************************************************************/
|
|
18483
|
+
function build_delete_question(gobj, records) {
|
|
18484
|
+
let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
|
|
18485
|
+
let impact = delete_impact(desc, records);
|
|
18486
|
+
let lines = [];
|
|
18487
|
+
let pkey = desc && desc.pkey || "id";
|
|
18488
|
+
let one = impact.records === 1 && Array.isArray(records) ? records[0] : impact.records === 1 ? records : null;
|
|
18489
|
+
if (one && one[pkey] !== void 0 && one[pkey] !== null) lines.push([
|
|
18490
|
+
"p",
|
|
18491
|
+
{ class: "DELETE_ASK_WHAT" },
|
|
18492
|
+
[
|
|
18493
|
+
[
|
|
18494
|
+
"span",
|
|
18495
|
+
{ i18n: "delete" },
|
|
18496
|
+
(0, i18next.t)("delete")
|
|
18497
|
+
],
|
|
18498
|
+
[
|
|
18499
|
+
"span",
|
|
18500
|
+
{},
|
|
18501
|
+
" "
|
|
18502
|
+
],
|
|
18503
|
+
[
|
|
18504
|
+
"strong",
|
|
18505
|
+
{},
|
|
18506
|
+
String(one[pkey])
|
|
18507
|
+
]
|
|
18508
|
+
]
|
|
18509
|
+
]);
|
|
18510
|
+
else lines.push([
|
|
18511
|
+
"p",
|
|
18512
|
+
{ class: "DELETE_ASK_WHAT" },
|
|
18513
|
+
[
|
|
18514
|
+
[
|
|
18515
|
+
"span",
|
|
18516
|
+
{ i18n: "delete" },
|
|
18517
|
+
(0, i18next.t)("delete")
|
|
18518
|
+
],
|
|
18519
|
+
[
|
|
18520
|
+
"span",
|
|
18521
|
+
{},
|
|
18522
|
+
" "
|
|
18523
|
+
],
|
|
18524
|
+
[
|
|
18525
|
+
"strong",
|
|
18526
|
+
{},
|
|
18527
|
+
String(impact.records)
|
|
18528
|
+
],
|
|
18529
|
+
[
|
|
18530
|
+
"span",
|
|
18531
|
+
{},
|
|
18532
|
+
" "
|
|
18533
|
+
],
|
|
18534
|
+
[
|
|
18535
|
+
"span",
|
|
18536
|
+
{ i18n: "records" },
|
|
18537
|
+
(0, i18next.t)("records")
|
|
18538
|
+
]
|
|
18539
|
+
]
|
|
18540
|
+
]);
|
|
18541
|
+
if (impact.children > 0) lines.push([
|
|
18542
|
+
"p",
|
|
18543
|
+
{ class: "DELETE_ASK_CHILDREN has-text-weight-semibold" },
|
|
18544
|
+
[
|
|
18545
|
+
[
|
|
18546
|
+
"strong",
|
|
18547
|
+
{},
|
|
18548
|
+
String(impact.children)
|
|
18549
|
+
],
|
|
18550
|
+
[
|
|
18551
|
+
"span",
|
|
18552
|
+
{},
|
|
18553
|
+
" "
|
|
18554
|
+
],
|
|
18555
|
+
[
|
|
18556
|
+
"span",
|
|
18557
|
+
{ i18n: "children will be unlinked, not deleted" },
|
|
18558
|
+
(0, i18next.t)("children will be unlinked, not deleted")
|
|
18559
|
+
]
|
|
18560
|
+
]
|
|
18561
|
+
]);
|
|
18562
|
+
if (impact.parents > 0) lines.push([
|
|
18563
|
+
"p",
|
|
18564
|
+
{ class: "DELETE_ASK_PARENTS" },
|
|
18565
|
+
[
|
|
18566
|
+
[
|
|
18567
|
+
"span",
|
|
18568
|
+
{ i18n: "it will be detached from" },
|
|
18569
|
+
(0, i18next.t)("it will be detached from")
|
|
18570
|
+
],
|
|
18571
|
+
[
|
|
18572
|
+
"span",
|
|
18573
|
+
{},
|
|
18574
|
+
" "
|
|
18575
|
+
],
|
|
18576
|
+
[
|
|
18577
|
+
"strong",
|
|
18578
|
+
{},
|
|
18579
|
+
String(impact.parents)
|
|
18580
|
+
],
|
|
18581
|
+
[
|
|
18582
|
+
"span",
|
|
18583
|
+
{},
|
|
18584
|
+
" "
|
|
18585
|
+
],
|
|
18586
|
+
[
|
|
18587
|
+
"span",
|
|
18588
|
+
{ i18n: "parents" },
|
|
18589
|
+
(0, i18next.t)("parents")
|
|
18590
|
+
]
|
|
18591
|
+
]
|
|
18592
|
+
]);
|
|
18593
|
+
lines.push([
|
|
18594
|
+
"p",
|
|
18595
|
+
{
|
|
18596
|
+
class: "DELETE_ASK_SURE",
|
|
18597
|
+
i18n: "are you sure"
|
|
18598
|
+
},
|
|
18599
|
+
(0, i18next.t)("are you sure")
|
|
18600
|
+
]);
|
|
18601
|
+
return (0, _yuneta_gobj_js.createElement2)([
|
|
18602
|
+
"div",
|
|
18603
|
+
{ class: "DELETE_ASK" },
|
|
18604
|
+
lines
|
|
18605
|
+
]);
|
|
18606
|
+
}
|
|
18607
|
+
/************************************************************
|
|
18395
18608
|
* Ask the host for one page and park the promise Tabulator wants.
|
|
18396
18609
|
*
|
|
18397
18610
|
* The transport belongs to the HOST, so the request goes up as an event
|
|
@@ -18460,6 +18673,7 @@ function ac_repull_page(gobj, event, kw, src) {
|
|
|
18460
18673
|
* The host answered a page.
|
|
18461
18674
|
************************************************************/
|
|
18462
18675
|
function ac_page_loaded(gobj, event, kw, src) {
|
|
18676
|
+
if (typeof kw.total === "number") gobj.priv._page_total = kw.total;
|
|
18463
18677
|
settle_page(gobj, kw.req_id, {
|
|
18464
18678
|
data: (0, _yuneta_gobj_js.is_array)(kw.rows) ? kw.rows : [],
|
|
18465
18679
|
last_page: kw.pages || 1,
|
|
@@ -54879,7 +55093,7 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
54879
55093
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
54880
55094
|
const popover = create_popover_base(iconRect.right - containerRect.left + 6, iconRect.top - containerRect.top - 4, "g6-confirm-popover", "#ff4d4f", 160);
|
|
54881
55095
|
let msg = document.createElement("div");
|
|
54882
|
-
msg.style.cssText = "margin-bottom:10px;font-weight:500;";
|
|
55096
|
+
msg.style.cssText = "margin-bottom:10px;font-weight:500;white-space:pre-line;";
|
|
54883
55097
|
msg.textContent = message;
|
|
54884
55098
|
popover.appendChild(msg);
|
|
54885
55099
|
create_form_button_row(popover, [{
|
|
@@ -56453,7 +56667,11 @@ function show_delete_confirm(gobj, nodeData) {
|
|
|
56453
56667
|
let priv = gobj.priv;
|
|
56454
56668
|
let record = nodeData.data.record || {};
|
|
56455
56669
|
let topic_name = nodeData.data.desc.topic_name;
|
|
56456
|
-
|
|
56670
|
+
let impact = delete_impact(nodeData.data.desc, record);
|
|
56671
|
+
let question = (0, i18next.t)("delete") + " " + topic_name + ": " + record.id + "?";
|
|
56672
|
+
if (impact.children > 0) question += "\n" + impact.children + " " + (0, i18next.t)("children will be unlinked, not deleted");
|
|
56673
|
+
if (impact.parents > 0) question += "\n" + (0, i18next.t)("it will be detached from") + " " + impact.parents + " " + (0, i18next.t)("parents");
|
|
56674
|
+
show_confirm_popover(gobj, priv._node_delete_el, question, "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
|
|
56457
56675
|
}
|
|
56458
56676
|
function hide_delete_confirm(gobj) {
|
|
56459
56677
|
hide_overlay(gobj, "_delete_confirm_el");
|
|
@@ -56611,7 +56829,7 @@ function execute_unlink_edge(gobj, edgeData) {
|
|
|
56611
56829
|
function show_unlink_confirm(gobj, edgeData) {
|
|
56612
56830
|
let priv = gobj.priv;
|
|
56613
56831
|
const d = edgeData.data;
|
|
56614
|
-
show_confirm_popover(gobj, priv._edge_delete_el, (0, i18next.t)("unlink") + " " + d.child_id + " → " + d.parent_id + "
|
|
56832
|
+
show_confirm_popover(gobj, priv._edge_delete_el, (0, i18next.t)("unlink") + " " + d.child_id + " → " + d.parent_id + "?\n" + (0, i18next.t)("neither record is deleted"), "unlink", "#ff4d4f", () => execute_unlink_edge(gobj, edgeData), "_unlink_confirm_el");
|
|
56615
56833
|
}
|
|
56616
56834
|
function hide_unlink_confirm(gobj) {
|
|
56617
56835
|
hide_overlay(gobj, "_unlink_confirm_el");
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -16376,6 +16376,71 @@ function plan_treedb_writes(flags) {
|
|
|
16376
16376
|
};
|
|
16377
16377
|
}
|
|
16378
16378
|
//#endregion
|
|
16379
|
+
//#region src/delete_impact.js
|
|
16380
|
+
/***********************************************************************
|
|
16381
|
+
* delete_impact.js
|
|
16382
|
+
*
|
|
16383
|
+
* What a delete takes with it, read off the record itself.
|
|
16384
|
+
*
|
|
16385
|
+
* A treedb delete is not one thing. The backend refuses a node
|
|
16386
|
+
* that has links — UNLESS the caller sends `force`, and the topic
|
|
16387
|
+
* views do. With `force`:
|
|
16388
|
+
*
|
|
16389
|
+
* children are UNLINKED, not deleted. They survive, loose.
|
|
16390
|
+
* parents lose the node from their hook.
|
|
16391
|
+
*
|
|
16392
|
+
* So "delete this row" can mean "detach eleven records from their
|
|
16393
|
+
* only parent", and the question that used to be asked — `are you
|
|
16394
|
+
* sure` — said none of it. This counts what is at stake so the
|
|
16395
|
+
* question can name it.
|
|
16396
|
+
*
|
|
16397
|
+
* Read from the record the table ALREADY has (`list_dict` fills
|
|
16398
|
+
* the hook and fkey columns), so asking costs no round trip.
|
|
16399
|
+
*
|
|
16400
|
+
* Pure and tested, because the shapes are the fiddly part: a hook
|
|
16401
|
+
* or fkey value arrives as a list of refs, a dict keyed by id, or
|
|
16402
|
+
* a single ref string, depending on the column's declared type.
|
|
16403
|
+
*
|
|
16404
|
+
* Copyright (c) 2026, ArtGins.
|
|
16405
|
+
* All Rights Reserved.
|
|
16406
|
+
***********************************************************************/
|
|
16407
|
+
/***************************************************************
|
|
16408
|
+
* How many things a hook/fkey value holds.
|
|
16409
|
+
***************************************************************/
|
|
16410
|
+
function ref_count(value) {
|
|
16411
|
+
if (Array.isArray(value)) return value.length;
|
|
16412
|
+
if (value && typeof value === "object") return Object.keys(value).length;
|
|
16413
|
+
if (typeof value === "string" && value.length > 0) return 1;
|
|
16414
|
+
return 0;
|
|
16415
|
+
}
|
|
16416
|
+
/***************************************************************
|
|
16417
|
+
* delete_impact(desc, records) -> {records, children, parents}
|
|
16418
|
+
*
|
|
16419
|
+
* `records` is one record or a list of them, so the same answer
|
|
16420
|
+
* serves the one-row delete and the selection.
|
|
16421
|
+
***************************************************************/
|
|
16422
|
+
function delete_impact(desc, records) {
|
|
16423
|
+
let list = Array.isArray(records) ? records : records ? [records] : [];
|
|
16424
|
+
let out = {
|
|
16425
|
+
records: list.length,
|
|
16426
|
+
children: 0,
|
|
16427
|
+
parents: 0
|
|
16428
|
+
};
|
|
16429
|
+
let cols = desc && Array.isArray(desc.cols) ? desc.cols : [];
|
|
16430
|
+
if (!cols.length) return out;
|
|
16431
|
+
for (let record of list) {
|
|
16432
|
+
if (!record) continue;
|
|
16433
|
+
for (let col of cols) {
|
|
16434
|
+
if (!col || !col.id || !Array.isArray(col.flag)) continue;
|
|
16435
|
+
let n = ref_count(record[col.id]);
|
|
16436
|
+
if (n === 0) continue;
|
|
16437
|
+
if (col.flag.indexOf("hook") >= 0) out.children += n;
|
|
16438
|
+
if (col.flag.indexOf("fkey") >= 0) out.parents += n;
|
|
16439
|
+
}
|
|
16440
|
+
}
|
|
16441
|
+
return out;
|
|
16442
|
+
}
|
|
16443
|
+
//#endregion
|
|
16379
16444
|
//#region src/c_yui_treedb_topic_with_form.js
|
|
16380
16445
|
/***********************************************************************
|
|
16381
16446
|
* c_yui_treedb_topic_with_form.js
|
|
@@ -16442,6 +16507,7 @@ var attrs_table$5 = [
|
|
|
16442
16507
|
var PRIVATE_DATA$5 = {
|
|
16443
16508
|
_pending_pages: null,
|
|
16444
16509
|
_page_seq: 0,
|
|
16510
|
+
_page_total: null,
|
|
16445
16511
|
$container: null,
|
|
16446
16512
|
treedb_name: "",
|
|
16447
16513
|
topic_name: "",
|
|
@@ -17210,11 +17276,19 @@ function create_tabulator(gobj) {
|
|
|
17210
17276
|
} catch (e) {
|
|
17211
17277
|
n = 0;
|
|
17212
17278
|
}
|
|
17213
|
-
|
|
17279
|
+
let paged = gobj_read_bool_attr(gobj, "with_remote_paging");
|
|
17280
|
+
let total = gobj.priv._page_total;
|
|
17281
|
+
if (paged && typeof total === "number" && total > n) $rc.textContent = `${n} / ${total} ${t("rows")}`;
|
|
17282
|
+
else $rc.textContent = `${n} ${t("rows")}`;
|
|
17214
17283
|
let single_page = true;
|
|
17215
17284
|
try {
|
|
17216
|
-
|
|
17217
|
-
|
|
17285
|
+
if (paged) {
|
|
17286
|
+
let max = tabulator.getPageMax();
|
|
17287
|
+
single_page = max === false || max <= 1;
|
|
17288
|
+
} else {
|
|
17289
|
+
let size = tabulator.getPageSize();
|
|
17290
|
+
single_page = !size || n <= size;
|
|
17291
|
+
}
|
|
17218
17292
|
} catch (e) {
|
|
17219
17293
|
single_page = true;
|
|
17220
17294
|
}
|
|
@@ -18205,7 +18279,7 @@ function ac_delete_rows(gobj, event, kw, src) {
|
|
|
18205
18279
|
});
|
|
18206
18280
|
return 0;
|
|
18207
18281
|
}
|
|
18208
|
-
yui_shell_confirm_yesnocancel(yui_shell_of(gobj),
|
|
18282
|
+
yui_shell_confirm_yesnocancel(yui_shell_of(gobj), build_delete_question(gobj, rows), {
|
|
18209
18283
|
t,
|
|
18210
18284
|
yes_label: "yes",
|
|
18211
18285
|
no_label: "no",
|
|
@@ -18216,7 +18290,7 @@ function ac_delete_rows(gobj, event, kw, src) {
|
|
|
18216
18290
|
record: row
|
|
18217
18291
|
});
|
|
18218
18292
|
});
|
|
18219
|
-
} else yui_shell_confirm_yesnocancel(yui_shell_of(gobj),
|
|
18293
|
+
} else yui_shell_confirm_yesnocancel(yui_shell_of(gobj), build_delete_question(gobj, kw.row), {
|
|
18220
18294
|
t,
|
|
18221
18295
|
yes_label: "yes",
|
|
18222
18296
|
no_label: "no",
|
|
@@ -18387,6 +18461,145 @@ function ac_show_schema(gobj, event, kw, src) {
|
|
|
18387
18461
|
return 0;
|
|
18388
18462
|
}
|
|
18389
18463
|
/************************************************************
|
|
18464
|
+
* The question a delete deserves.
|
|
18465
|
+
*
|
|
18466
|
+
* These views delete with `force`, and `force` on a treedb node does
|
|
18467
|
+
* not only remove it: its children are UNLINKED — they survive, loose
|
|
18468
|
+
* — and it is cleaned off its parents. So "delete this row" can mean
|
|
18469
|
+
* "detach eleven records from their only parent", and `are you sure`
|
|
18470
|
+
* said none of it.
|
|
18471
|
+
*
|
|
18472
|
+
* Built as DOM, not as a string, and that is forced rather than
|
|
18473
|
+
* chosen: the dialog renders its message AS AN I18N KEY, so a
|
|
18474
|
+
* composed sentence could never be one. Each translatable half
|
|
18475
|
+
* carries its own key and the numbers sit between them as data —
|
|
18476
|
+
* which is also the only way the question survives a language switch.
|
|
18477
|
+
************************************************************/
|
|
18478
|
+
function build_delete_question(gobj, records) {
|
|
18479
|
+
let desc = gobj_read_attr(gobj, "desc");
|
|
18480
|
+
let impact = delete_impact(desc, records);
|
|
18481
|
+
let lines = [];
|
|
18482
|
+
let pkey = desc && desc.pkey || "id";
|
|
18483
|
+
let one = impact.records === 1 && Array.isArray(records) ? records[0] : impact.records === 1 ? records : null;
|
|
18484
|
+
if (one && one[pkey] !== void 0 && one[pkey] !== null) lines.push([
|
|
18485
|
+
"p",
|
|
18486
|
+
{ class: "DELETE_ASK_WHAT" },
|
|
18487
|
+
[
|
|
18488
|
+
[
|
|
18489
|
+
"span",
|
|
18490
|
+
{ i18n: "delete" },
|
|
18491
|
+
t("delete")
|
|
18492
|
+
],
|
|
18493
|
+
[
|
|
18494
|
+
"span",
|
|
18495
|
+
{},
|
|
18496
|
+
" "
|
|
18497
|
+
],
|
|
18498
|
+
[
|
|
18499
|
+
"strong",
|
|
18500
|
+
{},
|
|
18501
|
+
String(one[pkey])
|
|
18502
|
+
]
|
|
18503
|
+
]
|
|
18504
|
+
]);
|
|
18505
|
+
else lines.push([
|
|
18506
|
+
"p",
|
|
18507
|
+
{ class: "DELETE_ASK_WHAT" },
|
|
18508
|
+
[
|
|
18509
|
+
[
|
|
18510
|
+
"span",
|
|
18511
|
+
{ i18n: "delete" },
|
|
18512
|
+
t("delete")
|
|
18513
|
+
],
|
|
18514
|
+
[
|
|
18515
|
+
"span",
|
|
18516
|
+
{},
|
|
18517
|
+
" "
|
|
18518
|
+
],
|
|
18519
|
+
[
|
|
18520
|
+
"strong",
|
|
18521
|
+
{},
|
|
18522
|
+
String(impact.records)
|
|
18523
|
+
],
|
|
18524
|
+
[
|
|
18525
|
+
"span",
|
|
18526
|
+
{},
|
|
18527
|
+
" "
|
|
18528
|
+
],
|
|
18529
|
+
[
|
|
18530
|
+
"span",
|
|
18531
|
+
{ i18n: "records" },
|
|
18532
|
+
t("records")
|
|
18533
|
+
]
|
|
18534
|
+
]
|
|
18535
|
+
]);
|
|
18536
|
+
if (impact.children > 0) lines.push([
|
|
18537
|
+
"p",
|
|
18538
|
+
{ class: "DELETE_ASK_CHILDREN has-text-weight-semibold" },
|
|
18539
|
+
[
|
|
18540
|
+
[
|
|
18541
|
+
"strong",
|
|
18542
|
+
{},
|
|
18543
|
+
String(impact.children)
|
|
18544
|
+
],
|
|
18545
|
+
[
|
|
18546
|
+
"span",
|
|
18547
|
+
{},
|
|
18548
|
+
" "
|
|
18549
|
+
],
|
|
18550
|
+
[
|
|
18551
|
+
"span",
|
|
18552
|
+
{ i18n: "children will be unlinked, not deleted" },
|
|
18553
|
+
t("children will be unlinked, not deleted")
|
|
18554
|
+
]
|
|
18555
|
+
]
|
|
18556
|
+
]);
|
|
18557
|
+
if (impact.parents > 0) lines.push([
|
|
18558
|
+
"p",
|
|
18559
|
+
{ class: "DELETE_ASK_PARENTS" },
|
|
18560
|
+
[
|
|
18561
|
+
[
|
|
18562
|
+
"span",
|
|
18563
|
+
{ i18n: "it will be detached from" },
|
|
18564
|
+
t("it will be detached from")
|
|
18565
|
+
],
|
|
18566
|
+
[
|
|
18567
|
+
"span",
|
|
18568
|
+
{},
|
|
18569
|
+
" "
|
|
18570
|
+
],
|
|
18571
|
+
[
|
|
18572
|
+
"strong",
|
|
18573
|
+
{},
|
|
18574
|
+
String(impact.parents)
|
|
18575
|
+
],
|
|
18576
|
+
[
|
|
18577
|
+
"span",
|
|
18578
|
+
{},
|
|
18579
|
+
" "
|
|
18580
|
+
],
|
|
18581
|
+
[
|
|
18582
|
+
"span",
|
|
18583
|
+
{ i18n: "parents" },
|
|
18584
|
+
t("parents")
|
|
18585
|
+
]
|
|
18586
|
+
]
|
|
18587
|
+
]);
|
|
18588
|
+
lines.push([
|
|
18589
|
+
"p",
|
|
18590
|
+
{
|
|
18591
|
+
class: "DELETE_ASK_SURE",
|
|
18592
|
+
i18n: "are you sure"
|
|
18593
|
+
},
|
|
18594
|
+
t("are you sure")
|
|
18595
|
+
]);
|
|
18596
|
+
return createElement2([
|
|
18597
|
+
"div",
|
|
18598
|
+
{ class: "DELETE_ASK" },
|
|
18599
|
+
lines
|
|
18600
|
+
]);
|
|
18601
|
+
}
|
|
18602
|
+
/************************************************************
|
|
18390
18603
|
* Ask the host for one page and park the promise Tabulator wants.
|
|
18391
18604
|
*
|
|
18392
18605
|
* The transport belongs to the HOST, so the request goes up as an event
|
|
@@ -18455,6 +18668,7 @@ function ac_repull_page(gobj, event, kw, src) {
|
|
|
18455
18668
|
* The host answered a page.
|
|
18456
18669
|
************************************************************/
|
|
18457
18670
|
function ac_page_loaded(gobj, event, kw, src) {
|
|
18671
|
+
if (typeof kw.total === "number") gobj.priv._page_total = kw.total;
|
|
18458
18672
|
settle_page(gobj, kw.req_id, {
|
|
18459
18673
|
data: is_array(kw.rows) ? kw.rows : [],
|
|
18460
18674
|
last_page: kw.pages || 1,
|
|
@@ -54866,7 +55080,7 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
54866
55080
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
54867
55081
|
const popover = create_popover_base(iconRect.right - containerRect.left + 6, iconRect.top - containerRect.top - 4, "g6-confirm-popover", "#ff4d4f", 160);
|
|
54868
55082
|
let msg = document.createElement("div");
|
|
54869
|
-
msg.style.cssText = "margin-bottom:10px;font-weight:500;";
|
|
55083
|
+
msg.style.cssText = "margin-bottom:10px;font-weight:500;white-space:pre-line;";
|
|
54870
55084
|
msg.textContent = message;
|
|
54871
55085
|
popover.appendChild(msg);
|
|
54872
55086
|
create_form_button_row(popover, [{
|
|
@@ -56440,7 +56654,11 @@ function show_delete_confirm(gobj, nodeData) {
|
|
|
56440
56654
|
let priv = gobj.priv;
|
|
56441
56655
|
let record = nodeData.data.record || {};
|
|
56442
56656
|
let topic_name = nodeData.data.desc.topic_name;
|
|
56443
|
-
|
|
56657
|
+
let impact = delete_impact(nodeData.data.desc, record);
|
|
56658
|
+
let question = t("delete") + " " + topic_name + ": " + record.id + "?";
|
|
56659
|
+
if (impact.children > 0) question += "\n" + impact.children + " " + t("children will be unlinked, not deleted");
|
|
56660
|
+
if (impact.parents > 0) question += "\n" + t("it will be detached from") + " " + impact.parents + " " + t("parents");
|
|
56661
|
+
show_confirm_popover(gobj, priv._node_delete_el, question, "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
|
|
56444
56662
|
}
|
|
56445
56663
|
function hide_delete_confirm(gobj) {
|
|
56446
56664
|
hide_overlay(gobj, "_delete_confirm_el");
|
|
@@ -56598,7 +56816,7 @@ function execute_unlink_edge(gobj, edgeData) {
|
|
|
56598
56816
|
function show_unlink_confirm(gobj, edgeData) {
|
|
56599
56817
|
let priv = gobj.priv;
|
|
56600
56818
|
const d = edgeData.data;
|
|
56601
|
-
show_confirm_popover(gobj, priv._edge_delete_el, t("unlink") + " " + d.child_id + " → " + d.parent_id + "
|
|
56819
|
+
show_confirm_popover(gobj, priv._edge_delete_el, t("unlink") + " " + d.child_id + " → " + d.parent_id + "?\n" + t("neither record is deleted"), "unlink", "#ff4d4f", () => execute_unlink_edge(gobj, edgeData), "_unlink_confirm_el");
|
|
56602
56820
|
}
|
|
56603
56821
|
function hide_unlink_confirm(gobj) {
|
|
56604
56822
|
hide_overlay(gobj, "_unlink_confirm_el");
|
package/package.json
CHANGED
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
} from "./lib_graph.js";
|
|
82
82
|
|
|
83
83
|
import {node_label} from "./treedb_node_label.js";
|
|
84
|
+
import {delete_impact} from "./delete_impact.js";
|
|
84
85
|
|
|
85
86
|
import {
|
|
86
87
|
BaseLayout,
|
|
@@ -2782,7 +2783,9 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
2782
2783
|
|
|
2783
2784
|
// Message
|
|
2784
2785
|
let msg = document.createElement('div');
|
|
2785
|
-
|
|
2786
|
+
/* pre-line: a confirmation that has to say what it takes with it needs
|
|
2787
|
+
* a second line, and textContent alone would run it together. */
|
|
2788
|
+
msg.style.cssText = 'margin-bottom:10px;font-weight:500;white-space:pre-line;';
|
|
2786
2789
|
msg.textContent = message;
|
|
2787
2790
|
popover.appendChild(msg);
|
|
2788
2791
|
|
|
@@ -5001,8 +5004,23 @@ function show_delete_confirm(gobj, nodeData)
|
|
|
5001
5004
|
let record = nodeData.data.record || {};
|
|
5002
5005
|
let topic_name = nodeData.data.desc.topic_name;
|
|
5003
5006
|
|
|
5007
|
+
/* What the delete takes with it. These views delete with `force`, and
|
|
5008
|
+
* `force` UNLINKS a node's children — they survive, loose — and cleans
|
|
5009
|
+
* it off its parents. A question that names only the node lets an
|
|
5010
|
+
* operator detach eleven records believing they removed one. */
|
|
5011
|
+
let impact = delete_impact(nodeData.data.desc, record);
|
|
5012
|
+
let question = t('delete') + ' ' + topic_name + ': ' + record.id + '?';
|
|
5013
|
+
if(impact.children > 0) {
|
|
5014
|
+
question += '\n' + impact.children + ' ' +
|
|
5015
|
+
t('children will be unlinked, not deleted');
|
|
5016
|
+
}
|
|
5017
|
+
if(impact.parents > 0) {
|
|
5018
|
+
question += '\n' + t('it will be detached from') + ' ' +
|
|
5019
|
+
impact.parents + ' ' + t('parents');
|
|
5020
|
+
}
|
|
5021
|
+
|
|
5004
5022
|
show_confirm_popover(gobj, priv._node_delete_el,
|
|
5005
|
-
|
|
5023
|
+
question,
|
|
5006
5024
|
'delete', '#ff4d4f',
|
|
5007
5025
|
() => execute_delete_node(gobj, nodeData),
|
|
5008
5026
|
'_delete_confirm_el'
|
|
@@ -5254,8 +5272,12 @@ function show_unlink_confirm(gobj, edgeData)
|
|
|
5254
5272
|
let priv = gobj.priv;
|
|
5255
5273
|
const d = edgeData.data;
|
|
5256
5274
|
|
|
5275
|
+
/* The reassurance is the point: an unlink removes the LINK and nothing
|
|
5276
|
+
* else, and next to a delete button painted the same red, that is not
|
|
5277
|
+
* obvious. */
|
|
5257
5278
|
show_confirm_popover(gobj, priv._edge_delete_el,
|
|
5258
|
-
t('unlink') + ' ' + d.child_id + ' → ' + d.parent_id + '?'
|
|
5279
|
+
t('unlink') + ' ' + d.child_id + ' → ' + d.parent_id + '?' +
|
|
5280
|
+
'\n' + t('neither record is deleted'),
|
|
5259
5281
|
'unlink', '#ff4d4f',
|
|
5260
5282
|
() => execute_unlink_edge(gobj, edgeData),
|
|
5261
5283
|
'_unlink_confirm_el'
|
|
@@ -78,6 +78,7 @@ import {yui_tabulator_lang, yui_tabulator_relocalize} from "./yui_tabulator_i18n
|
|
|
78
78
|
import {t} from "i18next";
|
|
79
79
|
|
|
80
80
|
import {plan_treedb_writes} from "./treedb_write_plan.js";
|
|
81
|
+
import {delete_impact} from "./delete_impact.js";
|
|
81
82
|
|
|
82
83
|
import "./c_yui_treedb_topic_with_form.css";
|
|
83
84
|
import "./tabulator.css";
|
|
@@ -151,6 +152,7 @@ SDATA_END()
|
|
|
151
152
|
let PRIVATE_DATA = {
|
|
152
153
|
_pending_pages: null, // req_id -> {resolve, reject, timer}
|
|
153
154
|
_page_seq: 0, // correlation id of a page request
|
|
155
|
+
_page_total: null, // rows the topic HAS, from the last page answer
|
|
154
156
|
$container: null,
|
|
155
157
|
treedb_name: "",
|
|
156
158
|
topic_name: "",
|
|
@@ -1203,15 +1205,33 @@ function create_tabulator(gobj)
|
|
|
1203
1205
|
n = 0;
|
|
1204
1206
|
}
|
|
1205
1207
|
}
|
|
1206
|
-
|
|
1208
|
+
/* Remote paging: `n` is the PAGE, so saying "50 rows" of a topic
|
|
1209
|
+
* with 114 would be a lie by omission. Say both. */
|
|
1210
|
+
let paged = gobj_read_bool_attr(gobj, "with_remote_paging");
|
|
1211
|
+
let total = gobj.priv._page_total;
|
|
1212
|
+
if(paged && typeof total === "number" && total > n) {
|
|
1213
|
+
$rc.textContent = `${n} / ${total} ${t("rows")}`;
|
|
1214
|
+
} else {
|
|
1215
|
+
$rc.textContent = `${n} ${t("rows")}`;
|
|
1216
|
+
}
|
|
1207
1217
|
|
|
1208
|
-
/* Derived from the count and the page size rather than read from
|
|
1209
|
-
* getPageMax(), which is stale in the filter path for the same
|
|
1210
|
-
* reason. getPageSize() throws with pagination off -> one page. */
|
|
1211
1218
|
let single_page = true;
|
|
1212
1219
|
try {
|
|
1213
|
-
|
|
1214
|
-
|
|
1220
|
+
if(paged) {
|
|
1221
|
+
/* Tabulator knows the last page from the answer's
|
|
1222
|
+
* `last_page`, and that is the only thing that says whether
|
|
1223
|
+
* there is more. Deriving it from the row count instead —
|
|
1224
|
+
* which is what the local path does — hid the paginator on
|
|
1225
|
+
* every FULL page: 50 rows of a 50-row page reads as "it all
|
|
1226
|
+
* fits" and it does not. */
|
|
1227
|
+
let max = tabulator.getPageMax();
|
|
1228
|
+
single_page = (max === false) || max <= 1;
|
|
1229
|
+
} else {
|
|
1230
|
+
/* Derived from the count and the page size rather than read
|
|
1231
|
+
* from getPageMax(), which is stale in the filter path. */
|
|
1232
|
+
let size = tabulator.getPageSize();
|
|
1233
|
+
single_page = !size || n <= size;
|
|
1234
|
+
}
|
|
1215
1235
|
} catch(e) {
|
|
1216
1236
|
single_page = true;
|
|
1217
1237
|
}
|
|
@@ -2705,7 +2725,7 @@ function ac_delete_rows(gobj, event, kw, src)
|
|
|
2705
2725
|
}
|
|
2706
2726
|
|
|
2707
2727
|
yui_shell_confirm_yesnocancel(
|
|
2708
|
-
yui_shell_of(gobj),
|
|
2728
|
+
yui_shell_of(gobj), build_delete_question(gobj, rows),
|
|
2709
2729
|
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
2710
2730
|
).then(function(answer) {
|
|
2711
2731
|
if(answer === "yes") {
|
|
@@ -2729,7 +2749,7 @@ function ac_delete_rows(gobj, event, kw, src)
|
|
|
2729
2749
|
* {index: , row: }
|
|
2730
2750
|
*----------------------------*/
|
|
2731
2751
|
yui_shell_confirm_yesnocancel(
|
|
2732
|
-
yui_shell_of(gobj),
|
|
2752
|
+
yui_shell_of(gobj), build_delete_question(gobj, kw.row),
|
|
2733
2753
|
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
2734
2754
|
).then(function(answer) {
|
|
2735
2755
|
if(answer === "yes") {
|
|
@@ -3000,6 +3020,73 @@ function ac_show_schema(gobj, event, kw, src)
|
|
|
3000
3020
|
return 0;
|
|
3001
3021
|
}
|
|
3002
3022
|
|
|
3023
|
+
/************************************************************
|
|
3024
|
+
* The question a delete deserves.
|
|
3025
|
+
*
|
|
3026
|
+
* These views delete with `force`, and `force` on a treedb node does
|
|
3027
|
+
* not only remove it: its children are UNLINKED — they survive, loose
|
|
3028
|
+
* — and it is cleaned off its parents. So "delete this row" can mean
|
|
3029
|
+
* "detach eleven records from their only parent", and `are you sure`
|
|
3030
|
+
* said none of it.
|
|
3031
|
+
*
|
|
3032
|
+
* Built as DOM, not as a string, and that is forced rather than
|
|
3033
|
+
* chosen: the dialog renders its message AS AN I18N KEY, so a
|
|
3034
|
+
* composed sentence could never be one. Each translatable half
|
|
3035
|
+
* carries its own key and the numbers sit between them as data —
|
|
3036
|
+
* which is also the only way the question survives a language switch.
|
|
3037
|
+
************************************************************/
|
|
3038
|
+
function build_delete_question(gobj, records)
|
|
3039
|
+
{
|
|
3040
|
+
let desc = gobj_read_attr(gobj, "desc");
|
|
3041
|
+
let impact = delete_impact(desc, records);
|
|
3042
|
+
|
|
3043
|
+
let lines = [];
|
|
3044
|
+
|
|
3045
|
+
/* What is being deleted: one named record, or how many. */
|
|
3046
|
+
let pkey = (desc && desc.pkey) || "id";
|
|
3047
|
+
let one = (impact.records === 1 && Array.isArray(records))? records[0] :
|
|
3048
|
+
(impact.records === 1? records : null);
|
|
3049
|
+
if(one && one[pkey] !== undefined && one[pkey] !== null) {
|
|
3050
|
+
lines.push(["p", {class: "DELETE_ASK_WHAT"}, [
|
|
3051
|
+
["span", {i18n: "delete"}, t("delete")],
|
|
3052
|
+
["span", {}, " "],
|
|
3053
|
+
["strong", {}, String(one[pkey])]
|
|
3054
|
+
]]);
|
|
3055
|
+
} else {
|
|
3056
|
+
lines.push(["p", {class: "DELETE_ASK_WHAT"}, [
|
|
3057
|
+
["span", {i18n: "delete"}, t("delete")],
|
|
3058
|
+
["span", {}, " "],
|
|
3059
|
+
["strong", {}, String(impact.records)],
|
|
3060
|
+
["span", {}, " "],
|
|
3061
|
+
["span", {i18n: "records"}, t("records")]
|
|
3062
|
+
]]);
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
/* What goes with it. Said only when there IS something: a loose
|
|
3066
|
+
* record must not be dressed up as a dangerous one. */
|
|
3067
|
+
if(impact.children > 0) {
|
|
3068
|
+
lines.push(["p", {class: "DELETE_ASK_CHILDREN has-text-weight-semibold"}, [
|
|
3069
|
+
["strong", {}, String(impact.children)],
|
|
3070
|
+
["span", {}, " "],
|
|
3071
|
+
["span", {i18n: "children will be unlinked, not deleted"},
|
|
3072
|
+
t("children will be unlinked, not deleted")]
|
|
3073
|
+
]]);
|
|
3074
|
+
}
|
|
3075
|
+
if(impact.parents > 0) {
|
|
3076
|
+
lines.push(["p", {class: "DELETE_ASK_PARENTS"}, [
|
|
3077
|
+
["span", {i18n: "it will be detached from"}, t("it will be detached from")],
|
|
3078
|
+
["span", {}, " "],
|
|
3079
|
+
["strong", {}, String(impact.parents)],
|
|
3080
|
+
["span", {}, " "],
|
|
3081
|
+
["span", {i18n: "parents"}, t("parents")]
|
|
3082
|
+
]]);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
lines.push(["p", {class: "DELETE_ASK_SURE", i18n: "are you sure"}, t("are you sure")]);
|
|
3086
|
+
|
|
3087
|
+
return createElement2(["div", {class: "DELETE_ASK"}, lines]);
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3003
3090
|
/************************************************************
|
|
3004
3091
|
* Ask the host for one page and park the promise Tabulator wants.
|
|
3005
3092
|
*
|
|
@@ -3089,6 +3176,9 @@ function ac_repull_page(gobj, event, kw, src)
|
|
|
3089
3176
|
************************************************************/
|
|
3090
3177
|
function ac_page_loaded(gobj, event, kw, src)
|
|
3091
3178
|
{
|
|
3179
|
+
if(typeof kw.total === "number") {
|
|
3180
|
+
gobj.priv._page_total = kw.total;
|
|
3181
|
+
}
|
|
3092
3182
|
settle_page(gobj, kw.req_id, {
|
|
3093
3183
|
data: is_array(kw.rows)? kw.rows : [],
|
|
3094
3184
|
last_page: kw.pages || 1,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* delete_impact.js
|
|
3
|
+
*
|
|
4
|
+
* What a delete takes with it, read off the record itself.
|
|
5
|
+
*
|
|
6
|
+
* A treedb delete is not one thing. The backend refuses a node
|
|
7
|
+
* that has links — UNLESS the caller sends `force`, and the topic
|
|
8
|
+
* views do. With `force`:
|
|
9
|
+
*
|
|
10
|
+
* children are UNLINKED, not deleted. They survive, loose.
|
|
11
|
+
* parents lose the node from their hook.
|
|
12
|
+
*
|
|
13
|
+
* So "delete this row" can mean "detach eleven records from their
|
|
14
|
+
* only parent", and the question that used to be asked — `are you
|
|
15
|
+
* sure` — said none of it. This counts what is at stake so the
|
|
16
|
+
* question can name it.
|
|
17
|
+
*
|
|
18
|
+
* Read from the record the table ALREADY has (`list_dict` fills
|
|
19
|
+
* the hook and fkey columns), so asking costs no round trip.
|
|
20
|
+
*
|
|
21
|
+
* Pure and tested, because the shapes are the fiddly part: a hook
|
|
22
|
+
* or fkey value arrives as a list of refs, a dict keyed by id, or
|
|
23
|
+
* a single ref string, depending on the column's declared type.
|
|
24
|
+
*
|
|
25
|
+
* Copyright (c) 2026, ArtGins.
|
|
26
|
+
* All Rights Reserved.
|
|
27
|
+
***********************************************************************/
|
|
28
|
+
|
|
29
|
+
/***************************************************************
|
|
30
|
+
* How many things a hook/fkey value holds.
|
|
31
|
+
***************************************************************/
|
|
32
|
+
function ref_count(value)
|
|
33
|
+
{
|
|
34
|
+
if(Array.isArray(value)) {
|
|
35
|
+
return value.length;
|
|
36
|
+
}
|
|
37
|
+
if(value && typeof value === "object") {
|
|
38
|
+
return Object.keys(value).length;
|
|
39
|
+
}
|
|
40
|
+
if(typeof value === "string" && value.length > 0) {
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/***************************************************************
|
|
47
|
+
* delete_impact(desc, records) -> {records, children, parents}
|
|
48
|
+
*
|
|
49
|
+
* `records` is one record or a list of them, so the same answer
|
|
50
|
+
* serves the one-row delete and the selection.
|
|
51
|
+
***************************************************************/
|
|
52
|
+
function delete_impact(desc, records)
|
|
53
|
+
{
|
|
54
|
+
let list = Array.isArray(records)? records : (records? [records] : []);
|
|
55
|
+
let out = {records: list.length, children: 0, parents: 0};
|
|
56
|
+
|
|
57
|
+
let cols = (desc && Array.isArray(desc.cols))? desc.cols : [];
|
|
58
|
+
if(!cols.length) {
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for(let record of list) {
|
|
63
|
+
if(!record) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for(let col of cols) {
|
|
67
|
+
if(!col || !col.id || !Array.isArray(col.flag)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let n = ref_count(record[col.id]);
|
|
71
|
+
if(n === 0) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
/* A column can be BOTH (a department's `users` is a hook to its
|
|
75
|
+
* users and a fkey to the department that manages it). Counting
|
|
76
|
+
* it on both sides is right: the delete does both things. */
|
|
77
|
+
if(col.flag.indexOf("hook") >= 0) {
|
|
78
|
+
out.children += n;
|
|
79
|
+
}
|
|
80
|
+
if(col.flag.indexOf("fkey") >= 0) {
|
|
81
|
+
out.parents += n;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
export {delete_impact, ref_count};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
import {delete_impact, ref_count} from "./delete_impact.js";
|
|
3
|
+
|
|
4
|
+
const DESC = {
|
|
5
|
+
cols: [
|
|
6
|
+
{id: "id", flag: ["persistent", "required"]},
|
|
7
|
+
{id: "name", flag: ["persistent", "writable"]},
|
|
8
|
+
{id: "users", flag: ["hook"]},
|
|
9
|
+
{id: "parent", flag: ["fkey"]},
|
|
10
|
+
{id: "managers", flag: ["hook", "fkey"]}
|
|
11
|
+
]
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("ref_count", () => {
|
|
15
|
+
it("counts a list, a dict, a ref string and nothing", () => {
|
|
16
|
+
expect(ref_count([1, 2, 3])).toBe(3);
|
|
17
|
+
expect(ref_count({a: 1, b: 2})).toBe(2);
|
|
18
|
+
expect(ref_count("topic^id^hook")).toBe(1);
|
|
19
|
+
expect(ref_count("")).toBe(0);
|
|
20
|
+
expect(ref_count(null)).toBe(0);
|
|
21
|
+
expect(ref_count(undefined)).toBe(0);
|
|
22
|
+
expect(ref_count(7)).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("delete_impact", () => {
|
|
27
|
+
it("a loose record takes nothing with it", () => {
|
|
28
|
+
let i = delete_impact(DESC, {id: "a", name: "A"});
|
|
29
|
+
expect(i).toEqual({records: 1, children: 0, parents: 0});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("counts children off a hook and parents off a fkey", () => {
|
|
33
|
+
let i = delete_impact(DESC, {
|
|
34
|
+
id: "a", users: [{id: "u1"}, {id: "u2"}], parent: "roles^root^users"
|
|
35
|
+
});
|
|
36
|
+
expect(i.children).toBe(2);
|
|
37
|
+
expect(i.parents).toBe(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("a column that is BOTH counts on both sides: the delete does both", () => {
|
|
41
|
+
let i = delete_impact(DESC, {id: "a", managers: [{id: "m1"}, {id: "m2"}]});
|
|
42
|
+
expect(i.children).toBe(2);
|
|
43
|
+
expect(i.parents).toBe(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("adds up over a selection", () => {
|
|
47
|
+
let i = delete_impact(DESC, [
|
|
48
|
+
{id: "a", users: [{id: "u1"}]},
|
|
49
|
+
{id: "b", users: {u2: {}, u3: {}}, parent: "x^y^z"}
|
|
50
|
+
]);
|
|
51
|
+
expect(i.records).toBe(2);
|
|
52
|
+
expect(i.children).toBe(3);
|
|
53
|
+
expect(i.parents).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("says nothing it cannot know", () => {
|
|
57
|
+
expect(delete_impact(null, {id: "a"})).toEqual({records: 1, children: 0, parents: 0});
|
|
58
|
+
expect(delete_impact(DESC, [])).toEqual({records: 0, children: 0, parents: 0});
|
|
59
|
+
expect(delete_impact(DESC, null)).toEqual({records: 0, children: 0, parents: 0});
|
|
60
|
+
});
|
|
61
|
+
});
|