@yuneta/gobj-ui 7.9.2 → 7.10.1
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 +190 -5
- package/dist/gobj-ui.es.js +190 -5
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +25 -3
- package/src/c_yui_treedb_topic_with_form.js +70 -2
- 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
|
|
@@ -18219,7 +18284,7 @@ function ac_delete_rows(gobj, event, kw, src) {
|
|
|
18219
18284
|
});
|
|
18220
18285
|
return 0;
|
|
18221
18286
|
}
|
|
18222
|
-
yui_shell_confirm_yesnocancel(yui_shell_of(gobj),
|
|
18287
|
+
yui_shell_confirm_yesnocancel(yui_shell_of(gobj), build_delete_question(gobj, rows), {
|
|
18223
18288
|
t: i18next.t,
|
|
18224
18289
|
yes_label: "yes",
|
|
18225
18290
|
no_label: "no",
|
|
@@ -18230,7 +18295,7 @@ function ac_delete_rows(gobj, event, kw, src) {
|
|
|
18230
18295
|
record: row
|
|
18231
18296
|
});
|
|
18232
18297
|
});
|
|
18233
|
-
} 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), {
|
|
18234
18299
|
t: i18next.t,
|
|
18235
18300
|
yes_label: "yes",
|
|
18236
18301
|
no_label: "no",
|
|
@@ -18401,6 +18466,122 @@ function ac_show_schema(gobj, event, kw, src) {
|
|
|
18401
18466
|
return 0;
|
|
18402
18467
|
}
|
|
18403
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
|
+
const ROW = "display:flex; align-items:baseline; gap:.35rem; flex-wrap:wrap;";
|
|
18487
|
+
let lines = [];
|
|
18488
|
+
let pkey = desc && desc.pkey || "id";
|
|
18489
|
+
let one = impact.records === 1 && Array.isArray(records) ? records[0] : impact.records === 1 ? records : null;
|
|
18490
|
+
if (one && one[pkey] !== void 0 && one[pkey] !== null) lines.push([
|
|
18491
|
+
"p",
|
|
18492
|
+
{
|
|
18493
|
+
class: "DELETE_ASK_WHAT",
|
|
18494
|
+
style: ROW
|
|
18495
|
+
},
|
|
18496
|
+
[[
|
|
18497
|
+
"span",
|
|
18498
|
+
{ i18n: "delete" },
|
|
18499
|
+
(0, i18next.t)("delete")
|
|
18500
|
+
], [
|
|
18501
|
+
"strong",
|
|
18502
|
+
{},
|
|
18503
|
+
String(one[pkey])
|
|
18504
|
+
]]
|
|
18505
|
+
]);
|
|
18506
|
+
else lines.push([
|
|
18507
|
+
"p",
|
|
18508
|
+
{
|
|
18509
|
+
class: "DELETE_ASK_WHAT",
|
|
18510
|
+
style: ROW
|
|
18511
|
+
},
|
|
18512
|
+
[
|
|
18513
|
+
[
|
|
18514
|
+
"span",
|
|
18515
|
+
{ i18n: "delete" },
|
|
18516
|
+
(0, i18next.t)("delete")
|
|
18517
|
+
],
|
|
18518
|
+
[
|
|
18519
|
+
"strong",
|
|
18520
|
+
{},
|
|
18521
|
+
String(impact.records)
|
|
18522
|
+
],
|
|
18523
|
+
[
|
|
18524
|
+
"span",
|
|
18525
|
+
{ i18n: "records" },
|
|
18526
|
+
(0, i18next.t)("records")
|
|
18527
|
+
]
|
|
18528
|
+
]
|
|
18529
|
+
]);
|
|
18530
|
+
if (impact.children > 0) lines.push([
|
|
18531
|
+
"p",
|
|
18532
|
+
{
|
|
18533
|
+
class: "DELETE_ASK_CHILDREN has-text-weight-semibold",
|
|
18534
|
+
style: ROW
|
|
18535
|
+
},
|
|
18536
|
+
[[
|
|
18537
|
+
"strong",
|
|
18538
|
+
{},
|
|
18539
|
+
String(impact.children)
|
|
18540
|
+
], [
|
|
18541
|
+
"span",
|
|
18542
|
+
{ i18n: "children will be unlinked, not deleted" },
|
|
18543
|
+
(0, i18next.t)("children will be unlinked, not deleted")
|
|
18544
|
+
]]
|
|
18545
|
+
]);
|
|
18546
|
+
if (impact.parents > 0) lines.push([
|
|
18547
|
+
"p",
|
|
18548
|
+
{
|
|
18549
|
+
class: "DELETE_ASK_PARENTS",
|
|
18550
|
+
style: ROW
|
|
18551
|
+
},
|
|
18552
|
+
[
|
|
18553
|
+
[
|
|
18554
|
+
"span",
|
|
18555
|
+
{ i18n: "it will be detached from" },
|
|
18556
|
+
(0, i18next.t)("it will be detached from")
|
|
18557
|
+
],
|
|
18558
|
+
[
|
|
18559
|
+
"strong",
|
|
18560
|
+
{},
|
|
18561
|
+
String(impact.parents)
|
|
18562
|
+
],
|
|
18563
|
+
[
|
|
18564
|
+
"span",
|
|
18565
|
+
{ i18n: "parents" },
|
|
18566
|
+
(0, i18next.t)("parents")
|
|
18567
|
+
]
|
|
18568
|
+
]
|
|
18569
|
+
]);
|
|
18570
|
+
lines.push([
|
|
18571
|
+
"p",
|
|
18572
|
+
{
|
|
18573
|
+
class: "DELETE_ASK_SURE",
|
|
18574
|
+
i18n: "are you sure"
|
|
18575
|
+
},
|
|
18576
|
+
(0, i18next.t)("are you sure")
|
|
18577
|
+
]);
|
|
18578
|
+
return (0, _yuneta_gobj_js.createElement2)([
|
|
18579
|
+
"div",
|
|
18580
|
+
{ class: "DELETE_ASK" },
|
|
18581
|
+
lines
|
|
18582
|
+
]);
|
|
18583
|
+
}
|
|
18584
|
+
/************************************************************
|
|
18404
18585
|
* Ask the host for one page and park the promise Tabulator wants.
|
|
18405
18586
|
*
|
|
18406
18587
|
* The transport belongs to the HOST, so the request goes up as an event
|
|
@@ -54889,7 +55070,7 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
54889
55070
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
54890
55071
|
const popover = create_popover_base(iconRect.right - containerRect.left + 6, iconRect.top - containerRect.top - 4, "g6-confirm-popover", "#ff4d4f", 160);
|
|
54891
55072
|
let msg = document.createElement("div");
|
|
54892
|
-
msg.style.cssText = "margin-bottom:10px;font-weight:500;";
|
|
55073
|
+
msg.style.cssText = "margin-bottom:10px;font-weight:500;white-space:pre-line;";
|
|
54893
55074
|
msg.textContent = message;
|
|
54894
55075
|
popover.appendChild(msg);
|
|
54895
55076
|
create_form_button_row(popover, [{
|
|
@@ -56463,7 +56644,11 @@ function show_delete_confirm(gobj, nodeData) {
|
|
|
56463
56644
|
let priv = gobj.priv;
|
|
56464
56645
|
let record = nodeData.data.record || {};
|
|
56465
56646
|
let topic_name = nodeData.data.desc.topic_name;
|
|
56466
|
-
|
|
56647
|
+
let impact = delete_impact(nodeData.data.desc, record);
|
|
56648
|
+
let question = (0, i18next.t)("delete") + " " + topic_name + ": " + record.id + "?";
|
|
56649
|
+
if (impact.children > 0) question += "\n" + impact.children + " " + (0, i18next.t)("children will be unlinked, not deleted");
|
|
56650
|
+
if (impact.parents > 0) question += "\n" + (0, i18next.t)("it will be detached from") + " " + impact.parents + " " + (0, i18next.t)("parents");
|
|
56651
|
+
show_confirm_popover(gobj, priv._node_delete_el, question, "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
|
|
56467
56652
|
}
|
|
56468
56653
|
function hide_delete_confirm(gobj) {
|
|
56469
56654
|
hide_overlay(gobj, "_delete_confirm_el");
|
|
@@ -56621,7 +56806,7 @@ function execute_unlink_edge(gobj, edgeData) {
|
|
|
56621
56806
|
function show_unlink_confirm(gobj, edgeData) {
|
|
56622
56807
|
let priv = gobj.priv;
|
|
56623
56808
|
const d = edgeData.data;
|
|
56624
|
-
show_confirm_popover(gobj, priv._edge_delete_el, (0, i18next.t)("unlink") + " " + d.child_id + " → " + d.parent_id + "
|
|
56809
|
+
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");
|
|
56625
56810
|
}
|
|
56626
56811
|
function hide_unlink_confirm(gobj) {
|
|
56627
56812
|
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
|
|
@@ -18214,7 +18279,7 @@ function ac_delete_rows(gobj, event, kw, src) {
|
|
|
18214
18279
|
});
|
|
18215
18280
|
return 0;
|
|
18216
18281
|
}
|
|
18217
|
-
yui_shell_confirm_yesnocancel(yui_shell_of(gobj),
|
|
18282
|
+
yui_shell_confirm_yesnocancel(yui_shell_of(gobj), build_delete_question(gobj, rows), {
|
|
18218
18283
|
t,
|
|
18219
18284
|
yes_label: "yes",
|
|
18220
18285
|
no_label: "no",
|
|
@@ -18225,7 +18290,7 @@ function ac_delete_rows(gobj, event, kw, src) {
|
|
|
18225
18290
|
record: row
|
|
18226
18291
|
});
|
|
18227
18292
|
});
|
|
18228
|
-
} 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), {
|
|
18229
18294
|
t,
|
|
18230
18295
|
yes_label: "yes",
|
|
18231
18296
|
no_label: "no",
|
|
@@ -18396,6 +18461,122 @@ function ac_show_schema(gobj, event, kw, src) {
|
|
|
18396
18461
|
return 0;
|
|
18397
18462
|
}
|
|
18398
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
|
+
const ROW = "display:flex; align-items:baseline; gap:.35rem; flex-wrap:wrap;";
|
|
18482
|
+
let lines = [];
|
|
18483
|
+
let pkey = desc && desc.pkey || "id";
|
|
18484
|
+
let one = impact.records === 1 && Array.isArray(records) ? records[0] : impact.records === 1 ? records : null;
|
|
18485
|
+
if (one && one[pkey] !== void 0 && one[pkey] !== null) lines.push([
|
|
18486
|
+
"p",
|
|
18487
|
+
{
|
|
18488
|
+
class: "DELETE_ASK_WHAT",
|
|
18489
|
+
style: ROW
|
|
18490
|
+
},
|
|
18491
|
+
[[
|
|
18492
|
+
"span",
|
|
18493
|
+
{ i18n: "delete" },
|
|
18494
|
+
t("delete")
|
|
18495
|
+
], [
|
|
18496
|
+
"strong",
|
|
18497
|
+
{},
|
|
18498
|
+
String(one[pkey])
|
|
18499
|
+
]]
|
|
18500
|
+
]);
|
|
18501
|
+
else lines.push([
|
|
18502
|
+
"p",
|
|
18503
|
+
{
|
|
18504
|
+
class: "DELETE_ASK_WHAT",
|
|
18505
|
+
style: ROW
|
|
18506
|
+
},
|
|
18507
|
+
[
|
|
18508
|
+
[
|
|
18509
|
+
"span",
|
|
18510
|
+
{ i18n: "delete" },
|
|
18511
|
+
t("delete")
|
|
18512
|
+
],
|
|
18513
|
+
[
|
|
18514
|
+
"strong",
|
|
18515
|
+
{},
|
|
18516
|
+
String(impact.records)
|
|
18517
|
+
],
|
|
18518
|
+
[
|
|
18519
|
+
"span",
|
|
18520
|
+
{ i18n: "records" },
|
|
18521
|
+
t("records")
|
|
18522
|
+
]
|
|
18523
|
+
]
|
|
18524
|
+
]);
|
|
18525
|
+
if (impact.children > 0) lines.push([
|
|
18526
|
+
"p",
|
|
18527
|
+
{
|
|
18528
|
+
class: "DELETE_ASK_CHILDREN has-text-weight-semibold",
|
|
18529
|
+
style: ROW
|
|
18530
|
+
},
|
|
18531
|
+
[[
|
|
18532
|
+
"strong",
|
|
18533
|
+
{},
|
|
18534
|
+
String(impact.children)
|
|
18535
|
+
], [
|
|
18536
|
+
"span",
|
|
18537
|
+
{ i18n: "children will be unlinked, not deleted" },
|
|
18538
|
+
t("children will be unlinked, not deleted")
|
|
18539
|
+
]]
|
|
18540
|
+
]);
|
|
18541
|
+
if (impact.parents > 0) lines.push([
|
|
18542
|
+
"p",
|
|
18543
|
+
{
|
|
18544
|
+
class: "DELETE_ASK_PARENTS",
|
|
18545
|
+
style: ROW
|
|
18546
|
+
},
|
|
18547
|
+
[
|
|
18548
|
+
[
|
|
18549
|
+
"span",
|
|
18550
|
+
{ i18n: "it will be detached from" },
|
|
18551
|
+
t("it will be detached from")
|
|
18552
|
+
],
|
|
18553
|
+
[
|
|
18554
|
+
"strong",
|
|
18555
|
+
{},
|
|
18556
|
+
String(impact.parents)
|
|
18557
|
+
],
|
|
18558
|
+
[
|
|
18559
|
+
"span",
|
|
18560
|
+
{ i18n: "parents" },
|
|
18561
|
+
t("parents")
|
|
18562
|
+
]
|
|
18563
|
+
]
|
|
18564
|
+
]);
|
|
18565
|
+
lines.push([
|
|
18566
|
+
"p",
|
|
18567
|
+
{
|
|
18568
|
+
class: "DELETE_ASK_SURE",
|
|
18569
|
+
i18n: "are you sure"
|
|
18570
|
+
},
|
|
18571
|
+
t("are you sure")
|
|
18572
|
+
]);
|
|
18573
|
+
return createElement2([
|
|
18574
|
+
"div",
|
|
18575
|
+
{ class: "DELETE_ASK" },
|
|
18576
|
+
lines
|
|
18577
|
+
]);
|
|
18578
|
+
}
|
|
18579
|
+
/************************************************************
|
|
18399
18580
|
* Ask the host for one page and park the promise Tabulator wants.
|
|
18400
18581
|
*
|
|
18401
18582
|
* The transport belongs to the HOST, so the request goes up as an event
|
|
@@ -54876,7 +55057,7 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
54876
55057
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
54877
55058
|
const popover = create_popover_base(iconRect.right - containerRect.left + 6, iconRect.top - containerRect.top - 4, "g6-confirm-popover", "#ff4d4f", 160);
|
|
54878
55059
|
let msg = document.createElement("div");
|
|
54879
|
-
msg.style.cssText = "margin-bottom:10px;font-weight:500;";
|
|
55060
|
+
msg.style.cssText = "margin-bottom:10px;font-weight:500;white-space:pre-line;";
|
|
54880
55061
|
msg.textContent = message;
|
|
54881
55062
|
popover.appendChild(msg);
|
|
54882
55063
|
create_form_button_row(popover, [{
|
|
@@ -56450,7 +56631,11 @@ function show_delete_confirm(gobj, nodeData) {
|
|
|
56450
56631
|
let priv = gobj.priv;
|
|
56451
56632
|
let record = nodeData.data.record || {};
|
|
56452
56633
|
let topic_name = nodeData.data.desc.topic_name;
|
|
56453
|
-
|
|
56634
|
+
let impact = delete_impact(nodeData.data.desc, record);
|
|
56635
|
+
let question = t("delete") + " " + topic_name + ": " + record.id + "?";
|
|
56636
|
+
if (impact.children > 0) question += "\n" + impact.children + " " + t("children will be unlinked, not deleted");
|
|
56637
|
+
if (impact.parents > 0) question += "\n" + t("it will be detached from") + " " + impact.parents + " " + t("parents");
|
|
56638
|
+
show_confirm_popover(gobj, priv._node_delete_el, question, "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
|
|
56454
56639
|
}
|
|
56455
56640
|
function hide_delete_confirm(gobj) {
|
|
56456
56641
|
hide_overlay(gobj, "_delete_confirm_el");
|
|
@@ -56608,7 +56793,7 @@ function execute_unlink_edge(gobj, edgeData) {
|
|
|
56608
56793
|
function show_unlink_confirm(gobj, edgeData) {
|
|
56609
56794
|
let priv = gobj.priv;
|
|
56610
56795
|
const d = edgeData.data;
|
|
56611
|
-
show_confirm_popover(gobj, priv._edge_delete_el, t("unlink") + " " + d.child_id + " → " + d.parent_id + "
|
|
56796
|
+
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");
|
|
56612
56797
|
}
|
|
56613
56798
|
function hide_unlink_confirm(gobj) {
|
|
56614
56799
|
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";
|
|
@@ -2724,7 +2725,7 @@ function ac_delete_rows(gobj, event, kw, src)
|
|
|
2724
2725
|
}
|
|
2725
2726
|
|
|
2726
2727
|
yui_shell_confirm_yesnocancel(
|
|
2727
|
-
yui_shell_of(gobj),
|
|
2728
|
+
yui_shell_of(gobj), build_delete_question(gobj, rows),
|
|
2728
2729
|
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
2729
2730
|
).then(function(answer) {
|
|
2730
2731
|
if(answer === "yes") {
|
|
@@ -2748,7 +2749,7 @@ function ac_delete_rows(gobj, event, kw, src)
|
|
|
2748
2749
|
* {index: , row: }
|
|
2749
2750
|
*----------------------------*/
|
|
2750
2751
|
yui_shell_confirm_yesnocancel(
|
|
2751
|
-
yui_shell_of(gobj),
|
|
2752
|
+
yui_shell_of(gobj), build_delete_question(gobj, kw.row),
|
|
2752
2753
|
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
2753
2754
|
).then(function(answer) {
|
|
2754
2755
|
if(answer === "yes") {
|
|
@@ -3019,6 +3020,73 @@ function ac_show_schema(gobj, event, kw, src)
|
|
|
3019
3020
|
return 0;
|
|
3020
3021
|
}
|
|
3021
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
|
+
/* Every line is a flex row with a GAP, and the words carry no spaces of
|
|
3044
|
+
* their own: `createElement2` TRIMS text nodes, so a `["span", {}, " "]`
|
|
3045
|
+
* separator vanishes and the question reads "BorrarDeveloper". Spacing
|
|
3046
|
+
* is CSS here, as it has to be anywhere text is composed from keys. */
|
|
3047
|
+
const ROW = "display:flex; align-items:baseline; gap:.35rem; flex-wrap:wrap;";
|
|
3048
|
+
let lines = [];
|
|
3049
|
+
|
|
3050
|
+
/* What is being deleted: one named record, or how many. */
|
|
3051
|
+
let pkey = (desc && desc.pkey) || "id";
|
|
3052
|
+
let one = (impact.records === 1 && Array.isArray(records))? records[0] :
|
|
3053
|
+
(impact.records === 1? records : null);
|
|
3054
|
+
if(one && one[pkey] !== undefined && one[pkey] !== null) {
|
|
3055
|
+
lines.push(["p", {class: "DELETE_ASK_WHAT", style: ROW}, [
|
|
3056
|
+
["span", {i18n: "delete"}, t("delete")],
|
|
3057
|
+
["strong", {}, String(one[pkey])]
|
|
3058
|
+
]]);
|
|
3059
|
+
} else {
|
|
3060
|
+
lines.push(["p", {class: "DELETE_ASK_WHAT", style: ROW}, [
|
|
3061
|
+
["span", {i18n: "delete"}, t("delete")],
|
|
3062
|
+
["strong", {}, String(impact.records)],
|
|
3063
|
+
["span", {i18n: "records"}, t("records")]
|
|
3064
|
+
]]);
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
/* What goes with it. Said only when there IS something: a loose
|
|
3068
|
+
* record must not be dressed up as a dangerous one. */
|
|
3069
|
+
if(impact.children > 0) {
|
|
3070
|
+
lines.push(["p", {class: "DELETE_ASK_CHILDREN has-text-weight-semibold",
|
|
3071
|
+
style: ROW}, [
|
|
3072
|
+
["strong", {}, String(impact.children)],
|
|
3073
|
+
["span", {i18n: "children will be unlinked, not deleted"},
|
|
3074
|
+
t("children will be unlinked, not deleted")]
|
|
3075
|
+
]]);
|
|
3076
|
+
}
|
|
3077
|
+
if(impact.parents > 0) {
|
|
3078
|
+
lines.push(["p", {class: "DELETE_ASK_PARENTS", style: ROW}, [
|
|
3079
|
+
["span", {i18n: "it will be detached from"}, t("it will be detached from")],
|
|
3080
|
+
["strong", {}, String(impact.parents)],
|
|
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
|
+
|
|
3022
3090
|
/************************************************************
|
|
3023
3091
|
* Ask the host for one page and park the promise Tabulator wants.
|
|
3024
3092
|
*
|
|
@@ -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
|
+
});
|