@betterportal/config-manager 10.6.31 → 10.6.33
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/lib/.bsb/clients/service-betterportal-config-manager.d.ts +1 -1
- package/lib/.bsb/clients/service-betterportal-config-manager.js +1 -1
- package/lib/bp-contracts/org.betterportal.config-manager.json +69 -1
- package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.d.ts +32 -0
- package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts +3 -2
- package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/adminApi.js +58 -36
- package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/appQueries.d.ts +1096 -0
- package/lib/plugins/service-betterportal-config-manager/appQueries.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/appQueries.js +330 -0
- package/lib/plugins/service-betterportal-config-manager/appQueries.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview-environments/_renderer.bootstrap5/GET.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview-environments/_renderer.bootstrap5/GET.js +35 -5
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview-environments/_renderer.bootstrap5/GET.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/index.d.ts +4 -0
- package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/index.js +70 -22
- package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/m2mConnections.d.ts +6 -3
- package/lib/plugins/service-betterportal-config-manager/m2mConnections.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/m2mConnections.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts +2 -1
- package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/menuEditor.js +165 -176
- package/lib/plugins/service-betterportal-config-manager/menuEditor.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/previewEnvironmentManagement.d.ts +10 -0
- package/lib/plugins/service-betterportal-config-manager/previewEnvironmentManagement.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/previewEnvironmentManagement.js +90 -1
- package/lib/plugins/service-betterportal-config-manager/previewEnvironmentManagement.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/previewEnvironments.d.ts +1 -1
- package/lib/plugins/service-betterportal-config-manager/previewEnvironments.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/previewEnvironments.js +133 -23
- package/lib/plugins/service-betterportal-config-manager/previewEnvironments.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/routeContext.d.ts +2 -0
- package/lib/plugins/service-betterportal-config-manager/routeContext.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/routeContext.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/appData.d.ts +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/appData.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/appData.js +4 -4
- package/lib/plugins/service-betterportal-config-manager/storage/appData.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts +5 -3
- package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/core.js +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/entities.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/entities.js +4 -2
- package/lib/plugins/service-betterportal-config-manager/storage/entities.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts +14 -0
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.js +11 -0
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +4 -3
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/tenantManagement.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/tenantManagement.js +3 -1
- package/lib/plugins/service-betterportal-config-manager/tenantManagement.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/webhooks.js +147 -54
- package/lib/plugins/service-betterportal-config-manager/webhooks.js.map +1 -1
- package/lib/schemas/service-betterportal-config-manager.json +1 -1
- package/lib/schemas/service-betterportal-config-manager.plugin.json +1 -1
- package/package.json +3 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { htmlResponse, jsonResponse, uuidv7 } from "@betterportal/framework";
|
|
2
|
-
import { getManifestCache } from "./syncApi.js";
|
|
1
|
+
import { eventObservability, menuItemVisible, htmlResponse, jsonResponse, uuidv7 } from "@betterportal/framework";
|
|
2
|
+
import { getManifestCache, injectResolvedServicePaths } from "./syncApi.js";
|
|
3
3
|
import { isApiRoute, isMenuRouteExcluded } from "./routeMounts.js";
|
|
4
|
+
import { editAppRows, readApp } from "./appQueries.js";
|
|
4
5
|
const API_BASE = "/.well-known/bp/admin";
|
|
5
6
|
// Parse-only base for relative request URLs. Never emit this origin.
|
|
6
7
|
const RELATIVE_URL_PARSE_BASE = "http://betterportal.invalid";
|
|
@@ -101,7 +102,10 @@ function lookupServiceViews(serviceId) {
|
|
|
101
102
|
}));
|
|
102
103
|
}
|
|
103
104
|
function rowAttrs(item, depth, editing = false) {
|
|
104
|
-
return `id="bp-menu-row-${item.id}" draggable="${editing ? "false" : "true"}" data-bp-drag-item="${item.id}" data-bp-drag-type="${item.type}" data-bp-drag-depth="${depth}"${editing ? " data-bp-menu-editing" : ""} style="padding-left: ${depth * 1.5 + 1}rem;"`;
|
|
105
|
+
return `hx-indicator:inherited="#bp-menu-loading-${item.id}" hx-sync:inherited="closest li:queue" id="bp-menu-row-${item.id}" draggable="${editing ? "false" : "true"}" data-bp-drag-item="${item.id}" data-bp-drag-type="${item.type}" data-bp-drag-depth="${depth}"${editing ? " data-bp-menu-editing" : ""} style="padding-left: ${depth * 1.5 + 1}rem;"`;
|
|
106
|
+
}
|
|
107
|
+
function rowLoader(item) {
|
|
108
|
+
return `<div id="bp-menu-loading-${item.id}" class="htmx-indicator bp-menu-loading" role="status"><span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Updating…</div>`;
|
|
105
109
|
}
|
|
106
110
|
function editableTitleText(item, route) {
|
|
107
111
|
if (item.type === "section")
|
|
@@ -124,12 +128,12 @@ function titleDisplayHtml(item, route, appId) {
|
|
|
124
128
|
style="border:1px solid var(--bs-border-color); border-radius:0.375rem; padding:0.25rem 0.6rem; background:transparent; cursor:text; text-align:left; min-width:200px; font-weight:500;"
|
|
125
129
|
hx-get="${API_BASE}/menu-editor/item?appId=${encodeURIComponent(appId)}&itemId=${encodeURIComponent(item.id)}&mode=edit-title"
|
|
126
130
|
hx-target="#bp-menu-title-${item.id}"
|
|
127
|
-
hx-swap="outerHTML"
|
|
131
|
+
hx-swap="outerHTML settle:0"
|
|
128
132
|
title="Click to rename">${escapeHtml(titleText)}</button>`;
|
|
129
133
|
}
|
|
130
134
|
function titleEditorHtml(item, route, appId) {
|
|
131
135
|
return `<form id="bp-menu-title-${item.id}" data-bp-menu-editing
|
|
132
|
-
hx-post="${API_BASE}/menu-editor/save-title" hx-target="#bp-menu-
|
|
136
|
+
hx-post="${API_BASE}/menu-editor/save-title" hx-target="#bp-menu-row-${item.id}" hx-swap="outerHTML settle:0"
|
|
133
137
|
class="d-flex align-items-center gap-1 flex-grow-1 m-0">
|
|
134
138
|
<input type="hidden" name="appId" value="${escapeHtml(appId)}" />
|
|
135
139
|
<input type="hidden" name="itemId" value="${escapeHtml(item.id)}" />
|
|
@@ -137,7 +141,7 @@ function titleEditorHtml(item, route, appId) {
|
|
|
137
141
|
<button type="submit" class="btn btn-sm btn-success" title="Save">OK</button>
|
|
138
142
|
<button type="button" class="btn btn-sm btn-outline-secondary" title="Cancel"
|
|
139
143
|
hx-get="${API_BASE}/menu-editor/item?appId=${encodeURIComponent(appId)}&itemId=${encodeURIComponent(item.id)}&mode=display-title"
|
|
140
|
-
hx-target="#bp-menu-title-${item.id}" hx-swap="outerHTML">X</button>
|
|
144
|
+
hx-target="#bp-menu-title-${item.id}" hx-swap="outerHTML settle:0">X</button>
|
|
141
145
|
</form>`;
|
|
142
146
|
}
|
|
143
147
|
function subLineHtml(item, route, config, appId) {
|
|
@@ -154,7 +158,7 @@ function subLineHtml(item, route, config, appId) {
|
|
|
154
158
|
<button type="button" class="btn btn-sm btn-link p-0"
|
|
155
159
|
hx-get="${API_BASE}/menu-editor/item?appId=${encodeURIComponent(appId)}&itemId=${encodeURIComponent(item.id)}&mode=edit-external"
|
|
156
160
|
hx-target="#bp-menu-row-${item.id}"
|
|
157
|
-
hx-swap="outerHTML"
|
|
161
|
+
hx-swap="outerHTML settle:0"
|
|
158
162
|
title="Edit URL">Edit</button>
|
|
159
163
|
</div>`;
|
|
160
164
|
}
|
|
@@ -173,12 +177,12 @@ function subLineHtml(item, route, config, appId) {
|
|
|
173
177
|
<button type="button" class="btn btn-sm btn-link p-0 ms-1"
|
|
174
178
|
hx-get="${API_BASE}/menu-editor/item?appId=${encodeURIComponent(appId)}&itemId=${encodeURIComponent(item.id)}&mode=edit-link"
|
|
175
179
|
hx-target="#bp-menu-row-${item.id}"
|
|
176
|
-
hx-swap="outerHTML"
|
|
180
|
+
hx-swap="outerHTML settle:0"
|
|
177
181
|
title="Edit URL & paths">Edit</button>
|
|
178
182
|
</div>`;
|
|
179
183
|
}
|
|
180
184
|
function actionButtons(item, appId) {
|
|
181
|
-
const btn = (action, label, btnClass, title) => `<form hx-post="${API_BASE}/menu-editor/${action}" hx-target="#bp-menu-editor" hx-swap="outerHTML" class="d-inline m-0">
|
|
185
|
+
const btn = (action, label, btnClass, title) => `<form hx-post="${API_BASE}/menu-editor/${action}" hx-target="#bp-menu-editor" hx-swap="outerHTML settle:0" class="d-inline m-0">
|
|
182
186
|
<input type="hidden" name="appId" value="${escapeHtml(appId)}" />
|
|
183
187
|
<input type="hidden" name="itemId" value="${escapeHtml(item.id)}" />
|
|
184
188
|
<button type="submit" class="btn btn-sm ${btnClass}" title="${escapeHtml(title)}">${label}</button>
|
|
@@ -192,13 +196,52 @@ function actionButtons(item, appId) {
|
|
|
192
196
|
${btn("remove", "x", "btn-outline-danger", "Remove")}
|
|
193
197
|
</div>`;
|
|
194
198
|
}
|
|
195
|
-
function
|
|
199
|
+
function visibilityControls(item) {
|
|
200
|
+
const options = [["auto", "Automatic"], ["show", "Everyone"], ["hide-unauthenticated", "Signed in"],
|
|
201
|
+
["show-unauthenticated", "Only signed out"], ["hide-unauthorized", "Has permission"]];
|
|
202
|
+
return `<fieldset class="mb-0"><legend class="small mb-1">Audience</legend>
|
|
203
|
+
<div class="d-flex flex-wrap gap-1">${options.map(([value, label]) => {
|
|
204
|
+
const id = `bp-audience-${item.id}-${value}`;
|
|
205
|
+
return `<input type="radio" class="btn-check" name="authStatus" id="${id}" value="${value}"${(item.authStatus ?? "auto") === value ? " checked" : ""} />
|
|
206
|
+
<label class="btn btn-sm btn-outline-primary" style="--bs-btn-color:var(--bs-body-color)" for="${id}">${label}</label>`;
|
|
207
|
+
}).join("")}</div>${item.type === "group" ? `<div class="small text-secondary mt-1">Saving this audience also updates all children, including nested groups.</div>` : ""}</fieldset>
|
|
208
|
+
${item.type === "link" ? `<fieldset class="mb-0"><legend class="small mb-1">When service is unavailable</legend>
|
|
209
|
+
${[["show", "Show warning"], ["hide", "Hide"]].map(([value, label]) => `<label class="form-check form-check-inline small mb-0">
|
|
210
|
+
<input type="radio" class="form-check-input" name="serviceStatus" value="${value}"${(item.serviceStatus ?? "show") === value ? " checked" : ""} />${label}</label>`).join("")}</fieldset>` : ""}
|
|
211
|
+
${item.type !== "link" && item.rolesAnyOf?.length ? `<div class="small text-warning">Legacy role restriction: ${escapeHtml(item.rolesAnyOf.join(", "))}.
|
|
212
|
+
<label><input type="checkbox" name="clearRoleRestriction" value="true" /> Remove this restriction</label></div>` : ""}`;
|
|
213
|
+
}
|
|
214
|
+
function accessSummary(config, appDef, item, accessApp) {
|
|
215
|
+
if (item.type !== "link")
|
|
216
|
+
return item.type === "group" ? "Hidden when it has no visible children." : "Audience controls visibility; no linked route permissions.";
|
|
217
|
+
const app = accessApp ?? injectResolvedServicePaths({ tenants: config.tenants, apps: [appDef], managementOrigins: [] }).apps[0];
|
|
218
|
+
const route = app.routes.find(route => route.id === item.routeId);
|
|
219
|
+
if (!route || route.authRequired === undefined || route.menuPermissions === undefined)
|
|
220
|
+
return "Route access unknown until its service syncs. Automatic visibility stays hidden.";
|
|
221
|
+
if (!route.authRequired)
|
|
222
|
+
return "Public route. Access is derived automatically.";
|
|
223
|
+
if (!route.menuPermissions.length)
|
|
224
|
+
return "Requires sign-in. No additional route permissions.";
|
|
225
|
+
const aliases = Object.fromEntries(config.tenants.flatMap((tenant) => tenant.services.map((service) => [service.id, service.serviceId ?? service.id])));
|
|
226
|
+
for (const service of config.platformServices ?? [])
|
|
227
|
+
aliases[service.id] = service.serviceId ?? service.id;
|
|
228
|
+
for (const activation of config.sharedServiceActivations ?? []) {
|
|
229
|
+
const service = config.sharedServiceCatalog?.find((candidate) => candidate.id === activation.sharedServiceId);
|
|
230
|
+
if (service)
|
|
231
|
+
aliases[activation.id] = service.serviceId ?? service.id;
|
|
232
|
+
}
|
|
233
|
+
const roles = (app.auth?.roles ?? []).filter(role => role.id !== "*" && menuItemVisible({ authStatus: "hide-unauthorized" }, route, { status: "authenticated", permissions: role.permissions }, aliases));
|
|
234
|
+
const requirements = route.menuPermissions.map(requirement => `${requirement.viewId}: ${requirement.permissions.join(", ")}`).join("; ");
|
|
235
|
+
return `Route permissions: ${escapeHtml(requirements)}. ${roles.length ? `Roles with access: ${escapeHtml(roles.map(role => role.title).join(", "))}.` : "Access may require a combination of assigned roles."}`;
|
|
236
|
+
}
|
|
237
|
+
function renderRow(item, depth, mode, config, appDef, appId, accessApp) {
|
|
196
238
|
const routes = getRoutes(appDef);
|
|
197
239
|
const route = item.routeId ? routes.find((r) => r.id === item.routeId) ?? null : null;
|
|
198
240
|
const typeBadgeClass = item.type === "group" ? "text-bg-warning" : item.type === "external" ? "text-bg-info" : item.type === "link" ? "text-bg-primary" : "text-bg-secondary";
|
|
199
241
|
if (mode === "edit-external" && item.type === "external") {
|
|
200
|
-
return `<li ${rowAttrs(item, depth, true)} class="list-group-item">
|
|
201
|
-
|
|
242
|
+
return `<li ${rowAttrs(item, depth, true)} class="list-group-item position-relative">
|
|
243
|
+
${rowLoader(item)}
|
|
244
|
+
<form hx-post="${API_BASE}/menu-editor/save-external" hx-target="#bp-menu-row-${item.id}" hx-swap="outerHTML settle:0"
|
|
202
245
|
class="d-flex flex-column gap-2">
|
|
203
246
|
<div class="d-flex align-items-center gap-2">
|
|
204
247
|
<span class="badge ${typeBadgeClass}">${escapeHtml(item.type)}</span>
|
|
@@ -208,38 +251,19 @@ function renderRow(item, depth, mode, config, appDef, appId) {
|
|
|
208
251
|
<input type="hidden" name="itemId" value="${escapeHtml(item.id)}" />
|
|
209
252
|
<label class="form-label small mb-0">URL</label>
|
|
210
253
|
<input type="url" name="href" class="form-control form-control-sm" value="${escapeHtml(item.href ?? "")}" placeholder="https://..." required />
|
|
211
|
-
|
|
212
|
-
<div class="col-md-6">
|
|
213
|
-
<label class="form-label small mb-0">Unavailable service</label>
|
|
214
|
-
<select name="serviceStatus" class="form-select form-select-sm">
|
|
215
|
-
<option value="show"${item.serviceStatus !== "hide" ? " selected" : ""}>Show with warning</option>
|
|
216
|
-
<option value="hide"${item.serviceStatus === "hide" ? " selected" : ""}>Hide</option>
|
|
217
|
-
</select>
|
|
218
|
-
</div>
|
|
219
|
-
<div class="col-md-6">
|
|
220
|
-
<label class="form-label small mb-0">Required roles (any, comma separated)</label>
|
|
221
|
-
<input name="rolesAnyOf" class="form-control form-control-sm" value="${escapeHtml((item.rolesAnyOf ?? []).join(", "))}" />
|
|
222
|
-
<label class="form-label small mb-0">Authorization</label>
|
|
223
|
-
<select name="authStatus" class="form-select form-select-sm">
|
|
224
|
-
<option value="auto"${!item.authStatus || item.authStatus === "auto" ? " selected" : ""}>Automatic (route permissions)</option>
|
|
225
|
-
<option value="show"${item.authStatus === "show" ? " selected" : ""}>Always show</option>
|
|
226
|
-
<option value="hide-unauthenticated"${item.authStatus === "hide-unauthenticated" ? " selected" : ""}>Hide when signed out</option>
|
|
227
|
-
<option value="hide-unauthorized"${item.authStatus === "hide-unauthorized" ? " selected" : ""}>Hide when unauthorized</option>
|
|
228
|
-
<option value="show-unauthenticated"${item.authStatus === "show-unauthenticated" ? " selected" : ""}>Show only when signed out</option>
|
|
229
|
-
</select>
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
254
|
+
${visibilityControls(item)}
|
|
232
255
|
<div class="d-flex gap-2 justify-content-end">
|
|
233
256
|
<button type="submit" class="btn btn-sm btn-success">OK Save</button>
|
|
234
257
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
|
235
258
|
hx-get="${API_BASE}/menu-editor/item?appId=${encodeURIComponent(appId)}&itemId=${encodeURIComponent(item.id)}&mode=display"
|
|
236
|
-
hx-target="#bp-menu-row-${item.id}" hx-swap="outerHTML">X Cancel</button>
|
|
259
|
+
hx-target="#bp-menu-row-${item.id}" hx-swap="outerHTML settle:0">X Cancel</button>
|
|
237
260
|
</div>
|
|
238
261
|
</form>
|
|
239
262
|
</li>`;
|
|
240
263
|
}
|
|
241
264
|
// display mode
|
|
242
|
-
return `<li ${rowAttrs(item, depth)} class="list-group-item">
|
|
265
|
+
return `<li ${rowAttrs(item, depth)} class="list-group-item position-relative">
|
|
266
|
+
${rowLoader(item)}
|
|
243
267
|
<div class="d-flex align-items-start gap-3">
|
|
244
268
|
<span class="text-secondary" style="cursor:grab; padding-top:0.3rem; user-select:none; font-size:0.75rem; line-height:1;" title="Drag to reorder">drag</span>
|
|
245
269
|
<div class="d-flex flex-column gap-1 flex-grow-1 min-width-0">
|
|
@@ -248,21 +272,13 @@ function renderRow(item, depth, mode, config, appDef, appId) {
|
|
|
248
272
|
${titleDisplayHtml(item, route, appId)}
|
|
249
273
|
</div>
|
|
250
274
|
${subLineHtml(item, route, config, appId)}
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
</select>
|
|
259
|
-
</label>
|
|
260
|
-
<label class="small">Required roles (any, comma separated)
|
|
261
|
-
<input name="rolesAnyOf" class="form-control form-control-sm" value="${escapeHtml((item.rolesAnyOf ?? []).join(", "))}" />
|
|
262
|
-
</label>
|
|
263
|
-
<button type="submit" class="btn btn-sm btn-primary">Save visibility</button>
|
|
264
|
-
</form>
|
|
265
|
-
</details>
|
|
275
|
+
<div class="small text-body-secondary">${accessSummary(config, appDef, item, accessApp)}</div>
|
|
276
|
+
<form hx-post="${API_BASE}/menu-editor/save-visibility" hx-target="${item.type === "group" ? "#bp-menu-editor" : `#bp-menu-row-${item.id}`}" hx-swap="outerHTML settle:0" class="d-flex flex-column gap-2 mt-2">
|
|
277
|
+
<input type="hidden" name="appId" value="${escapeHtml(appId)}" />
|
|
278
|
+
<input type="hidden" name="itemId" value="${escapeHtml(item.id)}" />
|
|
279
|
+
${visibilityControls(item)}
|
|
280
|
+
<div><button type="submit" class="btn btn-sm btn-primary">Save visibility</button></div>
|
|
281
|
+
</form>
|
|
266
282
|
</div>
|
|
267
283
|
${actionButtons(item, appId)}
|
|
268
284
|
</div>
|
|
@@ -295,8 +311,9 @@ async function renderEditLink(item, route, depth, config, appDef, appId) {
|
|
|
295
311
|
...services.map((s) => `<option value="${escapeHtml(s.id)}"${s.id === route?.serviceId ? " selected" : ""}>${escapeHtml(s.title)}</option>`)
|
|
296
312
|
].join("");
|
|
297
313
|
const viewOpts = renderViewOptions(views, route?.viewId ?? "");
|
|
298
|
-
return `<li ${rowAttrs(item, depth, true)} class="list-group-item">
|
|
299
|
-
|
|
314
|
+
return `<li ${rowAttrs(item, depth, true)} class="list-group-item position-relative">
|
|
315
|
+
${rowLoader(item)}
|
|
316
|
+
<form hx-post="${API_BASE}/menu-editor/save-link" hx-target="#bp-menu-row-${item.id}" hx-swap="outerHTML settle:0"
|
|
300
317
|
class="d-flex flex-column gap-2">
|
|
301
318
|
<div class="d-flex align-items-center gap-2">
|
|
302
319
|
<span class="badge ${typeBadgeClass}">link</span>
|
|
@@ -320,7 +337,7 @@ async function renderEditLink(item, route, depth, config, appDef, appId) {
|
|
|
320
337
|
<select id="bp-views-${item.id}" name="viewId" class="form-select form-select-sm" required
|
|
321
338
|
hx-get="${API_BASE}/menu-editor/default-target"
|
|
322
339
|
hx-target="#bp-paths-${item.id}"
|
|
323
|
-
hx-swap="outerHTML"
|
|
340
|
+
hx-swap="outerHTML settle:0"
|
|
324
341
|
hx-trigger="change"
|
|
325
342
|
hx-include="closest form">${viewOpts}</select>
|
|
326
343
|
</div>
|
|
@@ -338,42 +355,22 @@ async function renderEditLink(item, route, depth, config, appDef, appId) {
|
|
|
338
355
|
</div>
|
|
339
356
|
</div>
|
|
340
357
|
${renderPathFields(item.id, route?.path ?? "", route?.targetPath ?? "")}
|
|
341
|
-
|
|
342
|
-
<div class="col-md-6">
|
|
343
|
-
<label class="form-label small mb-0">Unavailable service</label>
|
|
344
|
-
<select name="serviceStatus" class="form-select form-select-sm">
|
|
345
|
-
<option value="show"${item.serviceStatus !== "hide" ? " selected" : ""}>Show with warning</option>
|
|
346
|
-
<option value="hide"${item.serviceStatus === "hide" ? " selected" : ""}>Hide</option>
|
|
347
|
-
</select>
|
|
348
|
-
</div>
|
|
349
|
-
<div class="col-md-6">
|
|
350
|
-
<label class="form-label small mb-0">Required roles (any, comma separated)</label>
|
|
351
|
-
<input name="rolesAnyOf" class="form-control form-control-sm" value="${escapeHtml((item.rolesAnyOf ?? []).join(", "))}" />
|
|
352
|
-
<label class="form-label small mb-0">Authorization</label>
|
|
353
|
-
<select name="authStatus" class="form-select form-select-sm">
|
|
354
|
-
<option value="auto"${!item.authStatus || item.authStatus === "auto" ? " selected" : ""}>Automatic (route permissions)</option>
|
|
355
|
-
<option value="show"${item.authStatus === "show" ? " selected" : ""}>Always show</option>
|
|
356
|
-
<option value="hide-unauthenticated"${item.authStatus === "hide-unauthenticated" ? " selected" : ""}>Hide when signed out</option>
|
|
357
|
-
<option value="hide-unauthorized"${item.authStatus === "hide-unauthorized" ? " selected" : ""}>Hide when unauthorized</option>
|
|
358
|
-
<option value="show-unauthenticated"${item.authStatus === "show-unauthenticated" ? " selected" : ""}>Show only when signed out</option>
|
|
359
|
-
</select>
|
|
360
|
-
</div>
|
|
361
|
-
</div>
|
|
358
|
+
${visibilityControls(item)}
|
|
362
359
|
<div class="d-flex gap-2 justify-content-end">
|
|
363
360
|
<button type="submit" class="btn btn-sm btn-success">OK Save</button>
|
|
364
361
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
|
365
362
|
hx-get="${API_BASE}/menu-editor/item?appId=${encodeURIComponent(appId)}&itemId=${encodeURIComponent(item.id)}&mode=display"
|
|
366
|
-
hx-target="#bp-menu-row-${item.id}" hx-swap="outerHTML">X Cancel</button>
|
|
363
|
+
hx-target="#bp-menu-row-${item.id}" hx-swap="outerHTML settle:0">X Cancel</button>
|
|
367
364
|
</div>
|
|
368
365
|
<div class="small text-secondary">Path/target/service/view edits update the underlying route - affects any other menu items referencing it.</div>
|
|
369
366
|
</form>
|
|
370
367
|
</li>`;
|
|
371
368
|
}
|
|
372
|
-
function renderTree(items, depth, config, appDef, appId) {
|
|
369
|
+
function renderTree(items, depth, config, appDef, appId, accessApp = injectResolvedServicePaths({ tenants: config.tenants, apps: [appDef], managementOrigins: [] }).apps[0]) {
|
|
373
370
|
return items.map((item) => {
|
|
374
|
-
const row = renderRow(item, depth, "display", config, appDef, appId);
|
|
371
|
+
const row = renderRow(item, depth, "display", config, appDef, appId, accessApp);
|
|
375
372
|
const children = item.type === "group" && item.children && item.children.length > 0
|
|
376
|
-
? renderTree(item.children, depth + 1, config, appDef, appId)
|
|
373
|
+
? renderTree(item.children, depth + 1, config, appDef, appId, accessApp)
|
|
377
374
|
: "";
|
|
378
375
|
return row + children;
|
|
379
376
|
}).join("");
|
|
@@ -401,7 +398,7 @@ function renderAddForms(appId, routes, groups) {
|
|
|
401
398
|
<div class="card">
|
|
402
399
|
<div class="card-header"><strong>Add View Link</strong></div>
|
|
403
400
|
<div class="card-body">
|
|
404
|
-
<form hx-post="${API_BASE}/menu-editor/add" hx-target="#bp-menu-editor" hx-swap="outerHTML">
|
|
401
|
+
<form hx-post="${API_BASE}/menu-editor/add" hx-target="#bp-menu-editor" hx-swap="outerHTML settle:0">
|
|
405
402
|
<input type="hidden" name="appId" value="${escapeHtml(appId)}" />
|
|
406
403
|
<input type="hidden" name="type" value="link" />
|
|
407
404
|
<div class="mb-2">
|
|
@@ -428,7 +425,7 @@ function renderAddForms(appId, routes, groups) {
|
|
|
428
425
|
<div class="card">
|
|
429
426
|
<div class="card-header"><strong>Add External Link</strong></div>
|
|
430
427
|
<div class="card-body">
|
|
431
|
-
<form hx-post="${API_BASE}/menu-editor/add" hx-target="#bp-menu-editor" hx-swap="outerHTML">
|
|
428
|
+
<form hx-post="${API_BASE}/menu-editor/add" hx-target="#bp-menu-editor" hx-swap="outerHTML settle:0">
|
|
432
429
|
<input type="hidden" name="appId" value="${escapeHtml(appId)}" />
|
|
433
430
|
<input type="hidden" name="type" value="external" />
|
|
434
431
|
<div class="mb-2">
|
|
@@ -452,7 +449,7 @@ function renderAddForms(appId, routes, groups) {
|
|
|
452
449
|
<div class="card">
|
|
453
450
|
<div class="card-header"><strong>Add Group</strong></div>
|
|
454
451
|
<div class="card-body">
|
|
455
|
-
<form hx-post="${API_BASE}/menu-editor/add" hx-target="#bp-menu-editor" hx-swap="outerHTML">
|
|
452
|
+
<form hx-post="${API_BASE}/menu-editor/add" hx-target="#bp-menu-editor" hx-swap="outerHTML settle:0">
|
|
456
453
|
<input type="hidden" name="appId" value="${escapeHtml(appId)}" />
|
|
457
454
|
<input type="hidden" name="type" value="group" />
|
|
458
455
|
<div class="mb-2">
|
|
@@ -478,10 +475,14 @@ function renderEditor(config, appDef, appId) {
|
|
|
478
475
|
: renderTree(menu, 0, config, appDef, appId);
|
|
479
476
|
const groups = collectGroups(menu);
|
|
480
477
|
return `<div id="bp-menu-editor" data-bp-app-id="${escapeHtml(appId)}">
|
|
478
|
+
<style>
|
|
479
|
+
.bp-menu-loading { display: none; }
|
|
480
|
+
.bp-menu-loading.htmx-request { display: flex; position: absolute; inset: 0; z-index: 5; align-items: center; justify-content: center; gap: .5rem; background: var(--bs-body-bg); opacity: .9; }
|
|
481
|
+
</style>
|
|
481
482
|
<form id="bp-drag-move-form" style="display:none"
|
|
482
483
|
hx-post="${API_BASE}/menu-editor/move-after"
|
|
483
484
|
hx-target="#bp-menu-editor"
|
|
484
|
-
hx-swap="outerHTML">
|
|
485
|
+
hx-swap="outerHTML settle:0">
|
|
485
486
|
<input type="hidden" name="appId" value="${escapeHtml(appId)}" />
|
|
486
487
|
<input type="hidden" name="itemId" />
|
|
487
488
|
<input type="hidden" name="anchorId" />
|
|
@@ -492,9 +493,25 @@ function renderEditor(config, appDef, appId) {
|
|
|
492
493
|
</div>`;
|
|
493
494
|
}
|
|
494
495
|
// -- Endpoint registration --------------------------------------------
|
|
495
|
-
export function registerMenuEditorRoutes(app, store) {
|
|
496
|
-
const
|
|
497
|
-
|
|
496
|
+
export function registerMenuEditorRoutes(app, store, database) {
|
|
497
|
+
const read = async (appId) => {
|
|
498
|
+
if (!database)
|
|
499
|
+
return store.loadConfig();
|
|
500
|
+
return readApp(database, appId);
|
|
501
|
+
};
|
|
502
|
+
const post = (path, action) => {
|
|
503
|
+
app.post(`${API_BASE}/menu-editor/${path}`, async (event) => {
|
|
504
|
+
const form = await readFormBody(event);
|
|
505
|
+
if (database)
|
|
506
|
+
return editAppRows(database, form.appId, eventObservability(event), data => action(form, data));
|
|
507
|
+
const config = await store.loadConfig();
|
|
508
|
+
const response = await action(form, config);
|
|
509
|
+
if (response.ok)
|
|
510
|
+
await store.saveConfig(config);
|
|
511
|
+
return response;
|
|
512
|
+
});
|
|
513
|
+
};
|
|
514
|
+
const respondEditor = async (config, appId) => {
|
|
498
515
|
const appDef = getApp(config, appId);
|
|
499
516
|
if (!appDef)
|
|
500
517
|
return htmlResponse(`<div class="alert alert-danger">App not found</div>`, 200, "text/html; mode=fragment");
|
|
@@ -502,8 +519,7 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
502
519
|
"HX-Trigger": "bp:menu-changed"
|
|
503
520
|
});
|
|
504
521
|
};
|
|
505
|
-
const respondRow = async (appId, itemId, mode) => {
|
|
506
|
-
const config = await store.loadConfig();
|
|
522
|
+
const respondRow = async (config, appId, itemId, mode) => {
|
|
507
523
|
const appDef = getApp(config, appId);
|
|
508
524
|
if (!appDef)
|
|
509
525
|
return htmlResponse("", 200, "text/html; mode=fragment");
|
|
@@ -547,18 +563,16 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
547
563
|
const appId = url.searchParams.get("appId") ?? "";
|
|
548
564
|
if (!appId)
|
|
549
565
|
return htmlResponse(`<div class="alert alert-secondary">Select an app</div>`, 200, "text/html; mode=fragment");
|
|
550
|
-
return respondEditor(appId);
|
|
566
|
+
return respondEditor(await read(appId), appId);
|
|
551
567
|
});
|
|
552
568
|
app.get(`${API_BASE}/menu-editor/item`, async (event) => {
|
|
553
569
|
const url = new URL(event.req.url ?? "", RELATIVE_URL_PARSE_BASE);
|
|
554
570
|
const appId = url.searchParams.get("appId") ?? "";
|
|
555
571
|
const itemId = url.searchParams.get("itemId") ?? "";
|
|
556
572
|
const mode = (url.searchParams.get("mode") ?? "display");
|
|
557
|
-
return respondRow(appId, itemId, mode);
|
|
573
|
+
return respondRow(await read(appId), appId, itemId, mode);
|
|
558
574
|
});
|
|
559
|
-
|
|
560
|
-
const f = await readFormBody(event);
|
|
561
|
-
const config = await store.loadConfig();
|
|
575
|
+
post("save-visibility", async (f, config) => {
|
|
562
576
|
const appDef = getApp(config, f.appId);
|
|
563
577
|
if (!appDef)
|
|
564
578
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -568,14 +582,31 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
568
582
|
if (!["auto", "show", "show-unauthenticated", "hide-unauthenticated", "hide-unauthorized"].includes(f.authStatus)) {
|
|
569
583
|
return jsonResponse({ error: "Invalid visibility" }, 400);
|
|
570
584
|
}
|
|
571
|
-
|
|
572
|
-
found.item.
|
|
573
|
-
|
|
574
|
-
|
|
585
|
+
const audience = f.authStatus;
|
|
586
|
+
if (found.item.type === "group") {
|
|
587
|
+
const applyAudience = (items) => {
|
|
588
|
+
for (const item of items) {
|
|
589
|
+
item.authStatus = audience;
|
|
590
|
+
if (item.type === "link")
|
|
591
|
+
delete item.rolesAnyOf;
|
|
592
|
+
if (item.type === "group")
|
|
593
|
+
applyAudience(item.children ?? []);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
applyAudience(found.item.children ?? []);
|
|
597
|
+
}
|
|
598
|
+
found.item.authStatus = audience;
|
|
599
|
+
if (found.item.type === "link")
|
|
600
|
+
found.item.serviceStatus = f.serviceStatus === "hide" ? "hide" : "show";
|
|
601
|
+
if (found.item.type === "link" || f.clearRoleRestriction === "true")
|
|
602
|
+
delete found.item.rolesAnyOf;
|
|
603
|
+
if (found.item.type === "group")
|
|
604
|
+
return respondEditor(config, f.appId);
|
|
605
|
+
const response = await respondRow(config, f.appId, f.itemId, "display");
|
|
606
|
+
response.headers.set("HX-Trigger", "bp:menu-changed");
|
|
607
|
+
return response;
|
|
575
608
|
});
|
|
576
|
-
|
|
577
|
-
const f = await readFormBody(event);
|
|
578
|
-
const config = await store.loadConfig();
|
|
609
|
+
post("save-title", async (f, config) => {
|
|
579
610
|
const appDef = getApp(config, f.appId);
|
|
580
611
|
if (!appDef)
|
|
581
612
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -584,23 +615,11 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
584
615
|
if (found)
|
|
585
616
|
found.item.title = f.title || undefined;
|
|
586
617
|
appDef.menu = menu;
|
|
587
|
-
await
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const appDef2 = getApp(config2, f.appId);
|
|
591
|
-
const found2 = appDef2 ? locate(getMenu(appDef2), f.itemId) : null;
|
|
592
|
-
if (!appDef2 || !found2)
|
|
593
|
-
return htmlResponse("", 200, "text/html; mode=fragment");
|
|
594
|
-
const route = found2.item.routeId
|
|
595
|
-
? (appDef2.routes ?? []).find((candidate) => candidate.id === found2.item.routeId) ?? null
|
|
596
|
-
: null;
|
|
597
|
-
return htmlResponse(titleDisplayHtml(found2.item, route, f.appId), 200, "text/html; mode=fragment", {
|
|
598
|
-
"HX-Trigger": "bp:menu-changed"
|
|
599
|
-
});
|
|
618
|
+
const response = await respondRow(config, f.appId, f.itemId, "display");
|
|
619
|
+
response.headers.set("HX-Trigger", "bp:menu-changed");
|
|
620
|
+
return response;
|
|
600
621
|
});
|
|
601
|
-
|
|
602
|
-
const f = await readFormBody(event);
|
|
603
|
-
const config = await store.loadConfig();
|
|
622
|
+
post("save-link", async (f, config) => {
|
|
604
623
|
const appDef = getApp(config, f.appId);
|
|
605
624
|
if (!appDef)
|
|
606
625
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -610,7 +629,8 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
610
629
|
return htmlResponse(`<div class="alert alert-danger">Item not found</div>`, 200, "text/html; mode=fragment");
|
|
611
630
|
}
|
|
612
631
|
found.item.title = f.title || undefined;
|
|
613
|
-
found.item.
|
|
632
|
+
if (found.item.type === "link" || f.clearRoleRestriction === "true")
|
|
633
|
+
delete found.item.rolesAnyOf;
|
|
614
634
|
found.item.serviceStatus = f.serviceStatus === "hide" ? "hide" : "show";
|
|
615
635
|
found.item.authStatus = ["auto", "show", "hide-unauthenticated", "hide-unauthorized", "show-unauthenticated"].includes(f.authStatus) ? f.authStatus : "auto";
|
|
616
636
|
const route = (appDef.routes ?? []).find((r) => r.id === found.item.routeId);
|
|
@@ -628,8 +648,7 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
628
648
|
return jsonResponse({ error: "Route cannot be added to the menu" }, 400);
|
|
629
649
|
}
|
|
630
650
|
appDef.menu = menu;
|
|
631
|
-
|
|
632
|
-
return respondRow(f.appId, f.itemId, "display").then((r) => {
|
|
651
|
+
return respondRow(config, f.appId, f.itemId, "display").then((r) => {
|
|
633
652
|
const headers = new Headers(r.headers);
|
|
634
653
|
headers.set("HX-Trigger", "bp:menu-changed");
|
|
635
654
|
return new Response(r.body, { status: r.status, headers });
|
|
@@ -656,7 +675,7 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
656
675
|
if (url.searchParams.get("autoSetPaths") === "true" && serviceId && viewId) {
|
|
657
676
|
const views = lookupServiceViews(serviceId);
|
|
658
677
|
targetPath = views.find((v) => v.viewId === viewId)?.path ?? "";
|
|
659
|
-
const config = await
|
|
678
|
+
const config = await read(appId);
|
|
660
679
|
const appDef = getApp(config, appId);
|
|
661
680
|
const assignedRoute = (appDef?.routes ?? []).find((route) => route.kind !== "api" && route.serviceId === serviceId && route.viewId === viewId);
|
|
662
681
|
if (assignedRoute) {
|
|
@@ -666,16 +685,15 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
666
685
|
}
|
|
667
686
|
return htmlResponse(renderPathFields(itemId, path, targetPath), 200, "text/html; mode=fragment");
|
|
668
687
|
});
|
|
669
|
-
|
|
670
|
-
const f = await readFormBody(event);
|
|
671
|
-
const config = await store.loadConfig();
|
|
688
|
+
post("save-external", async (f, config) => {
|
|
672
689
|
const appDef = getApp(config, f.appId);
|
|
673
690
|
if (!appDef)
|
|
674
691
|
return jsonResponse({ error: "App not found" }, 404);
|
|
675
692
|
const menu = getMenu(appDef);
|
|
676
693
|
const found = locate(menu, f.itemId);
|
|
677
694
|
if (found) {
|
|
678
|
-
found.item.
|
|
695
|
+
if (found.item.type === "link" || f.clearRoleRestriction === "true")
|
|
696
|
+
delete found.item.rolesAnyOf;
|
|
679
697
|
found.item.authStatus = ["auto", "show", "hide-unauthenticated", "hide-unauthorized", "show-unauthenticated"].includes(f.authStatus) ? f.authStatus : "auto";
|
|
680
698
|
found.item.href = f.href || "";
|
|
681
699
|
if (f.title !== undefined && f.title !== "")
|
|
@@ -685,17 +703,14 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
685
703
|
return jsonResponse({ error: "Route cannot be added to the menu" }, 400);
|
|
686
704
|
}
|
|
687
705
|
appDef.menu = menu;
|
|
688
|
-
|
|
689
|
-
return respondRow(f.appId, f.itemId, "display").then((r) => {
|
|
706
|
+
return respondRow(config, f.appId, f.itemId, "display").then((r) => {
|
|
690
707
|
// Add HX-Trigger to existing response
|
|
691
708
|
const headers = new Headers(r.headers);
|
|
692
709
|
headers.set("HX-Trigger", "bp:menu-changed");
|
|
693
710
|
return new Response(r.body, { status: r.status, headers });
|
|
694
711
|
});
|
|
695
712
|
});
|
|
696
|
-
|
|
697
|
-
const f = await readFormBody(event);
|
|
698
|
-
const config = await store.loadConfig();
|
|
713
|
+
post("add", async (f, config) => {
|
|
699
714
|
const appDef = getApp(config, f.appId);
|
|
700
715
|
if (!appDef)
|
|
701
716
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -727,12 +742,9 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
727
742
|
menu.push(newItem);
|
|
728
743
|
}
|
|
729
744
|
appDef.menu = menu;
|
|
730
|
-
|
|
731
|
-
return respondEditor(f.appId);
|
|
745
|
+
return respondEditor(config, f.appId);
|
|
732
746
|
});
|
|
733
|
-
|
|
734
|
-
const f = await readFormBody(event);
|
|
735
|
-
const config = await store.loadConfig();
|
|
747
|
+
post("remove", async (f, config) => {
|
|
736
748
|
const appDef = getApp(config, f.appId);
|
|
737
749
|
if (!appDef)
|
|
738
750
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -741,12 +753,9 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
741
753
|
if (found)
|
|
742
754
|
found.parent.splice(found.index, 1);
|
|
743
755
|
appDef.menu = menu;
|
|
744
|
-
|
|
745
|
-
return respondEditor(f.appId);
|
|
756
|
+
return respondEditor(config, f.appId);
|
|
746
757
|
});
|
|
747
|
-
|
|
748
|
-
const f = await readFormBody(event);
|
|
749
|
-
const config = await store.loadConfig();
|
|
758
|
+
post("toggle", async (f, config) => {
|
|
750
759
|
const appDef = getApp(config, f.appId);
|
|
751
760
|
if (!appDef)
|
|
752
761
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -755,12 +764,9 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
755
764
|
if (found)
|
|
756
765
|
found.item.enabled = !found.item.enabled;
|
|
757
766
|
appDef.menu = menu;
|
|
758
|
-
|
|
759
|
-
return respondEditor(f.appId);
|
|
767
|
+
return respondEditor(config, f.appId);
|
|
760
768
|
});
|
|
761
|
-
|
|
762
|
-
const f = await readFormBody(event);
|
|
763
|
-
const config = await store.loadConfig();
|
|
769
|
+
post("toggle-expanded", async (f, config) => {
|
|
764
770
|
const appDef = getApp(config, f.appId);
|
|
765
771
|
if (!appDef)
|
|
766
772
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -770,12 +776,9 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
770
776
|
found.item.defaultExpanded = !found.item.defaultExpanded;
|
|
771
777
|
}
|
|
772
778
|
appDef.menu = menu;
|
|
773
|
-
|
|
774
|
-
return respondEditor(f.appId);
|
|
779
|
+
return respondEditor(config, f.appId);
|
|
775
780
|
});
|
|
776
|
-
|
|
777
|
-
const f = await readFormBody(event);
|
|
778
|
-
const config = await store.loadConfig();
|
|
781
|
+
post("move-up", async (f, config) => {
|
|
779
782
|
const appDef = getApp(config, f.appId);
|
|
780
783
|
if (!appDef)
|
|
781
784
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -786,12 +789,9 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
786
789
|
found.parent.splice(found.index - 1, 0, it);
|
|
787
790
|
}
|
|
788
791
|
appDef.menu = menu;
|
|
789
|
-
|
|
790
|
-
return respondEditor(f.appId);
|
|
792
|
+
return respondEditor(config, f.appId);
|
|
791
793
|
});
|
|
792
|
-
|
|
793
|
-
const f = await readFormBody(event);
|
|
794
|
-
const config = await store.loadConfig();
|
|
794
|
+
post("move-down", async (f, config) => {
|
|
795
795
|
const appDef = getApp(config, f.appId);
|
|
796
796
|
if (!appDef)
|
|
797
797
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -802,12 +802,9 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
802
802
|
found.parent.splice(found.index + 1, 0, it);
|
|
803
803
|
}
|
|
804
804
|
appDef.menu = menu;
|
|
805
|
-
|
|
806
|
-
return respondEditor(f.appId);
|
|
805
|
+
return respondEditor(config, f.appId);
|
|
807
806
|
});
|
|
808
|
-
|
|
809
|
-
const f = await readFormBody(event);
|
|
810
|
-
const config = await store.loadConfig();
|
|
807
|
+
post("move-in", async (f, config) => {
|
|
811
808
|
const appDef = getApp(config, f.appId);
|
|
812
809
|
if (!appDef)
|
|
813
810
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -822,12 +819,9 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
822
819
|
}
|
|
823
820
|
}
|
|
824
821
|
appDef.menu = menu;
|
|
825
|
-
|
|
826
|
-
return respondEditor(f.appId);
|
|
822
|
+
return respondEditor(config, f.appId);
|
|
827
823
|
});
|
|
828
|
-
|
|
829
|
-
const f = await readFormBody(event);
|
|
830
|
-
const config = await store.loadConfig();
|
|
824
|
+
post("move-after", async (f, config) => {
|
|
831
825
|
const appDef = getApp(config, f.appId);
|
|
832
826
|
if (!appDef)
|
|
833
827
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -836,10 +830,10 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
836
830
|
const anchorId = f.anchorId || "";
|
|
837
831
|
const targetDepth = Math.max(0, parseInt(f.targetDepth ?? "0", 10) || 0);
|
|
838
832
|
if (!srcId || srcId === anchorId)
|
|
839
|
-
return respondEditor(f.appId);
|
|
833
|
+
return respondEditor(config, f.appId);
|
|
840
834
|
const src = locate(menu, srcId);
|
|
841
835
|
if (!src)
|
|
842
|
-
return respondEditor(f.appId);
|
|
836
|
+
return respondEditor(config, f.appId);
|
|
843
837
|
// Prevent dropping into own descendant
|
|
844
838
|
const isDescendant = (items, id) => {
|
|
845
839
|
for (const it of items) {
|
|
@@ -851,7 +845,7 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
851
845
|
return false;
|
|
852
846
|
};
|
|
853
847
|
if (anchorId && src.item.type === "group" && src.item.children && isDescendant(src.item.children, anchorId)) {
|
|
854
|
-
return respondEditor(f.appId);
|
|
848
|
+
return respondEditor(config, f.appId);
|
|
855
849
|
}
|
|
856
850
|
// Remove src
|
|
857
851
|
const [srcItem] = src.parent.splice(src.index, 1);
|
|
@@ -859,8 +853,7 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
859
853
|
// Insert at start of root
|
|
860
854
|
menu.unshift(srcItem);
|
|
861
855
|
appDef.menu = menu;
|
|
862
|
-
|
|
863
|
-
return respondEditor(f.appId);
|
|
856
|
+
return respondEditor(config, f.appId);
|
|
864
857
|
}
|
|
865
858
|
// Build ancestor chain to anchor
|
|
866
859
|
const ancestorChain = (items, id, chain = [], depth = 0) => {
|
|
@@ -879,7 +872,7 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
879
872
|
const chain = ancestorChain(menu, anchorId);
|
|
880
873
|
if (!chain) {
|
|
881
874
|
src.parent.splice(src.index, 0, srcItem);
|
|
882
|
-
return respondEditor(f.appId);
|
|
875
|
+
return respondEditor(config, f.appId);
|
|
883
876
|
}
|
|
884
877
|
const anchor = chain[chain.length - 1];
|
|
885
878
|
const anchorDepth = anchor.depth;
|
|
@@ -904,12 +897,9 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
904
897
|
}
|
|
905
898
|
}
|
|
906
899
|
appDef.menu = menu;
|
|
907
|
-
|
|
908
|
-
return respondEditor(f.appId);
|
|
900
|
+
return respondEditor(config, f.appId);
|
|
909
901
|
});
|
|
910
|
-
|
|
911
|
-
const f = await readFormBody(event);
|
|
912
|
-
const config = await store.loadConfig();
|
|
902
|
+
post("move-out", async (f, config) => {
|
|
913
903
|
const appDef = getApp(config, f.appId);
|
|
914
904
|
if (!appDef)
|
|
915
905
|
return jsonResponse({ error: "App not found" }, 404);
|
|
@@ -924,8 +914,7 @@ export function registerMenuEditorRoutes(app, store) {
|
|
|
924
914
|
}
|
|
925
915
|
}
|
|
926
916
|
appDef.menu = menu;
|
|
927
|
-
|
|
928
|
-
return respondEditor(f.appId);
|
|
917
|
+
return respondEditor(config, f.appId);
|
|
929
918
|
});
|
|
930
919
|
}
|
|
931
920
|
//# sourceMappingURL=menuEditor.js.map
|