@nocobase/plugin-multi-portal 3.0.0-alpha.4 → 3.0.0-alpha.6

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.
@@ -60,9 +60,9 @@ const MULTI_PORTAL_RUNTIME_FIELDS = [
60
60
  "portalName",
61
61
  "routePath",
62
62
  "authCheck",
63
- "enabled"
63
+ "enabled",
64
+ "uiLayoutUid"
64
65
  ];
65
- const MULTI_PORTAL_RUNTIME_QUERY_FIELDS = [...MULTI_PORTAL_RUNTIME_FIELDS, "uiLayoutUid"];
66
66
  const MULTI_PORTAL_ACCESSIBLE_FIELDS = [
67
67
  "uid",
68
68
  "title",
@@ -71,10 +71,9 @@ const MULTI_PORTAL_ACCESSIBLE_FIELDS = [
71
71
  "portalName",
72
72
  "routePath",
73
73
  "authCheck",
74
- "enabled"
74
+ "enabled",
75
+ "uiLayoutUid"
75
76
  ];
76
- const MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS = [...MULTI_PORTAL_ACCESSIBLE_FIELDS, "uiLayoutUid"];
77
- const MULTI_PORTAL_UI_LAYOUT_RUNTIME_FIELDS = ["layoutType"];
78
77
  const DESKTOP_ROUTE_ROLE_PERMISSION_TARGET_FIELDS = ["id", "title", "hidden", "parentId", "options"];
79
78
  const UI_LAYOUT_DESKTOP_ROUTE_WRITE_LAYOUT_HANDLER_TAG = "plugin-ui-layout:desktop-route-write-layout";
80
79
  const MAIN_APP_NAME = "main";
@@ -181,7 +180,7 @@ function getDefaultMultiPortalRecord(options = {}) {
181
180
  authCheck: true,
182
181
  enabled: true,
183
182
  ...options.isDefault ? { isDefault: true } : {},
184
- uiLayoutUid: "admin-layout-model"
183
+ uiLayoutUid: import_constants.ADMIN_UI_LAYOUT_UID
185
184
  };
186
185
  }
187
186
  function getFreshMultiPortalRecords() {
@@ -198,7 +197,7 @@ function getFixedLayoutMultiPortalRecords() {
198
197
  routePath: "/admin",
199
198
  authCheck: true,
200
199
  enabled: true,
201
- uiLayoutUid: "admin-layout-model"
200
+ uiLayoutUid: import_constants.ADMIN_UI_LAYOUT_UID
202
201
  },
203
202
  {
204
203
  uid: import_constants.DEFAULT_MOBILE_MULTI_PORTAL_UID,
@@ -209,7 +208,7 @@ function getFixedLayoutMultiPortalRecords() {
209
208
  routePath: "/mobile",
210
209
  authCheck: true,
211
210
  enabled: true,
212
- uiLayoutUid: "mobile-layout-model"
211
+ uiLayoutUid: import_constants.MOBILE_UI_LAYOUT_UID
213
212
  }
214
213
  ];
215
214
  }
@@ -294,6 +293,13 @@ function getPortalDeployBasePath(appName, portalName) {
294
293
  const portalPath = appName === MAIN_APP_NAME ? `/${PORTAL_CLIENT_PREFIX}/${portalName}/` : `/${PORTAL_CLIENT_PREFIX}/apps/${appName}/${portalName}/`;
295
294
  return resolvePortalStoragePublicPath(joinPortalStoragePublicPath(process.env.APP_PUBLIC_PATH || "/", portalPath));
296
295
  }
296
+ function getPortalDeployBasePathCandidates(appName, portalName) {
297
+ const expectedBasePaths = [getPortalDeployBasePath(appName, portalName)];
298
+ if (appName !== MAIN_APP_NAME) {
299
+ expectedBasePaths.push(getPortalDeployBasePath(MAIN_APP_NAME, portalName));
300
+ }
301
+ return expectedBasePaths;
302
+ }
297
303
  function createPortalDeployUploadMiddleware() {
298
304
  const storage = import_utils.koaMulter.diskStorage({
299
305
  destination: import_os.default.tmpdir(),
@@ -436,80 +442,13 @@ async function resolvePortalTemplate(templateSource, logPath) {
436
442
  return downloadPortalTemplatePackage(templateSource, logPath);
437
443
  }
438
444
  async function copyPortalTemplate(sourceDir, targetDir) {
439
- const ignoredSegments = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store"]);
445
+ const ignoredSegments = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".env", ".env.local"]);
440
446
  await import_fs.default.promises.mkdir(import_path.default.dirname(targetDir), { recursive: true });
441
447
  await import_fs.default.promises.cp(sourceDir, targetDir, {
442
448
  recursive: true,
443
449
  filter: (source) => !import_path.default.relative(sourceDir, source).split(import_path.default.sep).some((segment) => segment.startsWith("._") || ignoredSegments.has(segment))
444
450
  });
445
451
  }
446
- function getPortalStorageConfig(options) {
447
- const sourceOptions = isRecordLike(options) ? options : {};
448
- const sourceStorage = trimString(sourceOptions.sourceStorage);
449
- if (sourceStorage !== "git") {
450
- return { sourceStorage: "nocobase" };
451
- }
452
- const gitOptions = isRecordLike(sourceOptions.git) ? sourceOptions.git : {};
453
- return {
454
- sourceStorage,
455
- git: {
456
- repo: trimString(gitOptions.repo),
457
- branch: trimString(gitOptions.branch) || "main",
458
- path: trimString(gitOptions.path) || "."
459
- }
460
- };
461
- }
462
- function upsertPortalEnvContent(content, values) {
463
- const nextValues = { ...values };
464
- const lines = content ? content.replace(/\r\n/g, "\n").split("\n") : [];
465
- const result = [];
466
- for (const line of lines) {
467
- if (!line && result.length === lines.length - 1) {
468
- continue;
469
- }
470
- const match = line.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=/);
471
- const key = match == null ? void 0 : match[2];
472
- if (key && Object.prototype.hasOwnProperty.call(nextValues, key)) {
473
- result.push(`${key}=${nextValues[key]}`);
474
- delete nextValues[key];
475
- continue;
476
- }
477
- result.push(line);
478
- }
479
- for (const [key, value] of Object.entries(nextValues)) {
480
- result.push(`${key}=${value}`);
481
- }
482
- return `${result.join("\n").replace(/\n*$/, "")}
483
- `;
484
- }
485
- async function upsertPortalEnvFile(filePath, values) {
486
- let content = "";
487
- try {
488
- content = await import_fs.default.promises.readFile(filePath, "utf-8");
489
- } catch {
490
- content = "";
491
- }
492
- await import_fs.default.promises.writeFile(filePath, upsertPortalEnvContent(content, values), "utf-8");
493
- }
494
- async function ensurePortalStorageConfigFiles(portalDir, item) {
495
- const apiUrl = getPortalStorageApiUrl(item.appName);
496
- const portalBase = getPortalDeployBasePath(item.appName, item.portalName);
497
- await import_fs.default.promises.mkdir(portalDir, { recursive: true });
498
- await upsertPortalEnvFile(import_path.default.join(portalDir, ".env"), {
499
- NOCOBASE_API_URL: resolvePortalStorageEnvApiUrl(apiUrl),
500
- NOCOBASE_PORTAL_BASE: portalBase
501
- });
502
- await upsertPortalEnvFile(import_path.default.join(portalDir, ".env.local"), {
503
- NOCOBASE_API_URL: apiUrl,
504
- NOCOBASE_PORTAL_BASE: portalBase
505
- });
506
- await import_fs.default.promises.writeFile(
507
- import_path.default.join(portalDir, "portal.config.json"),
508
- `${JSON.stringify(item.config, null, 2)}
509
- `,
510
- "utf-8"
511
- );
512
- }
513
452
  function sanitizePortalStorageNodeOptions(value) {
514
453
  return trimString(value).split(/\s+/).filter((option) => option !== "--preserve-symlinks" && option !== "--preserve-symlinks-main").join(" ");
515
454
  }
@@ -620,9 +559,9 @@ function validatePortalDeployBasePath(appName, portalName, basePath) {
620
559
  if (normalizedBasePath.includes("..")) {
621
560
  throw new Error('basePath cannot contain ".."');
622
561
  }
623
- const expectedBasePath = getPortalDeployBasePath(appName, portalName);
624
- if (normalizedBasePath !== expectedBasePath) {
625
- throw new Error(`basePath must be ${expectedBasePath}`);
562
+ const expectedBasePaths = getPortalDeployBasePathCandidates(appName, portalName);
563
+ if (!expectedBasePaths.includes(normalizedBasePath)) {
564
+ throw new Error(`basePath must be ${expectedBasePaths.join(" or ")}`);
626
565
  }
627
566
  }
628
567
  function isPortalDeployTarEntry(entry) {
@@ -860,19 +799,11 @@ function collectDesktopRouteIds(record) {
860
799
  }
861
800
  return uniqueDesktopRouteIds(routeIds);
862
801
  }
863
- function pickMultiPortalUiLayoutRuntimeFields(record) {
864
- const result = {};
865
- for (const field of MULTI_PORTAL_UI_LAYOUT_RUNTIME_FIELDS) {
866
- result[field] = getRecordField(record, field);
867
- }
868
- return result;
869
- }
870
802
  function pickMultiPortalRuntimeFields(record) {
871
803
  const result = {};
872
804
  for (const field of MULTI_PORTAL_RUNTIME_FIELDS) {
873
805
  result[field] = getRecordField(record, field);
874
806
  }
875
- result.uiLayout = pickMultiPortalUiLayoutRuntimeFields(getRecordField(record, "uiLayout"));
876
807
  return result;
877
808
  }
878
809
  function pickMultiPortalAccessibleFields(record) {
@@ -880,7 +811,6 @@ function pickMultiPortalAccessibleFields(record) {
880
811
  for (const field of MULTI_PORTAL_ACCESSIBLE_FIELDS) {
881
812
  result[field] = getRecordField(record, field) ?? null;
882
813
  }
883
- result.uiLayout = pickMultiPortalUiLayoutRuntimeFields(getRecordField(record, "uiLayout"));
884
814
  return result;
885
815
  }
886
816
  function getExplicitRequestedLayoutUid(layout) {
@@ -1130,14 +1060,27 @@ async function seedHistoricalMultiPortals(db) {
1130
1060
  }
1131
1061
  await repairFixedLayoutMultiPortalRecords(db);
1132
1062
  }
1133
- async function preventMultiPortalBackingLayoutChange(ctx, next) {
1063
+ async function validateMultiPortalUiLayoutUidWrite(ctx, next) {
1064
+ var _a;
1134
1065
  const targets = await getMultiPortalWriteTargets(ctx, ["uiLayoutUid"]);
1066
+ const actionName = (_a = ctx.action) == null ? void 0 : _a.actionName;
1067
+ const createsWhenMissing = actionName === "create" || actionName === "firstOrCreate" || actionName === "updateOrCreate";
1135
1068
  for (const { existing, values } of targets) {
1136
- if (!Object.prototype.hasOwnProperty.call(values, "uiLayoutUid")) {
1069
+ const hasUiLayoutUid = Object.prototype.hasOwnProperty.call(values, "uiLayoutUid");
1070
+ if (!existing && createsWhenMissing && !hasUiLayoutUid) {
1071
+ ctx.throw(400, `Portal UI layout must be one of: ${import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS.join(", ")}`);
1072
+ return;
1073
+ }
1074
+ if (!hasUiLayoutUid) {
1137
1075
  continue;
1138
1076
  }
1139
- if (existing && existing.get("uiLayoutUid") !== values.uiLayoutUid) {
1140
- ctx.throw(400, "Portal backing UI layout cannot be changed");
1077
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(values.uiLayoutUid)) {
1078
+ ctx.throw(400, `Portal UI layout must be one of: ${import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS.join(", ")}`);
1079
+ return;
1080
+ }
1081
+ const existingUiLayoutUid = existing == null ? void 0 : existing.get("uiLayoutUid");
1082
+ if (existing && existingUiLayoutUid !== values.uiLayoutUid) {
1083
+ ctx.throw(400, "Portal UI layout cannot be changed");
1141
1084
  return;
1142
1085
  }
1143
1086
  }
@@ -1165,20 +1108,8 @@ async function findRequestedMultiPortal(ctx, transaction) {
1165
1108
  ctx.throw(400, `Portal '${portalUid}' does not support desktop routes`);
1166
1109
  }
1167
1110
  const uiLayoutUid = portal.get("uiLayoutUid");
1168
- if (typeof uiLayoutUid !== "string" || !uiLayoutUid) {
1169
- ctx.throw(400, `Portal '${portalUid}' has no backing UI layout`);
1170
- }
1171
- const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
1172
- filter: {
1173
- uid: uiLayoutUid,
1174
- enabled: true
1175
- },
1176
- fields: ["uid"],
1177
- ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1178
- transaction
1179
- });
1180
- if (!uiLayout) {
1181
- ctx.throw(400, `Portal '${portalUid}' has no enabled backing UI layout`);
1111
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(uiLayoutUid)) {
1112
+ ctx.throw(400, `Portal '${portalUid}' has an unsupported UI layout UID`);
1182
1113
  }
1183
1114
  const usesLayoutPermissions = (0, import_constants.isDefaultLayoutMultiPortalUid)(portalUid);
1184
1115
  return {
@@ -1363,7 +1294,7 @@ async function removeRouteIdsWithUnauthorizedAncestors(ctx, routeIds) {
1363
1294
  }
1364
1295
  }
1365
1296
  }
1366
- async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1297
+ async function getMultiPortalAccessibleRouteIds(ctx, portalContext) {
1367
1298
  const currentRoles = getCurrentRoles(ctx);
1368
1299
  if (currentRoles.includes("root")) {
1369
1300
  return;
@@ -1371,11 +1302,11 @@ async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1371
1302
  if (!currentRoles.length) {
1372
1303
  return /* @__PURE__ */ new Set();
1373
1304
  }
1374
- const routePermissions = await ctx.db.getRepository("rolesMultiPortalDesktopRoutes").find({
1305
+ const routePermissions = await ctx.db.getRepository(portalContext.relation === "uiLayouts" ? "rolesDesktopRoutes" : "rolesMultiPortalDesktopRoutes").find({
1375
1306
  fields: ["desktopRouteId"],
1376
1307
  filter: {
1377
1308
  roleName: currentRoles,
1378
- multiPortalUid
1309
+ ...portalContext.relation === "multiPortals" ? { multiPortalUid: portalContext.portalUid } : {}
1379
1310
  }
1380
1311
  });
1381
1312
  const routeIds = /* @__PURE__ */ new Set();
@@ -1391,7 +1322,7 @@ async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1391
1322
  const portalRoutes = await ctx.db.getRepository("desktopRoutes").find({
1392
1323
  fields: ["id"],
1393
1324
  filter: {
1394
- ...getDesktopRoutePortalFilter(multiPortalUid),
1325
+ ...portalContext.filter,
1395
1326
  id: Array.from(routeIds)
1396
1327
  }
1397
1328
  });
@@ -1421,9 +1352,107 @@ function setDesktopRouteChildren(route, children) {
1421
1352
  const maybeModel = route;
1422
1353
  if (typeof maybeModel.setDataValue === "function") {
1423
1354
  maybeModel.setDataValue("children", children);
1355
+ } else {
1356
+ maybeModel.children = children;
1357
+ }
1358
+ if (!maybeModel._options) {
1359
+ return;
1360
+ }
1361
+ if (!maybeModel._options.includeNames) {
1362
+ maybeModel._options.includeNames = ["children"];
1424
1363
  return;
1425
1364
  }
1426
- route.children = children;
1365
+ if (!maybeModel._options.includeNames.includes("children")) {
1366
+ maybeModel._options.includeNames.push("children");
1367
+ }
1368
+ }
1369
+ function getDesktopRouteId(route) {
1370
+ const id = getRecordField(route, "id");
1371
+ return id === null || id === void 0 ? void 0 : String(id);
1372
+ }
1373
+ function getDesktopRouteParentId(route) {
1374
+ const parentId = getRecordField(route, "parentId");
1375
+ return parentId === null || parentId === void 0 ? void 0 : String(parentId);
1376
+ }
1377
+ function collectDesktopRouteStringIds(routes, routeIds) {
1378
+ for (const route of routes) {
1379
+ const routeId = getDesktopRouteId(route);
1380
+ if (routeId) {
1381
+ routeIds.add(routeId);
1382
+ }
1383
+ const children = getRecordField(route, "children");
1384
+ if (Array.isArray(children)) {
1385
+ collectDesktopRouteStringIds(children, routeIds);
1386
+ }
1387
+ }
1388
+ }
1389
+ function removeNestedRootDesktopRoutes(routes) {
1390
+ if (!Array.isArray(routes)) {
1391
+ return [];
1392
+ }
1393
+ const routeIds = /* @__PURE__ */ new Set();
1394
+ collectDesktopRouteStringIds(routes, routeIds);
1395
+ return routes.filter((route) => {
1396
+ const parentId = getDesktopRouteParentId(route);
1397
+ return !parentId || !routeIds.has(parentId);
1398
+ });
1399
+ }
1400
+ function buildAccessibleDesktopRouteTreeWithAncestors(routes, accessibleRouteIds) {
1401
+ const routeById = /* @__PURE__ */ new Map();
1402
+ const childrenByParentId = /* @__PURE__ */ new Map();
1403
+ const roots = [];
1404
+ for (const route of routes) {
1405
+ const routeId = getDesktopRouteId(route);
1406
+ if (!routeId) {
1407
+ continue;
1408
+ }
1409
+ setDesktopRouteChildren(route, void 0);
1410
+ routeById.set(routeId, route);
1411
+ }
1412
+ for (const route of routes) {
1413
+ const routeId = getDesktopRouteId(route);
1414
+ if (!routeId || !routeById.has(routeId)) {
1415
+ continue;
1416
+ }
1417
+ const parentId = getDesktopRouteParentId(route);
1418
+ if (!parentId || !routeById.has(parentId)) {
1419
+ roots.push(route);
1420
+ continue;
1421
+ }
1422
+ const children = childrenByParentId.get(parentId) ?? [];
1423
+ children.push(route);
1424
+ childrenByParentId.set(parentId, children);
1425
+ }
1426
+ const visitRoute = (route, visitingRouteIds) => {
1427
+ const routeId = getDesktopRouteId(route);
1428
+ if (!routeId || visitingRouteIds.has(routeId)) {
1429
+ return void 0;
1430
+ }
1431
+ visitingRouteIds.add(routeId);
1432
+ const visibleChildren = (childrenByParentId.get(routeId) ?? []).map((child) => visitRoute(child, visitingRouteIds)).filter((child) => child !== void 0);
1433
+ visitingRouteIds.delete(routeId);
1434
+ if (!accessibleRouteIds.has(routeId) && visibleChildren.length === 0) {
1435
+ return void 0;
1436
+ }
1437
+ setDesktopRouteChildren(route, visibleChildren.length ? visibleChildren : void 0);
1438
+ return route;
1439
+ };
1440
+ return roots.map((route) => visitRoute(route, /* @__PURE__ */ new Set())).filter((route) => route !== void 0);
1441
+ }
1442
+ async function includeDesktopRouteAncestorsForListAccessible(ctx, routes, portalFilter) {
1443
+ if (!Array.isArray(routes)) {
1444
+ return routes;
1445
+ }
1446
+ const accessibleRouteIds = /* @__PURE__ */ new Set();
1447
+ collectDesktopRouteStringIds(routes, accessibleRouteIds);
1448
+ if (!accessibleRouteIds.size) {
1449
+ return routes;
1450
+ }
1451
+ const portalRoutes = await ctx.db.getRepository("desktopRoutes").find({
1452
+ sort: "sort",
1453
+ filter: portalFilter
1454
+ });
1455
+ return buildAccessibleDesktopRouteTreeWithAncestors(portalRoutes, accessibleRouteIds);
1427
1456
  }
1428
1457
  function removeDesktopRoutesByIds(routes, routeIds) {
1429
1458
  return routes.map((route) => {
@@ -1490,23 +1519,26 @@ async function removeMultiPortalOwnedRouteFromGetResponse(ctx) {
1490
1519
  ctx.body = void 0;
1491
1520
  }
1492
1521
  async function replaceListAccessibleRoutesWithPortalScopedRoutes(ctx, portalContext) {
1493
- const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext.portalUid);
1522
+ const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext);
1494
1523
  if (routeIds && routeIds.size === 0) {
1495
1524
  ctx.body = [];
1496
1525
  return;
1497
1526
  }
1498
- ctx.body = await ctx.db.getRepository("desktopRoutes").find({
1527
+ const routes = await ctx.db.getRepository("desktopRoutes").find({
1499
1528
  tree: true,
1500
1529
  sort: "sort",
1501
1530
  filter: {
1502
- ...getDesktopRoutePortalFilter(portalContext.portalUid),
1531
+ ...portalContext.filter,
1503
1532
  ...routeIds ? { id: Array.from(routeIds) } : {}
1504
1533
  }
1505
1534
  });
1535
+ ctx.body = removeNestedRootDesktopRoutes(
1536
+ await includeDesktopRouteAncestorsForListAccessible(ctx, routes, portalContext.filter)
1537
+ );
1506
1538
  }
1507
1539
  async function replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext) {
1508
1540
  var _a;
1509
- const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext.portalUid);
1541
+ const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext);
1510
1542
  if (routeIds && routeIds.size === 0) {
1511
1543
  ctx.status = 204;
1512
1544
  ctx.body = void 0;
@@ -1516,7 +1548,7 @@ async function replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext
1516
1548
  sort: "sort",
1517
1549
  filterByTk: (_a = ctx.action) == null ? void 0 : _a.params.filterByTk,
1518
1550
  filter: {
1519
- ...getDesktopRoutePortalFilter(portalContext.portalUid),
1551
+ ...portalContext.filter,
1520
1552
  ...routeIds ? { id: Array.from(routeIds) } : {}
1521
1553
  }
1522
1554
  });
@@ -1553,13 +1585,10 @@ async function addMultiPortalListAccessibleGuard(ctx, next) {
1553
1585
  return;
1554
1586
  }
1555
1587
  await next();
1556
- if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1588
+ if (portalContext) {
1557
1589
  await replaceListAccessibleRoutesWithPortalScopedRoutes(ctx, portalContext);
1558
1590
  return;
1559
1591
  }
1560
- if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1561
- return;
1562
- }
1563
1592
  await removeMultiPortalOwnedRoutesFromListResponse(ctx);
1564
1593
  }
1565
1594
  async function addMultiPortalGetAccessibleGuard(ctx, next) {
@@ -1570,17 +1599,13 @@ async function addMultiPortalGetAccessibleGuard(ctx, next) {
1570
1599
  return;
1571
1600
  }
1572
1601
  await next();
1573
- if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1602
+ if (portalContext) {
1574
1603
  await replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext);
1575
1604
  return;
1576
1605
  }
1577
- if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1578
- return;
1579
- }
1580
1606
  await removeMultiPortalOwnedRouteFromGetResponse(ctx);
1581
1607
  }
1582
1608
  async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next) {
1583
- var _a;
1584
1609
  const portalRequest = await findRequestedMultiPortal(ctx);
1585
1610
  if (portalRequest.requested && !portalRequest.portal) {
1586
1611
  ctx.status = 200;
@@ -1588,22 +1613,19 @@ async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next)
1588
1613
  return;
1589
1614
  }
1590
1615
  const scope = portalRequest.scope;
1591
- if ((scope == null ? void 0 : scope.relation) === "uiLayouts") {
1592
- if ((_a = ctx.action) == null ? void 0 : _a.params) {
1593
- ctx.action.params.layout = scope.uiLayoutUid;
1594
- }
1595
- await next();
1596
- return;
1597
- }
1598
- if ((scope == null ? void 0 : scope.relation) === "multiPortals") {
1616
+ if (scope) {
1599
1617
  const routes = await ctx.db.getRepository("desktopRoutes").find({
1600
- tree: true,
1601
1618
  sort: "sort",
1602
1619
  filter: scope.filter,
1603
1620
  fields: [...DESKTOP_ROUTE_ROLE_PERMISSION_TARGET_FIELDS]
1604
1621
  });
1622
+ const routeIds = /* @__PURE__ */ new Set();
1623
+ collectDesktopRouteStringIds(routes, routeIds);
1624
+ const routeTree = buildAccessibleDesktopRouteTreeWithAncestors(routes, routeIds);
1605
1625
  ctx.status = 200;
1606
- ctx.body = routes.map((route) => pickDesktopRouteRolePermissionTargetFields(route));
1626
+ ctx.body = removeNestedRootDesktopRoutes(routeTree).map(
1627
+ (route) => pickDesktopRouteRolePermissionTargetFields(route)
1628
+ );
1607
1629
  return;
1608
1630
  }
1609
1631
  await next();
@@ -2191,10 +2213,11 @@ async function listEnabledMultiPortals(ctx, next) {
2191
2213
  const records = await ctx.db.getRepository("multiPortals").find({
2192
2214
  filter: {
2193
2215
  enabled: true,
2194
- "uiLayout.enabled": true
2216
+ uiLayoutUid: {
2217
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2218
+ }
2195
2219
  },
2196
- fields: [...MULTI_PORTAL_RUNTIME_QUERY_FIELDS],
2197
- appends: ["uiLayout"],
2220
+ fields: [...MULTI_PORTAL_RUNTIME_FIELDS],
2198
2221
  sort: ["uid"]
2199
2222
  });
2200
2223
  ctx.body = records.map((record) => pickMultiPortalRuntimeFields(record));
@@ -2222,7 +2245,6 @@ async function findEnabledDefaultMultiPortal(ctx, transaction) {
2222
2245
  isDefault: true
2223
2246
  },
2224
2247
  fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS, "uiLayoutUid"],
2225
- appends: ["uiLayout"],
2226
2248
  transaction
2227
2249
  });
2228
2250
  if (!record) {
@@ -2232,11 +2254,8 @@ async function findEnabledDefaultMultiPortal(ctx, transaction) {
2232
2254
  if (!portalType) {
2233
2255
  return null;
2234
2256
  }
2235
- if (portalType === "no-code") {
2236
- const uiLayout = record.get("uiLayout");
2237
- if (!uiLayout || uiLayout.get("enabled") !== true) {
2238
- return null;
2239
- }
2257
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(record.get("uiLayoutUid"))) {
2258
+ return null;
2240
2259
  }
2241
2260
  return record;
2242
2261
  }
@@ -2274,21 +2293,9 @@ async function setDefaultMultiPortal(ctx, next) {
2274
2293
  ctx.throw(400, ctx.t("Unsupported Portal type cannot be set as default", { ns: import_constants.NAMESPACE }));
2275
2294
  return null;
2276
2295
  }
2277
- if (portalType === "no-code") {
2278
- const uiLayoutUid = target.get("uiLayoutUid");
2279
- const uiLayout = typeof uiLayoutUid === "string" && uiLayoutUid ? await ctx.db.getRepository("uiLayouts").findOne({
2280
- filter: {
2281
- uid: uiLayoutUid,
2282
- enabled: true
2283
- },
2284
- fields: ["uid"],
2285
- lock: transaction.LOCK.UPDATE,
2286
- transaction
2287
- }) : null;
2288
- if (!uiLayout) {
2289
- ctx.throw(400, ctx.t("Portal layout must be enabled before setting it as default", { ns: import_constants.NAMESPACE }));
2290
- return null;
2291
- }
2296
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(target.get("uiLayoutUid"))) {
2297
+ ctx.throw(400, ctx.t("Portal device configuration is invalid", { ns: import_constants.NAMESPACE }));
2298
+ return null;
2292
2299
  }
2293
2300
  await repository.update({
2294
2301
  filter: { isDefault: true },
@@ -2314,10 +2321,11 @@ async function listAccessibleMultiPortals(ctx, next) {
2314
2321
  const records = await ctx.db.getRepository("multiPortals").find({
2315
2322
  filter: {
2316
2323
  enabled: true,
2317
- "uiLayout.enabled": true
2324
+ uiLayoutUid: {
2325
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2326
+ }
2318
2327
  },
2319
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2320
- appends: ["uiLayout"],
2328
+ fields: [...MULTI_PORTAL_ACCESSIBLE_FIELDS],
2321
2329
  sort: ["uid"]
2322
2330
  });
2323
2331
  const accessiblePortalUidSet = Array.isArray(accessiblePortalUids) ? new Set(accessiblePortalUids) : void 0;
@@ -2461,6 +2469,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2461
2469
  getAppName() {
2462
2470
  return this.app.name || MAIN_APP_NAME;
2463
2471
  }
2472
+ getCurrentStorageAppName() {
2473
+ return normalizePortalStorageName(this.getAppName()) || MAIN_APP_NAME;
2474
+ }
2464
2475
  getMultiPortalStorageItem(multiPortal, previous = false) {
2465
2476
  const record = multiPortal;
2466
2477
  const readField = (field) => previous && typeof record.previous === "function" ? record.previous(field) : getRecordField(record, field);
@@ -2475,8 +2486,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
2475
2486
  return {
2476
2487
  appName: this.getAppName(),
2477
2488
  portalName,
2478
- enabled: readField("enabled") === true,
2479
- config: getPortalStorageConfig(readField("options"))
2489
+ enabled: readField("enabled") === true
2480
2490
  };
2481
2491
  }
2482
2492
  warnPortalStorageSyncFailed(error) {
@@ -2549,8 +2559,6 @@ class PluginMultiPortalServer extends import_server.Plugin {
2549
2559
  await copyPortalTemplate(template.dir, portalDir);
2550
2560
  await appendPortalStorageLog(logPath, `Default portal template copied to ${portalDir}.`);
2551
2561
  }
2552
- await ensurePortalStorageConfigFiles(portalDir, item);
2553
- await appendPortalStorageLog(logPath, `Portal configuration files updated in ${portalDir}.`);
2554
2562
  if (item.enabled) {
2555
2563
  this.logPortalBuildHtml(item, "requested", "storage directory was initialized");
2556
2564
  await buildPortalStorageItem(portalDir, item);
@@ -2558,8 +2566,6 @@ class PluginMultiPortalServer extends import_server.Plugin {
2558
2566
  return;
2559
2567
  }
2560
2568
  this.logPortalBuildHtml(item, "skipped", "the portal is disabled");
2561
- await this.removePortalStorageIndexHtml(item);
2562
- await appendPortalStorageLog(logPath, `Portal index.html removed for ${item.appName}/${item.portalName}.`);
2563
2569
  } catch (error) {
2564
2570
  await appendPortalStorageLog(
2565
2571
  logPath,
@@ -2584,7 +2590,6 @@ class PluginMultiPortalServer extends import_server.Plugin {
2584
2590
  await this.schedulePortalTemplateCopyAndBuild(item, template, portalDir);
2585
2591
  return;
2586
2592
  }
2587
- await ensurePortalStorageConfigFiles(portalDir, item);
2588
2593
  if (item.enabled) {
2589
2594
  const hasPortalIndex = await pathExists(portalIndex);
2590
2595
  if (options.forceBuild || !hasPortalIndex) {
@@ -2601,14 +2606,12 @@ class PluginMultiPortalServer extends import_server.Plugin {
2601
2606
  return;
2602
2607
  }
2603
2608
  this.logPortalBuildHtml(item, "skipped", "the portal is disabled");
2604
- await this.removePortalStorageIndexHtml(item);
2605
- await appendPortalStorageLog(logPath, `Portal index.html removed for ${item.appName}/${item.portalName}.`);
2606
2609
  }
2607
2610
  async syncMultiPortalStorageItem(multiPortal, options, syncPrevious = false, forceBuild = false) {
2608
2611
  const currentItem = this.getMultiPortalStorageItem(multiPortal);
2609
2612
  const previousItem = syncPrevious ? this.getMultiPortalStorageItem(multiPortal, true) : null;
2610
2613
  await this.runPortalStorageTask(async () => {
2611
- if (previousItem && (!currentItem || previousItem.appName !== currentItem.appName || previousItem.portalName !== currentItem.portalName || !currentItem.enabled)) {
2614
+ if (previousItem && (!currentItem || previousItem.appName !== currentItem.appName || previousItem.portalName !== currentItem.portalName)) {
2612
2615
  await this.removePortalStorageIndexHtml(previousItem);
2613
2616
  }
2614
2617
  if (currentItem) {
@@ -2655,12 +2658,12 @@ class PluginMultiPortalServer extends import_server.Plugin {
2655
2658
  await next();
2656
2659
  }
2657
2660
  async deployPortalDist(ctx, next) {
2658
- var _a, _b, _c, _d;
2661
+ var _a, _b, _c;
2659
2662
  const deployCtx = ctx;
2660
- const appName = normalizePortalStorageName(((_a = deployCtx.request.body) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
2661
- const portalName = normalizePortalStorageName((_b = deployCtx.request.body) == null ? void 0 : _b.portal);
2662
- const basePath = trimString((_c = deployCtx.request.body) == null ? void 0 : _c.basePath);
2663
- const filePath = trimString((_d = deployCtx.request.file) == null ? void 0 : _d.path);
2663
+ const appName = this.getCurrentStorageAppName();
2664
+ const portalName = normalizePortalStorageName((_a = deployCtx.request.body) == null ? void 0 : _a.portal);
2665
+ const basePath = trimString((_b = deployCtx.request.body) == null ? void 0 : _b.basePath);
2666
+ const filePath = trimString((_c = deployCtx.request.file) == null ? void 0 : _c.path);
2664
2667
  try {
2665
2668
  if (!isValidPortalDeploySegment(appName)) {
2666
2669
  throw new Error("Invalid app");
@@ -2697,9 +2700,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2697
2700
  }
2698
2701
  }
2699
2702
  async pullPortalSource(ctx, next) {
2700
- var _a, _b;
2701
- const appName = normalizePortalStorageName(((_a = ctx.action.params.values) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
2702
- const portalName = normalizePortalStorageName((_b = ctx.action.params.values) == null ? void 0 : _b.portal);
2703
+ var _a;
2704
+ const appName = this.getCurrentStorageAppName();
2705
+ const portalName = normalizePortalStorageName((_a = ctx.action.params.values) == null ? void 0 : _a.portal);
2703
2706
  try {
2704
2707
  if (!isValidPortalDeploySegment(appName)) {
2705
2708
  throw new Error("Invalid app");
@@ -2719,11 +2722,11 @@ class PluginMultiPortalServer extends import_server.Plugin {
2719
2722
  }
2720
2723
  }
2721
2724
  async pushPortalSource(ctx, next) {
2722
- var _a, _b, _c;
2725
+ var _a, _b;
2723
2726
  const sourceCtx = ctx;
2724
- const appName = normalizePortalStorageName(((_a = sourceCtx.request.body) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
2725
- const portalName = normalizePortalStorageName((_b = sourceCtx.request.body) == null ? void 0 : _b.portal);
2726
- const filePath = trimString((_c = sourceCtx.request.file) == null ? void 0 : _c.path);
2727
+ const appName = this.getCurrentStorageAppName();
2728
+ const portalName = normalizePortalStorageName((_a = sourceCtx.request.body) == null ? void 0 : _a.portal);
2729
+ const filePath = trimString((_b = sourceCtx.request.file) == null ? void 0 : _b.path);
2727
2730
  try {
2728
2731
  if (!isValidPortalDeploySegment(appName)) {
2729
2732
  throw new Error("Invalid app");
@@ -2777,38 +2780,20 @@ class PluginMultiPortalServer extends import_server.Plugin {
2777
2780
  const records = await this.db.getRepository("multiPortals").find({
2778
2781
  filter: {
2779
2782
  enabled: true,
2780
- "uiLayout.enabled": true
2783
+ uiLayoutUid: {
2784
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2785
+ }
2781
2786
  },
2782
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2783
- appends: ["uiLayout"],
2787
+ fields: [...MULTI_PORTAL_ACCESSIBLE_FIELDS],
2784
2788
  sort: ["uid"],
2785
2789
  transaction: options == null ? void 0 : options.transaction
2786
2790
  });
2787
- return records.map((record) => {
2788
- const portal = pickMultiPortalAccessibleFields(record);
2789
- const uiLayout = portal.uiLayout;
2790
- const uid = typeof portal.uid === "string" ? portal.uid : "";
2791
- const title = typeof portal.title === "string" ? portal.title : "";
2792
- return {
2793
- uid,
2794
- title,
2795
- icon: typeof portal.icon === "string" ? portal.icon : null,
2796
- portalType: typeof portal.portalType === "string" ? portal.portalType : null,
2797
- routePath: String(portal.routePath || ""),
2798
- layout: typeof (uiLayout == null ? void 0 : uiLayout.layoutType) === "string" ? uiLayout.layoutType : null
2799
- };
2800
- });
2801
- }
2802
- async getUiLayout(uid, options) {
2803
- if (typeof uid !== "string" || !uid) {
2804
- return null;
2805
- }
2806
- return this.db.getRepository("uiLayouts").findOne({
2807
- filterByTk: uid,
2808
- transaction: options == null ? void 0 : options.transaction
2791
+ return records.flatMap((record) => {
2792
+ const item = this.toAppPortalManifestItem(record);
2793
+ return item ? [item] : [];
2809
2794
  });
2810
2795
  }
2811
- async toAppPortalManifestItem(multiPortal, options) {
2796
+ toAppPortalManifestItem(multiPortal) {
2812
2797
  const uid = getRecordField(multiPortal, "uid");
2813
2798
  const title = getRecordField(multiPortal, "title");
2814
2799
  const portalType = getRecordField(multiPortal, "portalType");
@@ -2818,19 +2803,18 @@ class PluginMultiPortalServer extends import_server.Plugin {
2818
2803
  return null;
2819
2804
  }
2820
2805
  const uiLayoutUid = getRecordField(multiPortal, "uiLayoutUid");
2821
- const uiLayout = getRecordField(multiPortal, "uiLayout") || await this.getUiLayout(uiLayoutUid, options);
2822
- if (!uiLayout || getRecordField(uiLayout, "enabled") !== true) {
2806
+ const layout = (0, import_constants.getMultiPortalLayoutType)(uiLayoutUid);
2807
+ if (!layout) {
2823
2808
  return null;
2824
2809
  }
2825
2810
  const icon = getRecordField(multiPortal, "icon");
2826
- const layoutType = getRecordField(uiLayout, "layoutType");
2827
2811
  return {
2828
2812
  uid,
2829
2813
  title,
2830
2814
  icon: typeof icon === "string" ? icon : null,
2831
2815
  portalType: typeof portalType === "string" ? portalType : null,
2832
2816
  routePath,
2833
- layout: typeof layoutType === "string" ? layoutType : null
2817
+ layout
2834
2818
  };
2835
2819
  }
2836
2820
  async setAppManifestItem(item, options) {
@@ -2873,30 +2857,13 @@ class PluginMultiPortalServer extends import_server.Plugin {
2873
2857
  if (typeof uid !== "string" || !uid) {
2874
2858
  return;
2875
2859
  }
2876
- const item = await this.toAppPortalManifestItem(multiPortal, options);
2860
+ const item = this.toAppPortalManifestItem(multiPortal);
2877
2861
  if (item) {
2878
2862
  await this.setAppManifestItem(item, options);
2879
2863
  return;
2880
2864
  }
2881
2865
  await this.removeAppManifestItem(uid, options);
2882
2866
  }
2883
- async publishUiLayoutManifestItems(uiLayout, options) {
2884
- const uiLayoutUid = getRecordField(uiLayout, "uid");
2885
- if (typeof uiLayoutUid !== "string" || !uiLayoutUid) {
2886
- return;
2887
- }
2888
- const records = await this.db.getRepository("multiPortals").find({
2889
- filter: {
2890
- uiLayoutUid
2891
- },
2892
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2893
- transaction: options == null ? void 0 : options.transaction
2894
- });
2895
- for (const record of records) {
2896
- record.set("uiLayout", uiLayout);
2897
- await this.publishAppManifestItem(record, options);
2898
- }
2899
- }
2900
2867
  async reconcilePortalStorage(options) {
2901
2868
  const records = await this.db.getRepository("multiPortals").find({
2902
2869
  filter: {
@@ -2960,15 +2927,16 @@ class PluginMultiPortalServer extends import_server.Plugin {
2960
2927
  mapMultiPortalLayoutToUiLayoutForRolePermissionTargets
2961
2928
  );
2962
2929
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", preventDirectDefaultPortalMutation);
2930
+ this.app.resourceManager.registerPreActionHandler("multiPortals:create", validateMultiPortalUiLayoutUidWrite);
2963
2931
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", captureSkipCreatePortalDirectory);
2964
2932
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", normalizeMultiPortalSlugValues);
2965
2933
  this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventDirectDefaultPortalMutation);
2966
- this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventMultiPortalBackingLayoutChange);
2934
+ this.app.resourceManager.registerPreActionHandler("multiPortals:update", validateMultiPortalUiLayoutUidWrite);
2967
2935
  this.app.resourceManager.registerPreActionHandler("multiPortals:update", normalizeMultiPortalSlugValues);
2968
2936
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", preventDirectDefaultPortalMutation);
2969
2937
  this.app.resourceManager.registerPreActionHandler(
2970
2938
  "multiPortals:firstOrCreate",
2971
- preventMultiPortalBackingLayoutChange
2939
+ validateMultiPortalUiLayoutUidWrite
2972
2940
  );
2973
2941
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", captureSkipCreatePortalDirectory);
2974
2942
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", normalizeMultiPortalSlugValues);
@@ -2978,7 +2946,7 @@ class PluginMultiPortalServer extends import_server.Plugin {
2978
2946
  );
2979
2947
  this.app.resourceManager.registerPreActionHandler(
2980
2948
  "multiPortals:updateOrCreate",
2981
- preventMultiPortalBackingLayoutChange
2949
+ validateMultiPortalUiLayoutUidWrite
2982
2950
  );
2983
2951
  this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", captureSkipCreatePortalDirectory);
2984
2952
  this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", normalizeMultiPortalSlugValues);
@@ -3063,9 +3031,6 @@ class PluginMultiPortalServer extends import_server.Plugin {
3063
3031
  }
3064
3032
  await this.removeMultiPortalStorageItem(multiPortal, options);
3065
3033
  });
3066
- this.app.db.on("uiLayouts.afterUpdate", async (uiLayout, options) => {
3067
- await this.publishUiLayoutManifestItems(uiLayout, options);
3068
- });
3069
3034
  }
3070
3035
  async install() {
3071
3036
  const version = await this.app.version.get();