@rogatio/cli 6.3.0 → 6.4.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/dist/editor/index.css +22 -4
- package/dist/editor/index.js +623 -115
- package/dist/node/index.js +75 -8
- package/package.json +1 -1
package/dist/editor/index.css
CHANGED
|
@@ -395,6 +395,28 @@
|
|
|
395
395
|
margin: 0;
|
|
396
396
|
min-width: 0;
|
|
397
397
|
}
|
|
398
|
+
.rogatio-editor [data-rename-controls] {
|
|
399
|
+
display: flex;
|
|
400
|
+
flex: 1 1 16rem;
|
|
401
|
+
align-items: center;
|
|
402
|
+
gap: 0.35rem;
|
|
403
|
+
min-width: 0;
|
|
404
|
+
}
|
|
405
|
+
.rogatio-editor [data-rename-controls] input {
|
|
406
|
+
flex: 1 1 auto;
|
|
407
|
+
width: auto;
|
|
408
|
+
min-width: 0;
|
|
409
|
+
max-width: 32rem;
|
|
410
|
+
}
|
|
411
|
+
.rogatio-editor button[data-icon] {
|
|
412
|
+
flex: none;
|
|
413
|
+
width: 1.8rem;
|
|
414
|
+
min-height: 1.8rem;
|
|
415
|
+
padding: 0.15rem;
|
|
416
|
+
font-family: var(--rogatio-font-mono);
|
|
417
|
+
font-size: 0.9rem;
|
|
418
|
+
line-height: 1;
|
|
419
|
+
}
|
|
398
420
|
.rogatio-editor [data-group-list] {
|
|
399
421
|
list-style: none;
|
|
400
422
|
margin: 0;
|
|
@@ -413,10 +435,6 @@
|
|
|
413
435
|
background: var(--rogatio-card-group);
|
|
414
436
|
padding: 0.65rem 0.85rem;
|
|
415
437
|
}
|
|
416
|
-
.rogatio-editor [data-group-card] {
|
|
417
|
-
background: var(--rogatio-card-group);
|
|
418
|
-
border-color: rgba(148, 163, 184, 0.2);
|
|
419
|
-
}
|
|
420
438
|
.rogatio-editor [data-rules-section] {
|
|
421
439
|
display: grid;
|
|
422
440
|
gap: 0.85rem;
|
package/dist/editor/index.js
CHANGED
|
@@ -185,6 +185,56 @@ var FORBIDDEN_RESPONSE_HEADERS = Object.freeze([
|
|
|
185
185
|
"via"
|
|
186
186
|
]);
|
|
187
187
|
var FORBIDDEN_REQUEST_PREFIXES = Object.freeze(["proxy-", "sec-"]);
|
|
188
|
+
var NON_ALPHANUMERIC = /[^a-z0-9]+/u;
|
|
189
|
+
var WHITESPACE_RUN = /\s+/gu;
|
|
190
|
+
var FALLBACK_PREFIX = Object.freeze({
|
|
191
|
+
group: "Group",
|
|
192
|
+
rule: "Rule"
|
|
193
|
+
});
|
|
194
|
+
function normalizeNameKey(name) {
|
|
195
|
+
if (typeof name !== "string") return "";
|
|
196
|
+
return name.trim().replace(WHITESPACE_RUN, " ").toLowerCase();
|
|
197
|
+
}
|
|
198
|
+
function pascalCase(value) {
|
|
199
|
+
const tokens = value.toLowerCase().split(NON_ALPHANUMERIC);
|
|
200
|
+
let result = "";
|
|
201
|
+
for (const token of tokens) {
|
|
202
|
+
const first = token.charAt(0);
|
|
203
|
+
if (first.length === 0) continue;
|
|
204
|
+
result += first.toUpperCase() + token.slice(1);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
function allocate(stem, reserved, maxLength) {
|
|
209
|
+
for (let suffix = 1; ; suffix += 1) {
|
|
210
|
+
const suffixText = suffix === 1 ? "" : String(suffix);
|
|
211
|
+
const room = Math.max(1, maxLength - suffixText.length);
|
|
212
|
+
const candidate = `${stem.slice(0, room)}${suffixText}`;
|
|
213
|
+
if (reserved.has(candidate)) continue;
|
|
214
|
+
reserved.add(candidate);
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function deriveEntityId(name, kind, reserved) {
|
|
219
|
+
const derived = pascalCase(typeof name === "string" ? name : "");
|
|
220
|
+
return allocate(
|
|
221
|
+
derived.length > 0 ? derived : FALLBACK_PREFIX[kind],
|
|
222
|
+
reserved,
|
|
223
|
+
LIMITS.maxIdLength
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
function uniqueName(base, reserved) {
|
|
227
|
+
const label = typeof base === "string" ? base : "";
|
|
228
|
+
for (let suffix = 1; ; suffix += 1) {
|
|
229
|
+
const suffixText = suffix === 1 ? "" : ` ${suffix}`;
|
|
230
|
+
const room = Math.max(1, LIMITS.maxLabelLength - suffixText.length);
|
|
231
|
+
const candidate = `${label.slice(0, room)}${suffixText}`;
|
|
232
|
+
const key = normalizeNameKey(candidate);
|
|
233
|
+
if (reserved.has(key)) continue;
|
|
234
|
+
reserved.add(key);
|
|
235
|
+
return candidate;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
188
238
|
var RESOURCE_TYPES = Object.freeze([
|
|
189
239
|
"main_frame",
|
|
190
240
|
"sub_frame",
|
|
@@ -2131,6 +2181,14 @@ var EditorControllerImpl = class {
|
|
|
2131
2181
|
aiAssistInFlight = false;
|
|
2132
2182
|
aiRepairTargets = [];
|
|
2133
2183
|
migrationNoticesDismissed = false;
|
|
2184
|
+
/**
|
|
2185
|
+
* The entity whose name is being edited in place, if any. The buffer lives
|
|
2186
|
+
* here rather than in the DOM because every render rebuilds the heading, and
|
|
2187
|
+
* the draft is deliberately untouched until an explicit commit.
|
|
2188
|
+
*/
|
|
2189
|
+
renameTarget;
|
|
2190
|
+
/** Re-entrancy guard: committing a rename renders, which asks again. */
|
|
2191
|
+
renamingInProgress = false;
|
|
2134
2192
|
constructor(options, initial, extensions) {
|
|
2135
2193
|
this.root = options.root;
|
|
2136
2194
|
this.document = options.root.ownerDocument;
|
|
@@ -2278,6 +2336,10 @@ var EditorControllerImpl = class {
|
|
|
2278
2336
|
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
|
|
2279
2337
|
return;
|
|
2280
2338
|
}
|
|
2339
|
+
if (target.dataset.renameInput !== void 0) {
|
|
2340
|
+
if (this.renameTarget) this.renameTarget.value = target.value;
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2281
2343
|
if (target.dataset.search !== void 0) {
|
|
2282
2344
|
this.searchQuery = target.value;
|
|
2283
2345
|
this.render();
|
|
@@ -2345,14 +2407,36 @@ var EditorControllerImpl = class {
|
|
|
2345
2407
|
};
|
|
2346
2408
|
handleSubmit = (event) => {
|
|
2347
2409
|
event.preventDefault();
|
|
2410
|
+
if (this.renameTarget && this.renameInputFocused()) {
|
|
2411
|
+
this.commitRename();
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2348
2414
|
this.dispatchCommand("save", this.form);
|
|
2349
2415
|
};
|
|
2416
|
+
renameInputFocused() {
|
|
2417
|
+
const active = this.document.activeElement;
|
|
2418
|
+
return active instanceof HTMLElement && active.dataset.renameInput !== void 0;
|
|
2419
|
+
}
|
|
2350
2420
|
handleKeydown = (event) => {
|
|
2351
2421
|
if (event.key === "Escape" && this.confirmation) {
|
|
2352
2422
|
event.preventDefault();
|
|
2353
2423
|
this.confirmation = void 0;
|
|
2354
2424
|
this.statusMessage = "No changes were discarded.";
|
|
2355
2425
|
this.render();
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
const target = event.target;
|
|
2429
|
+
const inRenameInput = target instanceof HTMLElement && target.dataset.renameInput !== void 0;
|
|
2430
|
+
if (!inRenameInput) return;
|
|
2431
|
+
if (this.composing || event.isComposing) return;
|
|
2432
|
+
if (event.key === "Enter") {
|
|
2433
|
+
event.preventDefault();
|
|
2434
|
+
this.commitRename();
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
if (event.key === "Escape") {
|
|
2438
|
+
event.preventDefault();
|
|
2439
|
+
this.cancelRename();
|
|
2356
2440
|
}
|
|
2357
2441
|
};
|
|
2358
2442
|
handleCompositionStart = () => {
|
|
@@ -2449,6 +2533,31 @@ var EditorControllerImpl = class {
|
|
|
2449
2533
|
if (groupId && ruleId) this.copyRule(groupId, ruleId);
|
|
2450
2534
|
return;
|
|
2451
2535
|
}
|
|
2536
|
+
if (command === "rename-entity") {
|
|
2537
|
+
const groupId = element.dataset.groupId;
|
|
2538
|
+
if (!groupId) return;
|
|
2539
|
+
const ruleId = element.dataset.ruleId;
|
|
2540
|
+
this.openRename(
|
|
2541
|
+
element.dataset.renameKind === "rule" ? "rule" : "group",
|
|
2542
|
+
groupId,
|
|
2543
|
+
ruleId
|
|
2544
|
+
);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
if (command === "commit-rename") {
|
|
2548
|
+
this.commitRename();
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
if (command === "cancel-rename") {
|
|
2552
|
+
this.cancelRename();
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
if (command === "repair-id") {
|
|
2556
|
+
const path = element.dataset.repairPath;
|
|
2557
|
+
if (!path) return;
|
|
2558
|
+
this.repairEntityId(path);
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2452
2561
|
if (command === "dismiss-migration-notices") {
|
|
2453
2562
|
void this.dismissMigrationNotices();
|
|
2454
2563
|
return;
|
|
@@ -2683,15 +2792,24 @@ var EditorControllerImpl = class {
|
|
|
2683
2792
|
}
|
|
2684
2793
|
return ids;
|
|
2685
2794
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2795
|
+
/**
|
|
2796
|
+
* Every name currently in the project, keyed for uniqueness comparison. Names
|
|
2797
|
+
* are unique per project across groups and rules together, so a new entity
|
|
2798
|
+
* must claim a free one or the project it produces is invalid.
|
|
2799
|
+
*/
|
|
2800
|
+
allNameKeys() {
|
|
2801
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
2802
|
+
for (const group of this.draft.groups) {
|
|
2803
|
+
if (typeof group.name === "string") {
|
|
2804
|
+
reserved.add(normalizeNameKey(group.name));
|
|
2805
|
+
}
|
|
2806
|
+
for (const rule of group.rules) {
|
|
2807
|
+
if (typeof rule.name === "string") {
|
|
2808
|
+
reserved.add(normalizeNameKey(rule.name));
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
return reserved;
|
|
2695
2813
|
}
|
|
2696
2814
|
copyLabel(name) {
|
|
2697
2815
|
const base = typeof name === "string" ? name : "";
|
|
@@ -2703,42 +2821,35 @@ var EditorControllerImpl = class {
|
|
|
2703
2821
|
}
|
|
2704
2822
|
addGroup() {
|
|
2705
2823
|
if (this.saving) return;
|
|
2706
|
-
const
|
|
2824
|
+
const name = uniqueName("New group", this.allNameKeys());
|
|
2825
|
+
const groupId = deriveEntityId(name, "group", this.allIds());
|
|
2707
2826
|
this.draft.groups.push({
|
|
2708
2827
|
id: groupId,
|
|
2709
|
-
name
|
|
2828
|
+
name,
|
|
2710
2829
|
rules: []
|
|
2711
2830
|
});
|
|
2712
2831
|
this.markChanged();
|
|
2713
2832
|
this.route = { kind: "group", groupId };
|
|
2714
|
-
this.focusRequest = pointer("groups", this.draft.groups.length - 1, "name");
|
|
2715
2833
|
this.statusMessage = "Group added.";
|
|
2716
|
-
this.
|
|
2834
|
+
this.openRename("group", groupId);
|
|
2717
2835
|
}
|
|
2718
2836
|
addRule(groupId) {
|
|
2719
2837
|
if (this.saving) return;
|
|
2720
2838
|
const group = this.groupById(groupId);
|
|
2721
2839
|
if (!group) return;
|
|
2722
|
-
const
|
|
2840
|
+
const name = uniqueName("New rule", this.allNameKeys());
|
|
2841
|
+
const ruleId = deriveEntityId(name, "rule", this.allIds());
|
|
2723
2842
|
group.rules.push({
|
|
2724
2843
|
id: ruleId,
|
|
2725
|
-
name
|
|
2844
|
+
name,
|
|
2726
2845
|
source: { key: "url", operator: "regex", value: "" },
|
|
2727
2846
|
resourceTypes: ["main_frame"],
|
|
2728
2847
|
priority: 100
|
|
2729
2848
|
});
|
|
2730
2849
|
this.markChanged();
|
|
2731
2850
|
this.route = { kind: "group", groupId };
|
|
2732
|
-
const groupIndex = this.groupIndex(groupId);
|
|
2733
|
-
this.focusRequest = pointer(
|
|
2734
|
-
"groups",
|
|
2735
|
-
groupIndex,
|
|
2736
|
-
"rules",
|
|
2737
|
-
group.rules.length - 1,
|
|
2738
|
-
"name"
|
|
2739
|
-
);
|
|
2740
2851
|
this.statusMessage = "Rule added.";
|
|
2741
|
-
this.
|
|
2852
|
+
this.openRename("rule", groupId, ruleId);
|
|
2742
2853
|
}
|
|
2743
2854
|
copyRule(groupId, ruleId) {
|
|
2744
2855
|
if (this.saving) return;
|
|
@@ -2753,21 +2864,16 @@ var EditorControllerImpl = class {
|
|
|
2753
2864
|
return;
|
|
2754
2865
|
}
|
|
2755
2866
|
const copied = snapshot.value;
|
|
2756
|
-
copied.
|
|
2757
|
-
|
|
2867
|
+
copied.name = uniqueName(
|
|
2868
|
+
this.copyLabel(group.rules[sourceIndex].name),
|
|
2869
|
+
this.allNameKeys()
|
|
2870
|
+
);
|
|
2871
|
+
copied.id = deriveEntityId(copied.name, "rule", this.allIds());
|
|
2758
2872
|
group.rules.splice(sourceIndex + 1, 0, copied);
|
|
2759
2873
|
this.markChanged();
|
|
2760
2874
|
this.route = { kind: "group", groupId };
|
|
2761
|
-
const groupIndex = this.groupIndex(groupId);
|
|
2762
|
-
this.focusRequest = pointer(
|
|
2763
|
-
"groups",
|
|
2764
|
-
groupIndex,
|
|
2765
|
-
"rules",
|
|
2766
|
-
sourceIndex + 1,
|
|
2767
|
-
"name"
|
|
2768
|
-
);
|
|
2769
2875
|
this.statusMessage = "Rule copied.";
|
|
2770
|
-
this.
|
|
2876
|
+
this.openRename("rule", groupId, String(copied.id));
|
|
2771
2877
|
}
|
|
2772
2878
|
copyGroup(groupId) {
|
|
2773
2879
|
if (this.saving) return;
|
|
@@ -2786,18 +2892,20 @@ var EditorControllerImpl = class {
|
|
|
2786
2892
|
this.render();
|
|
2787
2893
|
return;
|
|
2788
2894
|
}
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
copied.name = this.copyLabel(source.name);
|
|
2895
|
+
const names = this.allNameKeys();
|
|
2896
|
+
const ids = this.allIds();
|
|
2897
|
+
copied.name = uniqueName(this.copyLabel(source.name), names);
|
|
2898
|
+
copied.id = deriveEntityId(copied.name, "group", ids);
|
|
2792
2899
|
for (const rule of copied.rules) {
|
|
2793
|
-
rule.
|
|
2900
|
+
if (typeof rule.name === "string") {
|
|
2901
|
+
rule.name = uniqueName(rule.name, names);
|
|
2902
|
+
}
|
|
2903
|
+
rule.id = deriveEntityId(rule.name, "rule", ids);
|
|
2794
2904
|
}
|
|
2795
2905
|
this.draft.groups.splice(sourceIndex + 1, 0, copied);
|
|
2796
2906
|
this.markChanged();
|
|
2797
|
-
this.route = { kind: "group", groupId: String(copied.id) };
|
|
2798
|
-
this.focusRequest = pointer("groups", sourceIndex + 1, "name");
|
|
2799
2907
|
this.statusMessage = "Group copied.";
|
|
2800
|
-
this.
|
|
2908
|
+
this.openRename("group", String(copied.id));
|
|
2801
2909
|
}
|
|
2802
2910
|
moveGroup(groupId, delta) {
|
|
2803
2911
|
if (this.saving) return;
|
|
@@ -2855,6 +2963,9 @@ var EditorControllerImpl = class {
|
|
|
2855
2963
|
if (!this.confirmation || this.confirmation.kind === "cancel") return;
|
|
2856
2964
|
const confirmation = this.confirmation;
|
|
2857
2965
|
this.confirmation = void 0;
|
|
2966
|
+
this.closeRenameFor(
|
|
2967
|
+
confirmation.kind === "remove-group" ? { groupId: confirmation.groupId, ruleId: void 0 } : { groupId: confirmation.groupId, ruleId: confirmation.ruleId }
|
|
2968
|
+
);
|
|
2858
2969
|
if (confirmation.kind === "remove-group") {
|
|
2859
2970
|
const index = this.groupIndex(confirmation.groupId);
|
|
2860
2971
|
if (index < 0) return;
|
|
@@ -2901,6 +3012,7 @@ var EditorControllerImpl = class {
|
|
|
2901
3012
|
this.errors = [];
|
|
2902
3013
|
this.conversionDiagnostics.clear();
|
|
2903
3014
|
this.confirmation = void 0;
|
|
3015
|
+
this.renameTarget = void 0;
|
|
2904
3016
|
this.statusMessage = "Changes discarded.";
|
|
2905
3017
|
try {
|
|
2906
3018
|
this.options.onCancel?.();
|
|
@@ -2960,13 +3072,14 @@ var EditorControllerImpl = class {
|
|
|
2960
3072
|
applyAIProposal(proposal) {
|
|
2961
3073
|
const repairs = [...this.aiRepairTargets];
|
|
2962
3074
|
this.aiRepairTargets = [];
|
|
3075
|
+
let lastApplied;
|
|
2963
3076
|
for (const ruleProposal of proposal.rules) {
|
|
2964
3077
|
let group = this.groupById(ruleProposal.groupId);
|
|
2965
3078
|
if (!group) {
|
|
2966
3079
|
const groupId = ruleProposal.groupId;
|
|
2967
3080
|
this.draft.groups.push({
|
|
2968
3081
|
id: groupId,
|
|
2969
|
-
name: "AI Group",
|
|
3082
|
+
name: uniqueName("AI Group", this.allNameKeys()),
|
|
2970
3083
|
rules: []
|
|
2971
3084
|
});
|
|
2972
3085
|
group = this.groupById(groupId);
|
|
@@ -2977,7 +3090,11 @@ var EditorControllerImpl = class {
|
|
|
2977
3090
|
(target) => target.groupId === ruleProposal.groupId && rules.some((existing) => String(existing.id) === target.ruleId)
|
|
2978
3091
|
);
|
|
2979
3092
|
let repairIndex = -1;
|
|
2980
|
-
|
|
3093
|
+
const proposedName = uniqueName(
|
|
3094
|
+
ruleProposal.name,
|
|
3095
|
+
repair === void 0 ? this.allNameKeys() : this.reservedNameKeys(repair)
|
|
3096
|
+
);
|
|
3097
|
+
let ruleId = repair === void 0 ? deriveEntityId(proposedName, "rule", this.allIds()) : repair.ruleId;
|
|
2981
3098
|
if (repair) {
|
|
2982
3099
|
repairs.splice(repairs.indexOf(repair), 1);
|
|
2983
3100
|
ruleId = repair.ruleId;
|
|
@@ -2987,7 +3104,7 @@ var EditorControllerImpl = class {
|
|
|
2987
3104
|
}
|
|
2988
3105
|
const rule = {
|
|
2989
3106
|
id: ruleId,
|
|
2990
|
-
name:
|
|
3107
|
+
name: proposedName,
|
|
2991
3108
|
source: {
|
|
2992
3109
|
key: ruleProposal.source.key,
|
|
2993
3110
|
operator: "regex",
|
|
@@ -3006,16 +3123,13 @@ var EditorControllerImpl = class {
|
|
|
3006
3123
|
rules.push(rule);
|
|
3007
3124
|
}
|
|
3008
3125
|
this.markChanged();
|
|
3009
|
-
|
|
3010
|
-
this.focusRequest = pointer(
|
|
3011
|
-
"groups",
|
|
3012
|
-
this.groupIndex(groupIdStr),
|
|
3013
|
-
"rules",
|
|
3014
|
-
repairIndex >= 0 ? repairIndex : rules.length - 1,
|
|
3015
|
-
"name"
|
|
3016
|
-
);
|
|
3126
|
+
lastApplied = { groupId: String(group.id), ruleId: String(rule.id) };
|
|
3017
3127
|
}
|
|
3018
3128
|
this.statusMessage = `Applied ${proposal.rules.length} rule${proposal.rules.length === 1 ? "" : "s"} from AI.`;
|
|
3129
|
+
if (lastApplied) {
|
|
3130
|
+
this.openRename("rule", lastApplied.groupId, lastApplied.ruleId);
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3019
3133
|
this.render();
|
|
3020
3134
|
}
|
|
3021
3135
|
/**
|
|
@@ -3259,7 +3373,7 @@ var EditorControllerImpl = class {
|
|
|
3259
3373
|
const ruleHeader = this.document.createElement("div");
|
|
3260
3374
|
ruleHeader.dataset.testRuleHeader = "true";
|
|
3261
3375
|
const ruleName = this.document.createElement("strong");
|
|
3262
|
-
ruleName.textContent =
|
|
3376
|
+
ruleName.textContent = this.testRuleLabel(rule.groupId, rule.ruleId);
|
|
3263
3377
|
const ruleBadge = this.document.createElement("span");
|
|
3264
3378
|
ruleBadge.dataset.testBadge = "true";
|
|
3265
3379
|
ruleBadge.dataset.variant = rule.matched ? "matched" : "unmatched";
|
|
@@ -3295,6 +3409,20 @@ var EditorControllerImpl = class {
|
|
|
3295
3409
|
resultsSection.append(card);
|
|
3296
3410
|
}
|
|
3297
3411
|
}
|
|
3412
|
+
/**
|
|
3413
|
+
* A test-result row is identified by the names the user actually sees. The dry
|
|
3414
|
+
* result carries ids only, and the draft the editor handed over is the source
|
|
3415
|
+
* of truth for both, so no contract changes. A rule that is no longer in the
|
|
3416
|
+
* draft falls back to its id rather than rendering a blank row.
|
|
3417
|
+
*/
|
|
3418
|
+
testRuleLabel(groupId, ruleId) {
|
|
3419
|
+
const group = this.groupById(groupId);
|
|
3420
|
+
if (!group) return `${groupId}/${ruleId}`;
|
|
3421
|
+
const groupName = displayName(group.name, "Unnamed group");
|
|
3422
|
+
const rule = this.ruleById(groupId, ruleId);
|
|
3423
|
+
if (!rule) return `${groupName} / ${ruleId}`;
|
|
3424
|
+
return `${groupName} / ${displayName(rule.name, "Unnamed rule")}`;
|
|
3425
|
+
}
|
|
3298
3426
|
navigate(route, groupId) {
|
|
3299
3427
|
if (route === "project") {
|
|
3300
3428
|
this.route = { kind: "project" };
|
|
@@ -3305,10 +3433,30 @@ var EditorControllerImpl = class {
|
|
|
3305
3433
|
} else {
|
|
3306
3434
|
return;
|
|
3307
3435
|
}
|
|
3436
|
+
this.closeRenameOnLeave();
|
|
3308
3437
|
this.testRequestId += 1;
|
|
3309
3438
|
this.statusMessage = "";
|
|
3310
3439
|
this.render();
|
|
3311
3440
|
}
|
|
3441
|
+
/**
|
|
3442
|
+
* An uncommitted rename belongs to the entity it was started on. Leaving that
|
|
3443
|
+
* entity, removing it, or discarding the draft closes the editor, so no buffer
|
|
3444
|
+
* survives against a heading that is no longer on screen.
|
|
3445
|
+
*/
|
|
3446
|
+
closeRenameOnLeave() {
|
|
3447
|
+
const target = this.renameTarget;
|
|
3448
|
+
if (!target) return;
|
|
3449
|
+
const onGroup = this.route.kind === "group" && this.route.groupId === target.groupId;
|
|
3450
|
+
if (!onGroup) this.renameTarget = void 0;
|
|
3451
|
+
}
|
|
3452
|
+
/** Close an open rename when the entity it belongs to goes away. */
|
|
3453
|
+
closeRenameFor(entity) {
|
|
3454
|
+
const target = this.renameTarget;
|
|
3455
|
+
if (!target) return;
|
|
3456
|
+
if (target.groupId === entity.groupId && target.ruleId === entity.ruleId) {
|
|
3457
|
+
this.renameTarget = void 0;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3312
3460
|
navigateToGroup(groupId) {
|
|
3313
3461
|
const groupIds = new Set(
|
|
3314
3462
|
this.draft.groups.map((group) => typeof group.id === "string" ? group.id : "").filter((id) => id.length > 0)
|
|
@@ -3334,11 +3482,22 @@ var EditorControllerImpl = class {
|
|
|
3334
3482
|
);
|
|
3335
3483
|
for (const button of buttons) {
|
|
3336
3484
|
const groupId = button.dataset.groupId ?? "";
|
|
3337
|
-
const
|
|
3338
|
-
const groupName =
|
|
3485
|
+
const draftGroup = this.draftGroupForSavedId(groupId);
|
|
3486
|
+
const groupName = displayName(
|
|
3487
|
+
draftGroup?.name,
|
|
3488
|
+
button.parentElement?.querySelector("h2")?.textContent?.trim() ?? ""
|
|
3489
|
+
);
|
|
3339
3490
|
this.applyGroupEnablementLabel(button, enabled.has(groupId), groupName);
|
|
3340
3491
|
}
|
|
3341
3492
|
}
|
|
3493
|
+
/** The draft group bound to a committed id, through the index-based binding. */
|
|
3494
|
+
draftGroupForSavedId(savedId) {
|
|
3495
|
+
if (savedId.length === 0) return void 0;
|
|
3496
|
+
for (const group of this.draft.groups) {
|
|
3497
|
+
if (this.savedGroupId(group) === savedId) return group;
|
|
3498
|
+
}
|
|
3499
|
+
return void 0;
|
|
3500
|
+
}
|
|
3342
3501
|
/**
|
|
3343
3502
|
* Scroll a rendered rule card into view and focus it. Explicit `behavior`
|
|
3344
3503
|
* overrides the stylesheet's `scroll-behavior`, so the reduced-motion
|
|
@@ -3400,10 +3559,14 @@ var EditorControllerImpl = class {
|
|
|
3400
3559
|
return;
|
|
3401
3560
|
}
|
|
3402
3561
|
const groupIndex = arrayIndex(segments[1] ?? "");
|
|
3403
|
-
|
|
3562
|
+
if (groupIndex === void 0) return;
|
|
3563
|
+
const group = this.draft.groups[groupIndex];
|
|
3404
3564
|
if (!group || typeof group.id !== "string") return;
|
|
3405
3565
|
this.route = { kind: "group", groupId: group.id };
|
|
3406
|
-
|
|
3566
|
+
const last = segments[segments.length - 1];
|
|
3567
|
+
const ruleIndex = arrayIndex(segments[3] ?? "");
|
|
3568
|
+
const entityPath = last === "name" ? path : segments.length === 2 ? pointer("groups", groupIndex, "name") : segments.length === 4 && ruleIndex !== void 0 ? pointer("groups", groupIndex, "rules", ruleIndex, "name") : path;
|
|
3569
|
+
this.focusRequest = entityPath;
|
|
3407
3570
|
this.render();
|
|
3408
3571
|
}
|
|
3409
3572
|
currentGroupId() {
|
|
@@ -3433,6 +3596,7 @@ var EditorControllerImpl = class {
|
|
|
3433
3596
|
}
|
|
3434
3597
|
render() {
|
|
3435
3598
|
if (this.destroyed) return;
|
|
3599
|
+
this.composing = false;
|
|
3436
3600
|
this.previousFocus = this.captureFocus();
|
|
3437
3601
|
this.cleanupExtensions();
|
|
3438
3602
|
this.controlNumber = 0;
|
|
@@ -3678,7 +3842,6 @@ var EditorControllerImpl = class {
|
|
|
3678
3842
|
const headingRow = this.document.createElement("div");
|
|
3679
3843
|
headingRow.dataset.groupHeading = "true";
|
|
3680
3844
|
const heading = this.document.createElement("h2");
|
|
3681
|
-
heading.textContent = groupName;
|
|
3682
3845
|
const copyGroup = this.createCommandButton(
|
|
3683
3846
|
"Copy group",
|
|
3684
3847
|
"copy-group",
|
|
@@ -3694,6 +3857,7 @@ var EditorControllerImpl = class {
|
|
|
3694
3857
|
);
|
|
3695
3858
|
const savedGroupId = this.savedGroupId(group);
|
|
3696
3859
|
const enablement = this.options.groupEnablement;
|
|
3860
|
+
const commands = [];
|
|
3697
3861
|
if (enablement && savedGroupId) {
|
|
3698
3862
|
const enableButton = this.document.createElement("button");
|
|
3699
3863
|
enableButton.type = "button";
|
|
@@ -3717,40 +3881,21 @@ var EditorControllerImpl = class {
|
|
|
3717
3881
|
}
|
|
3718
3882
|
);
|
|
3719
3883
|
});
|
|
3720
|
-
|
|
3721
|
-
} else {
|
|
3722
|
-
headingRow.append(heading, copyGroup, removeGroup);
|
|
3884
|
+
commands.push(enableButton);
|
|
3723
3885
|
}
|
|
3886
|
+
commands.push(copyGroup, removeGroup);
|
|
3887
|
+
this.renderNameHeading({
|
|
3888
|
+
row: headingRow,
|
|
3889
|
+
heading,
|
|
3890
|
+
headingId: `${this.instanceId}-group-title-${groupIndex}`,
|
|
3891
|
+
kind: "group",
|
|
3892
|
+
groupId,
|
|
3893
|
+
namePath: pointer("groups", groupIndex, "name"),
|
|
3894
|
+
name: group.name,
|
|
3895
|
+
fallback: "Unnamed group",
|
|
3896
|
+
commands
|
|
3897
|
+
});
|
|
3724
3898
|
this.form.append(headingRow);
|
|
3725
|
-
const settings = this.document.createElement("fieldset");
|
|
3726
|
-
settings.dataset.groupCard = "true";
|
|
3727
|
-
const legend = this.document.createElement("legend");
|
|
3728
|
-
legend.textContent = "Group details";
|
|
3729
|
-
settings.append(legend);
|
|
3730
|
-
const fields = this.document.createElement("div");
|
|
3731
|
-
fields.dataset.editorFields = "true";
|
|
3732
|
-
const id = this.document.createElement("input");
|
|
3733
|
-
id.type = "text";
|
|
3734
|
-
id.maxLength = 64;
|
|
3735
|
-
id.value = safeText(group.id);
|
|
3736
|
-
this.renderField(
|
|
3737
|
-
fields,
|
|
3738
|
-
"Group ID",
|
|
3739
|
-
pointer("groups", groupIndex, "id"),
|
|
3740
|
-
id
|
|
3741
|
-
);
|
|
3742
|
-
const name = this.document.createElement("input");
|
|
3743
|
-
name.type = "text";
|
|
3744
|
-
name.maxLength = 100;
|
|
3745
|
-
name.value = safeText(group.name);
|
|
3746
|
-
this.renderField(
|
|
3747
|
-
fields,
|
|
3748
|
-
"Group name",
|
|
3749
|
-
pointer("groups", groupIndex, "name"),
|
|
3750
|
-
name
|
|
3751
|
-
);
|
|
3752
|
-
settings.append(fields);
|
|
3753
|
-
this.form.append(settings);
|
|
3754
3899
|
const rulesSection = this.document.createElement("section");
|
|
3755
3900
|
rulesSection.dataset.rulesSection = "true";
|
|
3756
3901
|
const rulesHeadingRow = this.document.createElement("div");
|
|
@@ -3893,7 +4038,6 @@ var EditorControllerImpl = class {
|
|
|
3893
4038
|
renderRule(group, rule, groupIndex, ruleIndex) {
|
|
3894
4039
|
const groupId = safeText(group.id);
|
|
3895
4040
|
const ruleId = safeText(rule.id);
|
|
3896
|
-
const ruleName = displayName(rule.name, "Unnamed rule");
|
|
3897
4041
|
const rulePath = pointer("groups", groupIndex, "rules", ruleIndex);
|
|
3898
4042
|
const card = this.document.createElement("article");
|
|
3899
4043
|
card.dataset.ruleCard = "true";
|
|
@@ -3903,9 +4047,8 @@ var EditorControllerImpl = class {
|
|
|
3903
4047
|
const headingRow = this.document.createElement("div");
|
|
3904
4048
|
headingRow.dataset.ruleHeading = "true";
|
|
3905
4049
|
const heading = this.document.createElement("h3");
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
card.setAttribute("aria-labelledby", heading.id);
|
|
4050
|
+
const headingId = `${this.instanceId}-rule-title-${groupIndex}-${ruleIndex}`;
|
|
4051
|
+
card.setAttribute("aria-labelledby", headingId);
|
|
3909
4052
|
const actions = this.document.createElement("div");
|
|
3910
4053
|
actions.dataset.ruleActions = "true";
|
|
3911
4054
|
actions.append(
|
|
@@ -3933,27 +4076,20 @@ var EditorControllerImpl = class {
|
|
|
3933
4076
|
"danger"
|
|
3934
4077
|
)
|
|
3935
4078
|
);
|
|
3936
|
-
|
|
4079
|
+
this.renderNameHeading({
|
|
4080
|
+
row: headingRow,
|
|
4081
|
+
heading,
|
|
4082
|
+
headingId,
|
|
4083
|
+
kind: "rule",
|
|
4084
|
+
groupId,
|
|
4085
|
+
ruleId,
|
|
4086
|
+
namePath: `${rulePath}/name`,
|
|
4087
|
+
name: rule.name,
|
|
4088
|
+
fallback: "Unnamed rule",
|
|
4089
|
+
commands: [actions]
|
|
4090
|
+
});
|
|
3937
4091
|
card.append(headingRow);
|
|
3938
|
-
|
|
3939
|
-
const legend = this.document.createElement("legend");
|
|
3940
|
-
legend.textContent = "Common rule matcher";
|
|
3941
|
-
fields.append(legend);
|
|
3942
|
-
const grid = this.document.createElement("div");
|
|
3943
|
-
grid.dataset.editorFields = "true";
|
|
3944
|
-
const id = this.document.createElement("input");
|
|
3945
|
-
id.type = "text";
|
|
3946
|
-
id.maxLength = 64;
|
|
3947
|
-
id.value = safeText(rule.id);
|
|
3948
|
-
this.renderField(grid, "Rule ID", `${rulePath}/id`, id);
|
|
3949
|
-
const name = this.document.createElement("input");
|
|
3950
|
-
name.type = "text";
|
|
3951
|
-
name.maxLength = 100;
|
|
3952
|
-
name.value = safeText(rule.name);
|
|
3953
|
-
this.renderField(grid, "Rule name", `${rulePath}/name`, name);
|
|
3954
|
-
fields.append(grid);
|
|
3955
|
-
this.renderSource(fields, rule, rulePath, groupId, ruleId);
|
|
3956
|
-
card.append(fields);
|
|
4092
|
+
this.renderSource(card, rule, rulePath, groupId, ruleId);
|
|
3957
4093
|
this.renderResourceTypes(card, rule, rulePath);
|
|
3958
4094
|
const matcherFields = this.document.createElement("fieldset");
|
|
3959
4095
|
const matcherLegend = this.document.createElement("legend");
|
|
@@ -4359,6 +4495,299 @@ var EditorControllerImpl = class {
|
|
|
4359
4495
|
this.controls.set(path, control);
|
|
4360
4496
|
parent.append(field);
|
|
4361
4497
|
}
|
|
4498
|
+
/**
|
|
4499
|
+
* Attach a control's diagnostics the same way `renderField` does, for a control
|
|
4500
|
+
* that is not wrapped in a labelled field — the inline name editor.
|
|
4501
|
+
*/
|
|
4502
|
+
decorateWithErrors(control, path) {
|
|
4503
|
+
const errors = this.errors.filter((value) => value.path === path);
|
|
4504
|
+
if (errors.length === 0) {
|
|
4505
|
+
control.removeAttribute("aria-invalid");
|
|
4506
|
+
control.removeAttribute("aria-describedby");
|
|
4507
|
+
return;
|
|
4508
|
+
}
|
|
4509
|
+
control.setAttribute("aria-invalid", "true");
|
|
4510
|
+
const error = this.document.createElement("div");
|
|
4511
|
+
error.id = `${control.id}-error`;
|
|
4512
|
+
error.dataset.editorFieldError = "true";
|
|
4513
|
+
error.textContent = errors.map((value) => value.message).join(" ");
|
|
4514
|
+
control.setAttribute("aria-describedby", error.id);
|
|
4515
|
+
control.insertAdjacentElement("afterend", error);
|
|
4516
|
+
}
|
|
4517
|
+
/**
|
|
4518
|
+
* Render a group or rule heading together with its inline rename control.
|
|
4519
|
+
*
|
|
4520
|
+
* The heading element is always present, even while the inline editor is open,
|
|
4521
|
+
* because a rule card names itself through `aria-labelledby` and an `<input>`
|
|
4522
|
+
* has no text content to name it with. While editing, the heading is moved
|
|
4523
|
+
* into the visually-hidden state rather than removed, so the card keeps its
|
|
4524
|
+
* accessible name and the shared action buttons keep their per-entity context.
|
|
4525
|
+
*/
|
|
4526
|
+
renderNameHeading(options) {
|
|
4527
|
+
const { row, heading, headingId, kind, namePath } = options;
|
|
4528
|
+
const entityName = displayName(options.name, options.fallback);
|
|
4529
|
+
const target = this.renameTargetFor(options);
|
|
4530
|
+
const editing = target !== void 0;
|
|
4531
|
+
const label = kind === "group" ? "group" : "rule";
|
|
4532
|
+
const entityLabel = `${label} ${entityName}`;
|
|
4533
|
+
heading.id = headingId;
|
|
4534
|
+
heading.textContent = entityName;
|
|
4535
|
+
if (editing) {
|
|
4536
|
+
heading.dataset.editorVisuallyHidden = "true";
|
|
4537
|
+
}
|
|
4538
|
+
row.append(heading);
|
|
4539
|
+
if (editing && target) {
|
|
4540
|
+
const wrap = this.document.createElement("div");
|
|
4541
|
+
wrap.dataset.renameControls = "true";
|
|
4542
|
+
const input = this.document.createElement("input");
|
|
4543
|
+
input.type = "text";
|
|
4544
|
+
input.maxLength = LIMITS.maxLabelLength;
|
|
4545
|
+
input.value = target.value;
|
|
4546
|
+
input.id = this.controlId(namePath);
|
|
4547
|
+
input.dataset.renameInput = "true";
|
|
4548
|
+
input.dataset.editorKey = namePath;
|
|
4549
|
+
input.disabled = this.saving;
|
|
4550
|
+
input.setAttribute(
|
|
4551
|
+
"aria-label",
|
|
4552
|
+
// Named for the action and the entity. The removed field labels must
|
|
4553
|
+
// not reappear as an accessible name.
|
|
4554
|
+
`Rename ${entityLabel}`
|
|
4555
|
+
);
|
|
4556
|
+
const save = this.createCommandButton(
|
|
4557
|
+
"Save name",
|
|
4558
|
+
"commit-rename",
|
|
4559
|
+
this.saving,
|
|
4560
|
+
{ renameKind: kind, groupId: options.groupId, ruleId: options.ruleId },
|
|
4561
|
+
"primary"
|
|
4562
|
+
);
|
|
4563
|
+
save.setAttribute("aria-label", `Save ${entityLabel} name`);
|
|
4564
|
+
save.textContent = "\u2713";
|
|
4565
|
+
save.dataset.icon = "true";
|
|
4566
|
+
const cancel = this.createCommandButton(
|
|
4567
|
+
"Cancel rename",
|
|
4568
|
+
"cancel-rename",
|
|
4569
|
+
this.saving,
|
|
4570
|
+
{ renameKind: kind, groupId: options.groupId, ruleId: options.ruleId }
|
|
4571
|
+
);
|
|
4572
|
+
cancel.setAttribute("aria-label", `Cancel renaming ${entityLabel}`);
|
|
4573
|
+
cancel.textContent = "\xD7";
|
|
4574
|
+
cancel.dataset.icon = "true";
|
|
4575
|
+
wrap.append(input, save, cancel);
|
|
4576
|
+
row.append(wrap);
|
|
4577
|
+
this.decorateWithErrors(input, namePath);
|
|
4578
|
+
this.controls.set(namePath, input);
|
|
4579
|
+
} else {
|
|
4580
|
+
const pencil = this.createCommandButton(
|
|
4581
|
+
`Rename ${label}`,
|
|
4582
|
+
"rename-entity",
|
|
4583
|
+
this.saving,
|
|
4584
|
+
{ renameKind: kind, groupId: options.groupId, ruleId: options.ruleId }
|
|
4585
|
+
);
|
|
4586
|
+
pencil.setAttribute("aria-label", `Rename ${entityLabel}`);
|
|
4587
|
+
pencil.title = `Rename ${entityLabel}`;
|
|
4588
|
+
pencil.textContent = "\u270E";
|
|
4589
|
+
pencil.dataset.icon = "true";
|
|
4590
|
+
row.append(pencil);
|
|
4591
|
+
}
|
|
4592
|
+
for (const command of options.commands) row.append(command);
|
|
4593
|
+
}
|
|
4594
|
+
/**
|
|
4595
|
+
* The rename target for an entity, when it should be open.
|
|
4596
|
+
*
|
|
4597
|
+
* It is open when the entity is already being renamed, or when a focus request
|
|
4598
|
+
* names its `/name` path. The second case is what makes add, copy, move, AI
|
|
4599
|
+
* apply, and a name diagnostic all land in the inline editor with one
|
|
4600
|
+
* mechanism.
|
|
4601
|
+
*/
|
|
4602
|
+
renameTargetFor(options) {
|
|
4603
|
+
const open = this.renameTarget;
|
|
4604
|
+
if (open && open.groupId === options.groupId && open.ruleId === options.ruleId) {
|
|
4605
|
+
open.namePath = options.namePath;
|
|
4606
|
+
return open;
|
|
4607
|
+
}
|
|
4608
|
+
if (this.focusRequest !== options.namePath) return void 0;
|
|
4609
|
+
if (open && !this.renamingInProgress) {
|
|
4610
|
+
this.renamingInProgress = true;
|
|
4611
|
+
try {
|
|
4612
|
+
this.commitRename();
|
|
4613
|
+
} finally {
|
|
4614
|
+
this.renamingInProgress = false;
|
|
4615
|
+
}
|
|
4616
|
+
if (this.renameTarget) return void 0;
|
|
4617
|
+
}
|
|
4618
|
+
const value = this.currentName(options.groupId, options.ruleId);
|
|
4619
|
+
if (value === void 0) return void 0;
|
|
4620
|
+
const target = {
|
|
4621
|
+
kind: options.kind,
|
|
4622
|
+
groupId: options.groupId,
|
|
4623
|
+
ruleId: options.ruleId,
|
|
4624
|
+
namePath: options.namePath,
|
|
4625
|
+
value
|
|
4626
|
+
};
|
|
4627
|
+
this.renameTarget = target;
|
|
4628
|
+
return target;
|
|
4629
|
+
}
|
|
4630
|
+
currentName(groupId, ruleId) {
|
|
4631
|
+
const group = this.groupById(groupId);
|
|
4632
|
+
if (!group) return void 0;
|
|
4633
|
+
if (ruleId === void 0) return safeText(group.name);
|
|
4634
|
+
const rule = this.ruleById(groupId, ruleId);
|
|
4635
|
+
return rule ? safeText(rule.name) : void 0;
|
|
4636
|
+
}
|
|
4637
|
+
/**
|
|
4638
|
+
* Every group and rule name in the project, except the entity being renamed.
|
|
4639
|
+
* Compared through the shared normalized key, so a rename is refused for a name
|
|
4640
|
+
* that differs only by case or internal spacing.
|
|
4641
|
+
*/
|
|
4642
|
+
reservedNameKeys(exclude) {
|
|
4643
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
4644
|
+
for (const group of this.draft.groups) {
|
|
4645
|
+
const isTargetGroup = group.id === exclude.groupId;
|
|
4646
|
+
if (!isTargetGroup && typeof group.name === "string") {
|
|
4647
|
+
reserved.add(normalizeNameKey(group.name));
|
|
4648
|
+
}
|
|
4649
|
+
for (const rule of group.rules) {
|
|
4650
|
+
if (isTargetGroup && rule.id === exclude.ruleId) continue;
|
|
4651
|
+
if (typeof rule.name === "string") {
|
|
4652
|
+
reserved.add(normalizeNameKey(rule.name));
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
return reserved;
|
|
4657
|
+
}
|
|
4658
|
+
/**
|
|
4659
|
+
* A human description of whichever entity already holds a normalized name, so
|
|
4660
|
+
* a refused rename can name the conflict instead of only reporting that one
|
|
4661
|
+
* exists.
|
|
4662
|
+
*/
|
|
4663
|
+
nameHolderFor(key, exclude) {
|
|
4664
|
+
for (const group of this.draft.groups) {
|
|
4665
|
+
if (typeof group.name === "string" && normalizeNameKey(group.name) === key) {
|
|
4666
|
+
if (group.id === exclude.groupId && exclude.ruleId === void 0)
|
|
4667
|
+
continue;
|
|
4668
|
+
return `group \u201C${displayName(group.name, "Unnamed group")}\u201D`;
|
|
4669
|
+
}
|
|
4670
|
+
for (const rule of group.rules) {
|
|
4671
|
+
if (typeof rule.name !== "string") continue;
|
|
4672
|
+
if (normalizeNameKey(rule.name) !== key) continue;
|
|
4673
|
+
if (group.id === exclude.groupId && rule.id === exclude.ruleId)
|
|
4674
|
+
continue;
|
|
4675
|
+
return `rule \u201C${displayName(rule.name, "Unnamed rule")}\u201D`;
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
return void 0;
|
|
4679
|
+
}
|
|
4680
|
+
openRename(kind, groupId, ruleId) {
|
|
4681
|
+
if (this.saving) return;
|
|
4682
|
+
const groupIndex = this.groupIndex(groupId);
|
|
4683
|
+
if (groupIndex === -1) return;
|
|
4684
|
+
const group = this.draft.groups[groupIndex];
|
|
4685
|
+
if (!group) return;
|
|
4686
|
+
const namePath = ruleId === void 0 ? pointer("groups", groupIndex, "name") : pointer(
|
|
4687
|
+
"groups",
|
|
4688
|
+
groupIndex,
|
|
4689
|
+
"rules",
|
|
4690
|
+
this.ruleIndex(group, ruleId),
|
|
4691
|
+
"name"
|
|
4692
|
+
);
|
|
4693
|
+
const value = this.currentName(groupId, ruleId);
|
|
4694
|
+
if (value === void 0) return;
|
|
4695
|
+
this.route = { kind: "group", groupId };
|
|
4696
|
+
this.renameTarget = { kind, groupId, ruleId, namePath, value };
|
|
4697
|
+
this.composing = false;
|
|
4698
|
+
this.focusRequest = namePath;
|
|
4699
|
+
this.render();
|
|
4700
|
+
}
|
|
4701
|
+
cancelRename() {
|
|
4702
|
+
const target = this.renameTarget;
|
|
4703
|
+
if (!target) return;
|
|
4704
|
+
this.renameTarget = void 0;
|
|
4705
|
+
this.statusMessage = "Name change discarded.";
|
|
4706
|
+
this.render();
|
|
4707
|
+
this.focusRenameControl(target.groupId, target.ruleId);
|
|
4708
|
+
}
|
|
4709
|
+
focusRenameControl(groupId, ruleId) {
|
|
4710
|
+
for (const button of this.host.querySelectorAll(
|
|
4711
|
+
'[data-command="rename-entity"]'
|
|
4712
|
+
)) {
|
|
4713
|
+
if (button.dataset.groupId !== groupId) continue;
|
|
4714
|
+
if ((button.dataset.ruleId ?? void 0) !== ruleId) continue;
|
|
4715
|
+
button.focus();
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
/** Refuse a commit, keep the editor open, and say why. */
|
|
4720
|
+
rejectRename(target, message) {
|
|
4721
|
+
this.statusMessage = message;
|
|
4722
|
+
this.focusRequest = target.namePath;
|
|
4723
|
+
this.render();
|
|
4724
|
+
}
|
|
4725
|
+
/**
|
|
4726
|
+
* Write a name onto the entity the rename targets, resolved by identity.
|
|
4727
|
+
*
|
|
4728
|
+
* `setValueAtPath` refuses to create a property that is not already there,
|
|
4729
|
+
* because its allowlist exists to stop a field input inventing arbitrary keys.
|
|
4730
|
+
* A `name` is different: a hand-authored project can legitimately arrive with
|
|
4731
|
+
* the property *absent*, and the diagnostic that reports it is exactly the one
|
|
4732
|
+
* the inline editor exists to repair. So the name is written here, onto a
|
|
4733
|
+
* record resolved by id, which also means a stale index cannot land the value
|
|
4734
|
+
* on a different entity.
|
|
4735
|
+
*/
|
|
4736
|
+
writeEntityName(groupId, ruleId, value) {
|
|
4737
|
+
const group = this.groupById(groupId);
|
|
4738
|
+
if (!group) return false;
|
|
4739
|
+
const rule = ruleId === void 0 ? void 0 : this.ruleById(groupId, ruleId);
|
|
4740
|
+
if (ruleId !== void 0 && !rule) return false;
|
|
4741
|
+
const target = rule ?? group;
|
|
4742
|
+
if (target === void 0) return false;
|
|
4743
|
+
if (target.name === value) return false;
|
|
4744
|
+
Object.defineProperty(target, "name", {
|
|
4745
|
+
configurable: true,
|
|
4746
|
+
enumerable: true,
|
|
4747
|
+
value,
|
|
4748
|
+
writable: true
|
|
4749
|
+
});
|
|
4750
|
+
return true;
|
|
4751
|
+
}
|
|
4752
|
+
commitRename() {
|
|
4753
|
+
const target = this.renameTarget;
|
|
4754
|
+
if (!target || this.saving) return;
|
|
4755
|
+
const trimmed = target.value.trim();
|
|
4756
|
+
if (trimmed.length === 0) {
|
|
4757
|
+
this.rejectRename(target, "A name cannot be empty.");
|
|
4758
|
+
return;
|
|
4759
|
+
}
|
|
4760
|
+
if (trimmed.length > LIMITS.maxLabelLength) {
|
|
4761
|
+
this.rejectRename(
|
|
4762
|
+
target,
|
|
4763
|
+
`A name cannot be longer than ${LIMITS.maxLabelLength} characters.`
|
|
4764
|
+
);
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
const key = normalizeNameKey(trimmed);
|
|
4768
|
+
if (this.reservedNameKeys(target).has(key)) {
|
|
4769
|
+
const holder = this.nameHolderFor(key, target);
|
|
4770
|
+
this.rejectRename(
|
|
4771
|
+
target,
|
|
4772
|
+
holder ? `The ${holder} already uses that name. Pick a different name.` : "That name is already used in this project."
|
|
4773
|
+
);
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
const changed = this.writeEntityName(
|
|
4777
|
+
target.groupId,
|
|
4778
|
+
target.ruleId,
|
|
4779
|
+
trimmed
|
|
4780
|
+
);
|
|
4781
|
+
this.renameTarget = void 0;
|
|
4782
|
+
if (!changed) {
|
|
4783
|
+
this.statusMessage = "Name unchanged.";
|
|
4784
|
+
this.render();
|
|
4785
|
+
return;
|
|
4786
|
+
}
|
|
4787
|
+
this.markChanged();
|
|
4788
|
+
this.statusMessage = "Name updated.";
|
|
4789
|
+
this.render();
|
|
4790
|
+
}
|
|
4362
4791
|
renderSummary() {
|
|
4363
4792
|
this.summary.replaceChildren();
|
|
4364
4793
|
this.summary.hidden = this.errors.length === 0;
|
|
@@ -4373,10 +4802,89 @@ var EditorControllerImpl = class {
|
|
|
4373
4802
|
button.dataset.errorPath = error.path;
|
|
4374
4803
|
button.textContent = `${error.message} (${error.path || "project"})`;
|
|
4375
4804
|
item.append(button);
|
|
4805
|
+
const repair = this.idRepairFor(error.path);
|
|
4806
|
+
if (repair) {
|
|
4807
|
+
const fix = this.createCommandButton(
|
|
4808
|
+
"Assign a new ID",
|
|
4809
|
+
"repair-id",
|
|
4810
|
+
this.saving,
|
|
4811
|
+
{ repairPath: error.path },
|
|
4812
|
+
"secondary"
|
|
4813
|
+
);
|
|
4814
|
+
fix.setAttribute("aria-label", repair.label);
|
|
4815
|
+
item.append(fix);
|
|
4816
|
+
}
|
|
4376
4817
|
list.append(item);
|
|
4377
4818
|
}
|
|
4378
4819
|
this.summary.append(heading, list);
|
|
4379
4820
|
}
|
|
4821
|
+
/** A label for the entity an id diagnostic names, when it names one. */
|
|
4822
|
+
idRepairFor(path) {
|
|
4823
|
+
const segments = decodePointer(path);
|
|
4824
|
+
if (segments?.[0] !== "groups" || segments[segments.length - 1] !== "id") {
|
|
4825
|
+
return void 0;
|
|
4826
|
+
}
|
|
4827
|
+
const entity = this.entityAtIdPath(segments);
|
|
4828
|
+
if (!entity) return void 0;
|
|
4829
|
+
return { label: entity.label };
|
|
4830
|
+
}
|
|
4831
|
+
/**
|
|
4832
|
+
* Resolve the entity an id path names.
|
|
4833
|
+
*
|
|
4834
|
+
* By index, not by id: the whole point of this path is that the id is wrong,
|
|
4835
|
+
* and two entities can share it, so an id lookup would be ambiguous and could
|
|
4836
|
+
* repair the wrong one.
|
|
4837
|
+
*/
|
|
4838
|
+
entityAtIdPath(segments) {
|
|
4839
|
+
if (!segments) return void 0;
|
|
4840
|
+
const groupIndex = arrayIndex(segments[1] ?? "");
|
|
4841
|
+
if (groupIndex === void 0) return void 0;
|
|
4842
|
+
const group = this.draft.groups[groupIndex];
|
|
4843
|
+
if (!group) return void 0;
|
|
4844
|
+
if (segments.length === 3) {
|
|
4845
|
+
return {
|
|
4846
|
+
group,
|
|
4847
|
+
label: `Assign a new ID to group ${displayName(group.name, "Unnamed group")}`
|
|
4848
|
+
};
|
|
4849
|
+
}
|
|
4850
|
+
if (segments.length === 5) {
|
|
4851
|
+
const ruleIndex = arrayIndex(segments[3] ?? "");
|
|
4852
|
+
if (ruleIndex === void 0) return void 0;
|
|
4853
|
+
const rule = group.rules[ruleIndex];
|
|
4854
|
+
if (!rule) return void 0;
|
|
4855
|
+
return {
|
|
4856
|
+
group,
|
|
4857
|
+
rule,
|
|
4858
|
+
label: `Assign a new ID to rule ${displayName(rule.name, "Unnamed rule")}`
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
return void 0;
|
|
4862
|
+
}
|
|
4863
|
+
repairEntityId(path) {
|
|
4864
|
+
if (this.saving) return;
|
|
4865
|
+
const entity = this.entityAtIdPath(decodePointer(path));
|
|
4866
|
+
if (!entity) return;
|
|
4867
|
+
const { group, rule } = entity;
|
|
4868
|
+
const openIndex = this.route.kind === "group" ? this.groupIndex(this.route.groupId) : -1;
|
|
4869
|
+
const repairedIndex = this.draft.groups.indexOf(group);
|
|
4870
|
+
const name = rule === void 0 ? group.name : rule.name;
|
|
4871
|
+
const next = deriveEntityId(
|
|
4872
|
+
name,
|
|
4873
|
+
rule === void 0 ? "group" : "rule",
|
|
4874
|
+
this.allIds()
|
|
4875
|
+
);
|
|
4876
|
+
if (!setValueAtPath(this.draft, path, next)) {
|
|
4877
|
+
this.statusMessage = "ID unchanged.";
|
|
4878
|
+
this.render();
|
|
4879
|
+
return;
|
|
4880
|
+
}
|
|
4881
|
+
this.markChanged();
|
|
4882
|
+
this.statusMessage = `Assigned the ID ${next}.`;
|
|
4883
|
+
if (openIndex === repairedIndex) {
|
|
4884
|
+
this.route = { kind: "group", groupId: String(group.id) };
|
|
4885
|
+
}
|
|
4886
|
+
this.validate();
|
|
4887
|
+
}
|
|
4380
4888
|
renderSearchResults() {
|
|
4381
4889
|
const root = this.searchResults;
|
|
4382
4890
|
root.replaceChildren();
|
package/dist/node/index.js
CHANGED
|
@@ -218,6 +218,15 @@ var LIMITS = Object.freeze({
|
|
|
218
218
|
maxRequestBodyOperations: 32,
|
|
219
219
|
maxLocalOrigins: 32
|
|
220
220
|
});
|
|
221
|
+
var WHITESPACE_RUN = /\s+/gu;
|
|
222
|
+
var FALLBACK_PREFIX = Object.freeze({
|
|
223
|
+
group: "Group",
|
|
224
|
+
rule: "Rule"
|
|
225
|
+
});
|
|
226
|
+
function normalizeNameKey(name) {
|
|
227
|
+
if (typeof name !== "string") return "";
|
|
228
|
+
return name.trim().replace(WHITESPACE_RUN, " ").toLowerCase();
|
|
229
|
+
}
|
|
221
230
|
function normalizeSiteOrigin(value) {
|
|
222
231
|
if (typeof value !== "string" || value.length === 0 || value.trim() !== value)
|
|
223
232
|
return null;
|
|
@@ -972,12 +981,24 @@ ajv.addFormat("rogatio-url-regex", {
|
|
|
972
981
|
});
|
|
973
982
|
function ajvIssues(errors) {
|
|
974
983
|
return (errors ?? []).map((error) => ({
|
|
975
|
-
|
|
984
|
+
// Ajv reports a `required` violation against the object that lacks the
|
|
985
|
+
// property, with the property named in `params.missingProperty`. Left alone,
|
|
986
|
+
// that parent path names a location no field or entity owns, so the missing
|
|
987
|
+
// property's own path is reconstructed here — once, for every Ajv consumer.
|
|
988
|
+
instancePath: missingPropertyPath(error),
|
|
976
989
|
keyword: error.keyword,
|
|
977
990
|
message: error.message ?? "validation failed",
|
|
978
991
|
params: error.params
|
|
979
992
|
}));
|
|
980
993
|
}
|
|
994
|
+
function missingPropertyPath(error) {
|
|
995
|
+
if (error.keyword !== "required") return error.instancePath;
|
|
996
|
+
const missing = error.params?.missingProperty;
|
|
997
|
+
if (typeof missing !== "string" || missing.length === 0) {
|
|
998
|
+
return error.instancePath;
|
|
999
|
+
}
|
|
1000
|
+
return `${error.instancePath}/${missing}`;
|
|
1001
|
+
}
|
|
981
1002
|
function snapshotOwnData(value, ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
982
1003
|
if (value === null || typeof value !== "object") {
|
|
983
1004
|
return { valid: true, value };
|
|
@@ -1067,7 +1088,23 @@ var projectValidator = guardedProjectValidator;
|
|
|
1067
1088
|
function semanticIssues(project) {
|
|
1068
1089
|
const issues = [];
|
|
1069
1090
|
const ids = /* @__PURE__ */ new Map();
|
|
1091
|
+
const names = /* @__PURE__ */ new Map();
|
|
1070
1092
|
let ruleCount = 0;
|
|
1093
|
+
const claimName = (name, namePath) => {
|
|
1094
|
+
if (typeof name !== "string" || name.length === 0) return;
|
|
1095
|
+
const key = normalizeNameKey(name);
|
|
1096
|
+
const previous = names.get(key);
|
|
1097
|
+
if (previous === void 0) {
|
|
1098
|
+
names.set(key, namePath);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
issues.push({
|
|
1102
|
+
instancePath: namePath,
|
|
1103
|
+
keyword: "uniqueName",
|
|
1104
|
+
message: `must be unique; already used at ${previous}`,
|
|
1105
|
+
params: { previousPath: previous }
|
|
1106
|
+
});
|
|
1107
|
+
};
|
|
1071
1108
|
for (let groupIndex = 0; groupIndex < project.groups.length; groupIndex += 1) {
|
|
1072
1109
|
const group = project.groups[groupIndex];
|
|
1073
1110
|
const groupPath = `/groups/${groupIndex}`;
|
|
@@ -1082,6 +1119,7 @@ function semanticIssues(project) {
|
|
|
1082
1119
|
} else {
|
|
1083
1120
|
ids.set(group.id, `${groupPath}/id`);
|
|
1084
1121
|
}
|
|
1122
|
+
claimName(group.name, `${groupPath}/name`);
|
|
1085
1123
|
for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex += 1) {
|
|
1086
1124
|
const rule = group.rules[ruleIndex];
|
|
1087
1125
|
ruleCount += 1;
|
|
@@ -1097,6 +1135,7 @@ function semanticIssues(project) {
|
|
|
1097
1135
|
} else {
|
|
1098
1136
|
ids.set(rule.id, `${rulePath}/id`);
|
|
1099
1137
|
}
|
|
1138
|
+
claimName(rule.name, `${rulePath}/name`);
|
|
1100
1139
|
if (rule.type !== void 0 && rule.type !== "redirect" && rule.type !== "query" && rule.type !== "header" && rule.type !== "response-body" && rule.type !== "request-body") {
|
|
1101
1140
|
issues.push({
|
|
1102
1141
|
instancePath: `${rulePath}/type`,
|
|
@@ -1620,6 +1659,7 @@ var ISSUE_CODES = {
|
|
|
1620
1659
|
uniqueItems: "schema.out-of-range",
|
|
1621
1660
|
ownProperties: "schema.invalid-structure",
|
|
1622
1661
|
uniqueId: "schema.duplicate-id",
|
|
1662
|
+
uniqueName: "schema.duplicate-name",
|
|
1623
1663
|
maxRulesPerProject: "schema.rule-limit"
|
|
1624
1664
|
};
|
|
1625
1665
|
var SAFE_PARAM_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -1647,6 +1687,7 @@ var MESSAGES = {
|
|
|
1647
1687
|
"schema.out-of-range": "The project contains a value outside its allowed bounds.",
|
|
1648
1688
|
"schema.invalid-structure": "The project contains invalid structure.",
|
|
1649
1689
|
"schema.duplicate-id": "Project and rule IDs must be unique.",
|
|
1690
|
+
"schema.duplicate-name": "Group and rule names must be unique within a project.",
|
|
1650
1691
|
"schema.rule-limit": "The project contains too many rules.",
|
|
1651
1692
|
"compiler.invariant": "The compiler could not normalize validated project data.",
|
|
1652
1693
|
"compiler.forbidden-header": "The header name is forbidden for this direction.",
|
|
@@ -20972,12 +21013,22 @@ Replace-mode body may use URL captures. Regex-mode replacement strings use captu
|
|
|
20972
21013
|
Replace-mode body may use URL captures. Regex-mode replacement strings use captures from the body pattern, not the source pattern.
|
|
20973
21014
|
|
|
20974
21015
|
## Compiler Diagnostic Codes (stable)
|
|
20975
|
-
- schema.
|
|
20976
|
-
- schema.
|
|
20977
|
-
- schema.invalid-
|
|
20978
|
-
- schema.invalid-
|
|
20979
|
-
- schema.
|
|
20980
|
-
-
|
|
21016
|
+
- schema.required - Required project data is missing
|
|
21017
|
+
- schema.unknown-property - The project contains an unknown property
|
|
21018
|
+
- schema.invalid-type - A value has an invalid type
|
|
21019
|
+
- schema.invalid-format - A value has an invalid format
|
|
21020
|
+
- schema.invalid-value - A value is invalid
|
|
21021
|
+
- schema.out-of-range - A value is outside its allowed bounds
|
|
21022
|
+
- schema.invalid-structure - A value has invalid structure
|
|
21023
|
+
- schema.duplicate-id - Duplicate group or rule ID
|
|
21024
|
+
- schema.duplicate-name - Duplicate group or rule name; every group and rule name must be unique in the project
|
|
21025
|
+
- schema.rule-limit - The project contains too many rules
|
|
21026
|
+
- compiler.invariant - The compiler could not normalize the project
|
|
21027
|
+
- compiler.forbidden-header - The header is forbidden for this direction
|
|
21028
|
+
- compiler.header-value-required - A header value is required for set and append
|
|
21029
|
+
- compiler.header-value-unexpected - A header value is not allowed for remove
|
|
21030
|
+
- compiler.invalid-header-direction - Invalid header direction
|
|
21031
|
+
- compiler.invalid-header-operation - Invalid header operation
|
|
20981
21032
|
|
|
20982
21033
|
## Current Project
|
|
20983
21034
|
${projectJson}
|
|
@@ -27710,6 +27761,20 @@ async function testCommandImpl(args, stdinInput, captureOutput, options = {}) {
|
|
|
27710
27761
|
if (captureOutput) return output;
|
|
27711
27762
|
console.log(output.trim());
|
|
27712
27763
|
} else {
|
|
27764
|
+
const groupNames = /* @__PURE__ */ new Map();
|
|
27765
|
+
if (schemaResult.valid) {
|
|
27766
|
+
for (const group of schemaResult.data.groups) {
|
|
27767
|
+
groupNames.set(group.id, group.name);
|
|
27768
|
+
}
|
|
27769
|
+
}
|
|
27770
|
+
const labels = /* @__PURE__ */ new Map();
|
|
27771
|
+
for (const operation of compileResult.operations) {
|
|
27772
|
+
const groupName = groupNames.get(operation.groupId);
|
|
27773
|
+
labels.set(
|
|
27774
|
+
`${operation.groupId}/${operation.ruleId}`,
|
|
27775
|
+
groupName === void 0 ? operation.name : `${groupName} / ${operation.name}`
|
|
27776
|
+
);
|
|
27777
|
+
}
|
|
27713
27778
|
let output = "";
|
|
27714
27779
|
for (const urlResult of dryRunResult.results) {
|
|
27715
27780
|
output += `
|
|
@@ -27718,7 +27783,9 @@ URL: ${urlResult.url}
|
|
|
27718
27783
|
output += ` Matched rules: ${urlResult.matchedRuleCount}
|
|
27719
27784
|
`;
|
|
27720
27785
|
for (const rule of urlResult.rules) {
|
|
27721
|
-
|
|
27786
|
+
const key = `${rule.groupId}/${rule.ruleId}`;
|
|
27787
|
+
const label2 = labels.get(key) ?? key;
|
|
27788
|
+
output += ` ${label2}: ${rule.matched ? "MATCHED" : "NOT MATCHED"}
|
|
27722
27789
|
`;
|
|
27723
27790
|
output += ` source: ${rule.source.state} - ${rule.source.detail}
|
|
27724
27791
|
`;
|
package/package.json
CHANGED