@nocobase/plugin-multi-portal 3.0.0-alpha.3 → 3.0.0-alpha.5

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";
@@ -100,6 +99,7 @@ const MULTI_PORTAL_MANAGEMENT_ACTIONS = [
100
99
  "multiPortals:create",
101
100
  "multiPortals:update",
102
101
  "multiPortals:firstOrCreate",
102
+ "multiPortals:setDefault",
103
103
  "multiPortals:destroy",
104
104
  "multiPortals:deploy",
105
105
  "multiPortals:pullSource",
@@ -168,7 +168,7 @@ function formatInitPortalTitle(portalName) {
168
168
  const title = portalName.split(/[-_]/).filter(Boolean).map((segment) => `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`).join(" ");
169
169
  return title || portalName;
170
170
  }
171
- function getDefaultMultiPortalRecord() {
171
+ function getDefaultMultiPortalRecord(options = {}) {
172
172
  const portalName = getInitPortalName();
173
173
  return {
174
174
  uid: DEFAULT_MULTI_PORTAL_UID,
@@ -179,11 +179,12 @@ function getDefaultMultiPortalRecord() {
179
179
  routePath: `/${portalName}`,
180
180
  authCheck: true,
181
181
  enabled: true,
182
- uiLayoutUid: "admin-layout-model"
182
+ ...options.isDefault ? { isDefault: true } : {},
183
+ uiLayoutUid: import_constants.ADMIN_UI_LAYOUT_UID
183
184
  };
184
185
  }
185
186
  function getFreshMultiPortalRecords() {
186
- return [getDefaultMultiPortalRecord()];
187
+ return [getDefaultMultiPortalRecord({ isDefault: true })];
187
188
  }
188
189
  function getFixedLayoutMultiPortalRecords() {
189
190
  return [
@@ -196,7 +197,7 @@ function getFixedLayoutMultiPortalRecords() {
196
197
  routePath: "/admin",
197
198
  authCheck: true,
198
199
  enabled: true,
199
- uiLayoutUid: "admin-layout-model"
200
+ uiLayoutUid: import_constants.ADMIN_UI_LAYOUT_UID
200
201
  },
201
202
  {
202
203
  uid: import_constants.DEFAULT_MOBILE_MULTI_PORTAL_UID,
@@ -207,7 +208,7 @@ function getFixedLayoutMultiPortalRecords() {
207
208
  routePath: "/mobile",
208
209
  authCheck: true,
209
210
  enabled: true,
210
- uiLayoutUid: "mobile-layout-model"
211
+ uiLayoutUid: import_constants.MOBILE_UI_LAYOUT_UID
211
212
  }
212
213
  ];
213
214
  }
@@ -292,6 +293,13 @@ function getPortalDeployBasePath(appName, portalName) {
292
293
  const portalPath = appName === MAIN_APP_NAME ? `/${PORTAL_CLIENT_PREFIX}/${portalName}/` : `/${PORTAL_CLIENT_PREFIX}/apps/${appName}/${portalName}/`;
293
294
  return resolvePortalStoragePublicPath(joinPortalStoragePublicPath(process.env.APP_PUBLIC_PATH || "/", portalPath));
294
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
+ }
295
303
  function createPortalDeployUploadMiddleware() {
296
304
  const storage = import_utils.koaMulter.diskStorage({
297
305
  destination: import_os.default.tmpdir(),
@@ -618,9 +626,9 @@ function validatePortalDeployBasePath(appName, portalName, basePath) {
618
626
  if (normalizedBasePath.includes("..")) {
619
627
  throw new Error('basePath cannot contain ".."');
620
628
  }
621
- const expectedBasePath = getPortalDeployBasePath(appName, portalName);
622
- if (normalizedBasePath !== expectedBasePath) {
623
- throw new Error(`basePath must be ${expectedBasePath}`);
629
+ const expectedBasePaths = getPortalDeployBasePathCandidates(appName, portalName);
630
+ if (!expectedBasePaths.includes(normalizedBasePath)) {
631
+ throw new Error(`basePath must be ${expectedBasePaths.join(" or ")}`);
624
632
  }
625
633
  }
626
634
  function isPortalDeployTarEntry(entry) {
@@ -858,19 +866,11 @@ function collectDesktopRouteIds(record) {
858
866
  }
859
867
  return uniqueDesktopRouteIds(routeIds);
860
868
  }
861
- function pickMultiPortalUiLayoutRuntimeFields(record) {
862
- const result = {};
863
- for (const field of MULTI_PORTAL_UI_LAYOUT_RUNTIME_FIELDS) {
864
- result[field] = getRecordField(record, field);
865
- }
866
- return result;
867
- }
868
869
  function pickMultiPortalRuntimeFields(record) {
869
870
  const result = {};
870
871
  for (const field of MULTI_PORTAL_RUNTIME_FIELDS) {
871
872
  result[field] = getRecordField(record, field);
872
873
  }
873
- result.uiLayout = pickMultiPortalUiLayoutRuntimeFields(getRecordField(record, "uiLayout"));
874
874
  return result;
875
875
  }
876
876
  function pickMultiPortalAccessibleFields(record) {
@@ -878,7 +878,6 @@ function pickMultiPortalAccessibleFields(record) {
878
878
  for (const field of MULTI_PORTAL_ACCESSIBLE_FIELDS) {
879
879
  result[field] = getRecordField(record, field) ?? null;
880
880
  }
881
- result.uiLayout = pickMultiPortalUiLayoutRuntimeFields(getRecordField(record, "uiLayout"));
882
881
  return result;
883
882
  }
884
883
  function getExplicitRequestedLayoutUid(layout) {
@@ -1035,6 +1034,37 @@ async function normalizeMultiPortalSlugValues(ctx, next) {
1035
1034
  values.portalName = slug;
1036
1035
  values.routePath = `/${slug}`;
1037
1036
  }
1037
+ const repository = ctx.db.getRepository("multiPortals");
1038
+ const pendingPortalNames = /* @__PURE__ */ new Set();
1039
+ const targets = await getMultiPortalWriteTargets(ctx, ["uid"]);
1040
+ for (const { existing, values } of targets) {
1041
+ const portalName = values.portalName;
1042
+ if (typeof portalName !== "string" || !portalName) {
1043
+ continue;
1044
+ }
1045
+ if (pendingPortalNames.has(portalName)) {
1046
+ ctx.throw(409, ctx.t('Portal name "{{portalName}}" already exists', { ns: import_constants.NAMESPACE, portalName }));
1047
+ return;
1048
+ }
1049
+ pendingPortalNames.add(portalName);
1050
+ const conflict = await repository.findOne({
1051
+ filter: { portalName },
1052
+ fields: ["uid"]
1053
+ });
1054
+ if (conflict && conflict.get("uid") !== (existing == null ? void 0 : existing.get("uid"))) {
1055
+ ctx.throw(409, ctx.t('Portal name "{{portalName}}" already exists', { ns: import_constants.NAMESPACE, portalName }));
1056
+ return;
1057
+ }
1058
+ }
1059
+ await next();
1060
+ }
1061
+ async function preventDirectDefaultPortalMutation(ctx, next) {
1062
+ for (const values of getMultiPortalWriteRecords(ctx)) {
1063
+ if (Object.prototype.hasOwnProperty.call(values, "isDefault")) {
1064
+ ctx.throw(400, ctx.t("Use multiPortals:setDefault to update the default Portal", { ns: import_constants.NAMESPACE }));
1065
+ return;
1066
+ }
1067
+ }
1038
1068
  await next();
1039
1069
  }
1040
1070
  function isUniqueConstraintViolation(error) {
@@ -1097,14 +1127,27 @@ async function seedHistoricalMultiPortals(db) {
1097
1127
  }
1098
1128
  await repairFixedLayoutMultiPortalRecords(db);
1099
1129
  }
1100
- async function preventMultiPortalBackingLayoutChange(ctx, next) {
1130
+ async function validateMultiPortalUiLayoutUidWrite(ctx, next) {
1131
+ var _a;
1101
1132
  const targets = await getMultiPortalWriteTargets(ctx, ["uiLayoutUid"]);
1133
+ const actionName = (_a = ctx.action) == null ? void 0 : _a.actionName;
1134
+ const createsWhenMissing = actionName === "create" || actionName === "firstOrCreate" || actionName === "updateOrCreate";
1102
1135
  for (const { existing, values } of targets) {
1103
- if (!Object.prototype.hasOwnProperty.call(values, "uiLayoutUid")) {
1136
+ const hasUiLayoutUid = Object.prototype.hasOwnProperty.call(values, "uiLayoutUid");
1137
+ if (!existing && createsWhenMissing && !hasUiLayoutUid) {
1138
+ ctx.throw(400, `Portal UI layout must be one of: ${import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS.join(", ")}`);
1139
+ return;
1140
+ }
1141
+ if (!hasUiLayoutUid) {
1104
1142
  continue;
1105
1143
  }
1106
- if (existing && existing.get("uiLayoutUid") !== values.uiLayoutUid) {
1107
- ctx.throw(400, "Portal backing UI layout cannot be changed");
1144
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(values.uiLayoutUid)) {
1145
+ ctx.throw(400, `Portal UI layout must be one of: ${import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS.join(", ")}`);
1146
+ return;
1147
+ }
1148
+ const existingUiLayoutUid = existing == null ? void 0 : existing.get("uiLayoutUid");
1149
+ if (existing && existingUiLayoutUid !== values.uiLayoutUid) {
1150
+ ctx.throw(400, "Portal UI layout cannot be changed");
1108
1151
  return;
1109
1152
  }
1110
1153
  }
@@ -1132,20 +1175,8 @@ async function findRequestedMultiPortal(ctx, transaction) {
1132
1175
  ctx.throw(400, `Portal '${portalUid}' does not support desktop routes`);
1133
1176
  }
1134
1177
  const uiLayoutUid = portal.get("uiLayoutUid");
1135
- if (typeof uiLayoutUid !== "string" || !uiLayoutUid) {
1136
- ctx.throw(400, `Portal '${portalUid}' has no backing UI layout`);
1137
- }
1138
- const uiLayout = await ctx.db.getRepository("uiLayouts").findOne({
1139
- filter: {
1140
- uid: uiLayoutUid,
1141
- enabled: true
1142
- },
1143
- fields: ["uid"],
1144
- ...transaction ? { lock: transaction.LOCK.UPDATE } : {},
1145
- transaction
1146
- });
1147
- if (!uiLayout) {
1148
- ctx.throw(400, `Portal '${portalUid}' has no enabled backing UI layout`);
1178
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(uiLayoutUid)) {
1179
+ ctx.throw(400, `Portal '${portalUid}' has an unsupported UI layout UID`);
1149
1180
  }
1150
1181
  const usesLayoutPermissions = (0, import_constants.isDefaultLayoutMultiPortalUid)(portalUid);
1151
1182
  return {
@@ -1330,7 +1361,7 @@ async function removeRouteIdsWithUnauthorizedAncestors(ctx, routeIds) {
1330
1361
  }
1331
1362
  }
1332
1363
  }
1333
- async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1364
+ async function getMultiPortalAccessibleRouteIds(ctx, portalContext) {
1334
1365
  const currentRoles = getCurrentRoles(ctx);
1335
1366
  if (currentRoles.includes("root")) {
1336
1367
  return;
@@ -1338,11 +1369,11 @@ async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1338
1369
  if (!currentRoles.length) {
1339
1370
  return /* @__PURE__ */ new Set();
1340
1371
  }
1341
- const routePermissions = await ctx.db.getRepository("rolesMultiPortalDesktopRoutes").find({
1372
+ const routePermissions = await ctx.db.getRepository(portalContext.relation === "uiLayouts" ? "rolesDesktopRoutes" : "rolesMultiPortalDesktopRoutes").find({
1342
1373
  fields: ["desktopRouteId"],
1343
1374
  filter: {
1344
1375
  roleName: currentRoles,
1345
- multiPortalUid
1376
+ ...portalContext.relation === "multiPortals" ? { multiPortalUid: portalContext.portalUid } : {}
1346
1377
  }
1347
1378
  });
1348
1379
  const routeIds = /* @__PURE__ */ new Set();
@@ -1358,7 +1389,7 @@ async function getMultiPortalAccessibleRouteIds(ctx, multiPortalUid) {
1358
1389
  const portalRoutes = await ctx.db.getRepository("desktopRoutes").find({
1359
1390
  fields: ["id"],
1360
1391
  filter: {
1361
- ...getDesktopRoutePortalFilter(multiPortalUid),
1392
+ ...portalContext.filter,
1362
1393
  id: Array.from(routeIds)
1363
1394
  }
1364
1395
  });
@@ -1388,9 +1419,107 @@ function setDesktopRouteChildren(route, children) {
1388
1419
  const maybeModel = route;
1389
1420
  if (typeof maybeModel.setDataValue === "function") {
1390
1421
  maybeModel.setDataValue("children", children);
1422
+ } else {
1423
+ maybeModel.children = children;
1424
+ }
1425
+ if (!maybeModel._options) {
1426
+ return;
1427
+ }
1428
+ if (!maybeModel._options.includeNames) {
1429
+ maybeModel._options.includeNames = ["children"];
1391
1430
  return;
1392
1431
  }
1393
- route.children = children;
1432
+ if (!maybeModel._options.includeNames.includes("children")) {
1433
+ maybeModel._options.includeNames.push("children");
1434
+ }
1435
+ }
1436
+ function getDesktopRouteId(route) {
1437
+ const id = getRecordField(route, "id");
1438
+ return id === null || id === void 0 ? void 0 : String(id);
1439
+ }
1440
+ function getDesktopRouteParentId(route) {
1441
+ const parentId = getRecordField(route, "parentId");
1442
+ return parentId === null || parentId === void 0 ? void 0 : String(parentId);
1443
+ }
1444
+ function collectDesktopRouteStringIds(routes, routeIds) {
1445
+ for (const route of routes) {
1446
+ const routeId = getDesktopRouteId(route);
1447
+ if (routeId) {
1448
+ routeIds.add(routeId);
1449
+ }
1450
+ const children = getRecordField(route, "children");
1451
+ if (Array.isArray(children)) {
1452
+ collectDesktopRouteStringIds(children, routeIds);
1453
+ }
1454
+ }
1455
+ }
1456
+ function removeNestedRootDesktopRoutes(routes) {
1457
+ if (!Array.isArray(routes)) {
1458
+ return [];
1459
+ }
1460
+ const routeIds = /* @__PURE__ */ new Set();
1461
+ collectDesktopRouteStringIds(routes, routeIds);
1462
+ return routes.filter((route) => {
1463
+ const parentId = getDesktopRouteParentId(route);
1464
+ return !parentId || !routeIds.has(parentId);
1465
+ });
1466
+ }
1467
+ function buildAccessibleDesktopRouteTreeWithAncestors(routes, accessibleRouteIds) {
1468
+ const routeById = /* @__PURE__ */ new Map();
1469
+ const childrenByParentId = /* @__PURE__ */ new Map();
1470
+ const roots = [];
1471
+ for (const route of routes) {
1472
+ const routeId = getDesktopRouteId(route);
1473
+ if (!routeId) {
1474
+ continue;
1475
+ }
1476
+ setDesktopRouteChildren(route, void 0);
1477
+ routeById.set(routeId, route);
1478
+ }
1479
+ for (const route of routes) {
1480
+ const routeId = getDesktopRouteId(route);
1481
+ if (!routeId || !routeById.has(routeId)) {
1482
+ continue;
1483
+ }
1484
+ const parentId = getDesktopRouteParentId(route);
1485
+ if (!parentId || !routeById.has(parentId)) {
1486
+ roots.push(route);
1487
+ continue;
1488
+ }
1489
+ const children = childrenByParentId.get(parentId) ?? [];
1490
+ children.push(route);
1491
+ childrenByParentId.set(parentId, children);
1492
+ }
1493
+ const visitRoute = (route, visitingRouteIds) => {
1494
+ const routeId = getDesktopRouteId(route);
1495
+ if (!routeId || visitingRouteIds.has(routeId)) {
1496
+ return void 0;
1497
+ }
1498
+ visitingRouteIds.add(routeId);
1499
+ const visibleChildren = (childrenByParentId.get(routeId) ?? []).map((child) => visitRoute(child, visitingRouteIds)).filter((child) => child !== void 0);
1500
+ visitingRouteIds.delete(routeId);
1501
+ if (!accessibleRouteIds.has(routeId) && visibleChildren.length === 0) {
1502
+ return void 0;
1503
+ }
1504
+ setDesktopRouteChildren(route, visibleChildren.length ? visibleChildren : void 0);
1505
+ return route;
1506
+ };
1507
+ return roots.map((route) => visitRoute(route, /* @__PURE__ */ new Set())).filter((route) => route !== void 0);
1508
+ }
1509
+ async function includeDesktopRouteAncestorsForListAccessible(ctx, routes, portalFilter) {
1510
+ if (!Array.isArray(routes)) {
1511
+ return routes;
1512
+ }
1513
+ const accessibleRouteIds = /* @__PURE__ */ new Set();
1514
+ collectDesktopRouteStringIds(routes, accessibleRouteIds);
1515
+ if (!accessibleRouteIds.size) {
1516
+ return routes;
1517
+ }
1518
+ const portalRoutes = await ctx.db.getRepository("desktopRoutes").find({
1519
+ sort: "sort",
1520
+ filter: portalFilter
1521
+ });
1522
+ return buildAccessibleDesktopRouteTreeWithAncestors(portalRoutes, accessibleRouteIds);
1394
1523
  }
1395
1524
  function removeDesktopRoutesByIds(routes, routeIds) {
1396
1525
  return routes.map((route) => {
@@ -1457,23 +1586,26 @@ async function removeMultiPortalOwnedRouteFromGetResponse(ctx) {
1457
1586
  ctx.body = void 0;
1458
1587
  }
1459
1588
  async function replaceListAccessibleRoutesWithPortalScopedRoutes(ctx, portalContext) {
1460
- const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext.portalUid);
1589
+ const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext);
1461
1590
  if (routeIds && routeIds.size === 0) {
1462
1591
  ctx.body = [];
1463
1592
  return;
1464
1593
  }
1465
- ctx.body = await ctx.db.getRepository("desktopRoutes").find({
1594
+ const routes = await ctx.db.getRepository("desktopRoutes").find({
1466
1595
  tree: true,
1467
1596
  sort: "sort",
1468
1597
  filter: {
1469
- ...getDesktopRoutePortalFilter(portalContext.portalUid),
1598
+ ...portalContext.filter,
1470
1599
  ...routeIds ? { id: Array.from(routeIds) } : {}
1471
1600
  }
1472
1601
  });
1602
+ ctx.body = removeNestedRootDesktopRoutes(
1603
+ await includeDesktopRouteAncestorsForListAccessible(ctx, routes, portalContext.filter)
1604
+ );
1473
1605
  }
1474
1606
  async function replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext) {
1475
1607
  var _a;
1476
- const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext.portalUid);
1608
+ const routeIds = await getMultiPortalAccessibleRouteIds(ctx, portalContext);
1477
1609
  if (routeIds && routeIds.size === 0) {
1478
1610
  ctx.status = 204;
1479
1611
  ctx.body = void 0;
@@ -1483,7 +1615,7 @@ async function replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext
1483
1615
  sort: "sort",
1484
1616
  filterByTk: (_a = ctx.action) == null ? void 0 : _a.params.filterByTk,
1485
1617
  filter: {
1486
- ...getDesktopRoutePortalFilter(portalContext.portalUid),
1618
+ ...portalContext.filter,
1487
1619
  ...routeIds ? { id: Array.from(routeIds) } : {}
1488
1620
  }
1489
1621
  });
@@ -1520,13 +1652,10 @@ async function addMultiPortalListAccessibleGuard(ctx, next) {
1520
1652
  return;
1521
1653
  }
1522
1654
  await next();
1523
- if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1655
+ if (portalContext) {
1524
1656
  await replaceListAccessibleRoutesWithPortalScopedRoutes(ctx, portalContext);
1525
1657
  return;
1526
1658
  }
1527
- if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1528
- return;
1529
- }
1530
1659
  await removeMultiPortalOwnedRoutesFromListResponse(ctx);
1531
1660
  }
1532
1661
  async function addMultiPortalGetAccessibleGuard(ctx, next) {
@@ -1537,17 +1666,13 @@ async function addMultiPortalGetAccessibleGuard(ctx, next) {
1537
1666
  return;
1538
1667
  }
1539
1668
  await next();
1540
- if ((portalContext == null ? void 0 : portalContext.relation) === "multiPortals") {
1669
+ if (portalContext) {
1541
1670
  await replaceGetAccessibleRouteWithPortalScopedRoute(ctx, portalContext);
1542
1671
  return;
1543
1672
  }
1544
- if ((portalContext == null ? void 0 : portalContext.relation) === "uiLayouts") {
1545
- return;
1546
- }
1547
1673
  await removeMultiPortalOwnedRouteFromGetResponse(ctx);
1548
1674
  }
1549
1675
  async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next) {
1550
- var _a;
1551
1676
  const portalRequest = await findRequestedMultiPortal(ctx);
1552
1677
  if (portalRequest.requested && !portalRequest.portal) {
1553
1678
  ctx.status = 200;
@@ -1555,22 +1680,19 @@ async function mapMultiPortalLayoutToUiLayoutForRolePermissionTargets(ctx, next)
1555
1680
  return;
1556
1681
  }
1557
1682
  const scope = portalRequest.scope;
1558
- if ((scope == null ? void 0 : scope.relation) === "uiLayouts") {
1559
- if ((_a = ctx.action) == null ? void 0 : _a.params) {
1560
- ctx.action.params.layout = scope.uiLayoutUid;
1561
- }
1562
- await next();
1563
- return;
1564
- }
1565
- if ((scope == null ? void 0 : scope.relation) === "multiPortals") {
1683
+ if (scope) {
1566
1684
  const routes = await ctx.db.getRepository("desktopRoutes").find({
1567
- tree: true,
1568
1685
  sort: "sort",
1569
1686
  filter: scope.filter,
1570
1687
  fields: [...DESKTOP_ROUTE_ROLE_PERMISSION_TARGET_FIELDS]
1571
1688
  });
1689
+ const routeIds = /* @__PURE__ */ new Set();
1690
+ collectDesktopRouteStringIds(routes, routeIds);
1691
+ const routeTree = buildAccessibleDesktopRouteTreeWithAncestors(routes, routeIds);
1572
1692
  ctx.status = 200;
1573
- ctx.body = routes.map((route) => pickDesktopRouteRolePermissionTargetFields(route));
1693
+ ctx.body = removeNestedRootDesktopRoutes(routeTree).map(
1694
+ (route) => pickDesktopRouteRolePermissionTargetFields(route)
1695
+ );
1574
1696
  return;
1575
1697
  }
1576
1698
  await next();
@@ -2158,24 +2280,119 @@ async function listEnabledMultiPortals(ctx, next) {
2158
2280
  const records = await ctx.db.getRepository("multiPortals").find({
2159
2281
  filter: {
2160
2282
  enabled: true,
2161
- "uiLayout.enabled": true
2283
+ uiLayoutUid: {
2284
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2285
+ }
2162
2286
  },
2163
- fields: [...MULTI_PORTAL_RUNTIME_QUERY_FIELDS],
2164
- appends: ["uiLayout"],
2287
+ fields: [...MULTI_PORTAL_RUNTIME_FIELDS],
2165
2288
  sort: ["uid"]
2166
2289
  });
2167
2290
  ctx.body = records.map((record) => pickMultiPortalRuntimeFields(record));
2168
2291
  await next();
2169
2292
  }
2293
+ const DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS = ["uid", "portalType", "routePath"];
2294
+ function getDefaultMultiPortalType(record) {
2295
+ const portalType = record.get("portalType");
2296
+ if (portalType === null || portalType === void 0) {
2297
+ return "no-code";
2298
+ }
2299
+ return typeof portalType === "string" && isInitPortalType(portalType) ? portalType : null;
2300
+ }
2301
+ function pickDefaultMultiPortalFields(record) {
2302
+ return {
2303
+ uid: String(record.get("uid")),
2304
+ portalType: getDefaultMultiPortalType(record) || "no-code",
2305
+ routePath: String(record.get("routePath"))
2306
+ };
2307
+ }
2308
+ async function findEnabledDefaultMultiPortal(ctx, transaction) {
2309
+ const record = await ctx.db.getRepository("multiPortals").findOne({
2310
+ filter: {
2311
+ enabled: true,
2312
+ isDefault: true
2313
+ },
2314
+ fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS, "uiLayoutUid"],
2315
+ transaction
2316
+ });
2317
+ if (!record) {
2318
+ return null;
2319
+ }
2320
+ const portalType = getDefaultMultiPortalType(record);
2321
+ if (!portalType) {
2322
+ return null;
2323
+ }
2324
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(record.get("uiLayoutUid"))) {
2325
+ return null;
2326
+ }
2327
+ return record;
2328
+ }
2329
+ async function getDefaultMultiPortal(ctx, next) {
2330
+ const record = await findEnabledDefaultMultiPortal(ctx);
2331
+ ctx.body = record ? pickDefaultMultiPortalFields(record) : null;
2332
+ ctx.status = 200;
2333
+ await next();
2334
+ }
2335
+ async function setDefaultMultiPortal(ctx, next) {
2336
+ var _a;
2337
+ const filterByTk = (_a = ctx.action) == null ? void 0 : _a.params.filterByTk;
2338
+ if (filterByTk === void 0 || filterByTk === null || filterByTk === "") {
2339
+ ctx.throw(400, ctx.t("filterByTk is required", { ns: import_constants.NAMESPACE }));
2340
+ return;
2341
+ }
2342
+ const repository = ctx.db.getRepository("multiPortals");
2343
+ const updated = await ctx.db.sequelize.transaction(async (transaction) => {
2344
+ const target = await repository.findOne({
2345
+ filterByTk,
2346
+ fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS, "enabled", "uiLayoutUid"],
2347
+ lock: transaction.LOCK.UPDATE,
2348
+ transaction
2349
+ });
2350
+ if (!target) {
2351
+ ctx.throw(404, ctx.t("Portal not found", { ns: import_constants.NAMESPACE }));
2352
+ return null;
2353
+ }
2354
+ if (target.get("enabled") !== true) {
2355
+ ctx.throw(400, ctx.t("Disabled Portal cannot be set as default", { ns: import_constants.NAMESPACE }));
2356
+ return null;
2357
+ }
2358
+ const portalType = getDefaultMultiPortalType(target);
2359
+ if (!portalType) {
2360
+ ctx.throw(400, ctx.t("Unsupported Portal type cannot be set as default", { ns: import_constants.NAMESPACE }));
2361
+ return null;
2362
+ }
2363
+ if (!(0, import_constants.isMultiPortalUiLayoutUid)(target.get("uiLayoutUid"))) {
2364
+ ctx.throw(400, ctx.t("Portal device configuration is invalid", { ns: import_constants.NAMESPACE }));
2365
+ return null;
2366
+ }
2367
+ await repository.update({
2368
+ filter: { isDefault: true },
2369
+ values: { isDefault: null },
2370
+ transaction
2371
+ });
2372
+ await repository.update({
2373
+ filterByTk,
2374
+ values: { isDefault: true },
2375
+ transaction
2376
+ });
2377
+ return repository.findOne({
2378
+ filterByTk,
2379
+ fields: [...DEFAULT_MULTI_PORTAL_RESPONSE_FIELDS],
2380
+ transaction
2381
+ });
2382
+ });
2383
+ ctx.body = updated ? pickDefaultMultiPortalFields(updated) : null;
2384
+ await next();
2385
+ }
2170
2386
  async function listAccessibleMultiPortals(ctx, next) {
2171
2387
  const accessiblePortalUids = await listCurrentRoleAccessibleMultiPortalUids(ctx);
2172
2388
  const records = await ctx.db.getRepository("multiPortals").find({
2173
2389
  filter: {
2174
2390
  enabled: true,
2175
- "uiLayout.enabled": true
2391
+ uiLayoutUid: {
2392
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2393
+ }
2176
2394
  },
2177
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2178
- appends: ["uiLayout"],
2395
+ fields: [...MULTI_PORTAL_ACCESSIBLE_FIELDS],
2179
2396
  sort: ["uid"]
2180
2397
  });
2181
2398
  const accessiblePortalUidSet = Array.isArray(accessiblePortalUids) ? new Set(accessiblePortalUids) : void 0;
@@ -2319,6 +2536,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2319
2536
  getAppName() {
2320
2537
  return this.app.name || MAIN_APP_NAME;
2321
2538
  }
2539
+ getCurrentStorageAppName() {
2540
+ return normalizePortalStorageName(this.getAppName()) || MAIN_APP_NAME;
2541
+ }
2322
2542
  getMultiPortalStorageItem(multiPortal, previous = false) {
2323
2543
  const record = multiPortal;
2324
2544
  const readField = (field) => previous && typeof record.previous === "function" ? record.previous(field) : getRecordField(record, field);
@@ -2416,8 +2636,6 @@ class PluginMultiPortalServer extends import_server.Plugin {
2416
2636
  return;
2417
2637
  }
2418
2638
  this.logPortalBuildHtml(item, "skipped", "the portal is disabled");
2419
- await this.removePortalStorageIndexHtml(item);
2420
- await appendPortalStorageLog(logPath, `Portal index.html removed for ${item.appName}/${item.portalName}.`);
2421
2639
  } catch (error) {
2422
2640
  await appendPortalStorageLog(
2423
2641
  logPath,
@@ -2459,14 +2677,12 @@ class PluginMultiPortalServer extends import_server.Plugin {
2459
2677
  return;
2460
2678
  }
2461
2679
  this.logPortalBuildHtml(item, "skipped", "the portal is disabled");
2462
- await this.removePortalStorageIndexHtml(item);
2463
- await appendPortalStorageLog(logPath, `Portal index.html removed for ${item.appName}/${item.portalName}.`);
2464
2680
  }
2465
2681
  async syncMultiPortalStorageItem(multiPortal, options, syncPrevious = false, forceBuild = false) {
2466
2682
  const currentItem = this.getMultiPortalStorageItem(multiPortal);
2467
2683
  const previousItem = syncPrevious ? this.getMultiPortalStorageItem(multiPortal, true) : null;
2468
2684
  await this.runPortalStorageTask(async () => {
2469
- if (previousItem && (!currentItem || previousItem.appName !== currentItem.appName || previousItem.portalName !== currentItem.portalName || !currentItem.enabled)) {
2685
+ if (previousItem && (!currentItem || previousItem.appName !== currentItem.appName || previousItem.portalName !== currentItem.portalName)) {
2470
2686
  await this.removePortalStorageIndexHtml(previousItem);
2471
2687
  }
2472
2688
  if (currentItem) {
@@ -2513,12 +2729,12 @@ class PluginMultiPortalServer extends import_server.Plugin {
2513
2729
  await next();
2514
2730
  }
2515
2731
  async deployPortalDist(ctx, next) {
2516
- var _a, _b, _c, _d;
2732
+ var _a, _b, _c;
2517
2733
  const deployCtx = ctx;
2518
- const appName = normalizePortalStorageName(((_a = deployCtx.request.body) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
2519
- const portalName = normalizePortalStorageName((_b = deployCtx.request.body) == null ? void 0 : _b.portal);
2520
- const basePath = trimString((_c = deployCtx.request.body) == null ? void 0 : _c.basePath);
2521
- const filePath = trimString((_d = deployCtx.request.file) == null ? void 0 : _d.path);
2734
+ const appName = this.getCurrentStorageAppName();
2735
+ const portalName = normalizePortalStorageName((_a = deployCtx.request.body) == null ? void 0 : _a.portal);
2736
+ const basePath = trimString((_b = deployCtx.request.body) == null ? void 0 : _b.basePath);
2737
+ const filePath = trimString((_c = deployCtx.request.file) == null ? void 0 : _c.path);
2522
2738
  try {
2523
2739
  if (!isValidPortalDeploySegment(appName)) {
2524
2740
  throw new Error("Invalid app");
@@ -2555,9 +2771,9 @@ class PluginMultiPortalServer extends import_server.Plugin {
2555
2771
  }
2556
2772
  }
2557
2773
  async pullPortalSource(ctx, next) {
2558
- var _a, _b;
2559
- const appName = normalizePortalStorageName(((_a = ctx.action.params.values) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
2560
- const portalName = normalizePortalStorageName((_b = ctx.action.params.values) == null ? void 0 : _b.portal);
2774
+ var _a;
2775
+ const appName = this.getCurrentStorageAppName();
2776
+ const portalName = normalizePortalStorageName((_a = ctx.action.params.values) == null ? void 0 : _a.portal);
2561
2777
  try {
2562
2778
  if (!isValidPortalDeploySegment(appName)) {
2563
2779
  throw new Error("Invalid app");
@@ -2577,11 +2793,11 @@ class PluginMultiPortalServer extends import_server.Plugin {
2577
2793
  }
2578
2794
  }
2579
2795
  async pushPortalSource(ctx, next) {
2580
- var _a, _b, _c;
2796
+ var _a, _b;
2581
2797
  const sourceCtx = ctx;
2582
- const appName = normalizePortalStorageName(((_a = sourceCtx.request.body) == null ? void 0 : _a.app) || MAIN_APP_NAME) || MAIN_APP_NAME;
2583
- const portalName = normalizePortalStorageName((_b = sourceCtx.request.body) == null ? void 0 : _b.portal);
2584
- const filePath = trimString((_c = sourceCtx.request.file) == null ? void 0 : _c.path);
2798
+ const appName = this.getCurrentStorageAppName();
2799
+ const portalName = normalizePortalStorageName((_a = sourceCtx.request.body) == null ? void 0 : _a.portal);
2800
+ const filePath = trimString((_b = sourceCtx.request.file) == null ? void 0 : _b.path);
2585
2801
  try {
2586
2802
  if (!isValidPortalDeploySegment(appName)) {
2587
2803
  throw new Error("Invalid app");
@@ -2635,38 +2851,20 @@ class PluginMultiPortalServer extends import_server.Plugin {
2635
2851
  const records = await this.db.getRepository("multiPortals").find({
2636
2852
  filter: {
2637
2853
  enabled: true,
2638
- "uiLayout.enabled": true
2854
+ uiLayoutUid: {
2855
+ $in: [...import_constants.MULTI_PORTAL_UI_LAYOUT_UIDS]
2856
+ }
2639
2857
  },
2640
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2641
- appends: ["uiLayout"],
2858
+ fields: [...MULTI_PORTAL_ACCESSIBLE_FIELDS],
2642
2859
  sort: ["uid"],
2643
2860
  transaction: options == null ? void 0 : options.transaction
2644
2861
  });
2645
- return records.map((record) => {
2646
- const portal = pickMultiPortalAccessibleFields(record);
2647
- const uiLayout = portal.uiLayout;
2648
- const uid = typeof portal.uid === "string" ? portal.uid : "";
2649
- const title = typeof portal.title === "string" ? portal.title : "";
2650
- return {
2651
- uid,
2652
- title,
2653
- icon: typeof portal.icon === "string" ? portal.icon : null,
2654
- portalType: typeof portal.portalType === "string" ? portal.portalType : null,
2655
- routePath: String(portal.routePath || ""),
2656
- layout: typeof (uiLayout == null ? void 0 : uiLayout.layoutType) === "string" ? uiLayout.layoutType : null
2657
- };
2862
+ return records.flatMap((record) => {
2863
+ const item = this.toAppPortalManifestItem(record);
2864
+ return item ? [item] : [];
2658
2865
  });
2659
2866
  }
2660
- async getUiLayout(uid, options) {
2661
- if (typeof uid !== "string" || !uid) {
2662
- return null;
2663
- }
2664
- return this.db.getRepository("uiLayouts").findOne({
2665
- filterByTk: uid,
2666
- transaction: options == null ? void 0 : options.transaction
2667
- });
2668
- }
2669
- async toAppPortalManifestItem(multiPortal, options) {
2867
+ toAppPortalManifestItem(multiPortal) {
2670
2868
  const uid = getRecordField(multiPortal, "uid");
2671
2869
  const title = getRecordField(multiPortal, "title");
2672
2870
  const portalType = getRecordField(multiPortal, "portalType");
@@ -2676,19 +2874,18 @@ class PluginMultiPortalServer extends import_server.Plugin {
2676
2874
  return null;
2677
2875
  }
2678
2876
  const uiLayoutUid = getRecordField(multiPortal, "uiLayoutUid");
2679
- const uiLayout = getRecordField(multiPortal, "uiLayout") || await this.getUiLayout(uiLayoutUid, options);
2680
- if (!uiLayout || getRecordField(uiLayout, "enabled") !== true) {
2877
+ const layout = (0, import_constants.getMultiPortalLayoutType)(uiLayoutUid);
2878
+ if (!layout) {
2681
2879
  return null;
2682
2880
  }
2683
2881
  const icon = getRecordField(multiPortal, "icon");
2684
- const layoutType = getRecordField(uiLayout, "layoutType");
2685
2882
  return {
2686
2883
  uid,
2687
2884
  title,
2688
2885
  icon: typeof icon === "string" ? icon : null,
2689
2886
  portalType: typeof portalType === "string" ? portalType : null,
2690
2887
  routePath,
2691
- layout: typeof layoutType === "string" ? layoutType : null
2888
+ layout
2692
2889
  };
2693
2890
  }
2694
2891
  async setAppManifestItem(item, options) {
@@ -2731,30 +2928,13 @@ class PluginMultiPortalServer extends import_server.Plugin {
2731
2928
  if (typeof uid !== "string" || !uid) {
2732
2929
  return;
2733
2930
  }
2734
- const item = await this.toAppPortalManifestItem(multiPortal, options);
2931
+ const item = this.toAppPortalManifestItem(multiPortal);
2735
2932
  if (item) {
2736
2933
  await this.setAppManifestItem(item, options);
2737
2934
  return;
2738
2935
  }
2739
2936
  await this.removeAppManifestItem(uid, options);
2740
2937
  }
2741
- async publishUiLayoutManifestItems(uiLayout, options) {
2742
- const uiLayoutUid = getRecordField(uiLayout, "uid");
2743
- if (typeof uiLayoutUid !== "string" || !uiLayoutUid) {
2744
- return;
2745
- }
2746
- const records = await this.db.getRepository("multiPortals").find({
2747
- filter: {
2748
- uiLayoutUid
2749
- },
2750
- fields: [...MULTI_PORTAL_ACCESSIBLE_QUERY_FIELDS],
2751
- transaction: options == null ? void 0 : options.transaction
2752
- });
2753
- for (const record of records) {
2754
- record.set("uiLayout", uiLayout);
2755
- await this.publishAppManifestItem(record, options);
2756
- }
2757
- }
2758
2938
  async reconcilePortalStorage(options) {
2759
2939
  const records = await this.db.getRepository("multiPortals").find({
2760
2940
  filter: {
@@ -2817,19 +2997,27 @@ class PluginMultiPortalServer extends import_server.Plugin {
2817
2997
  "desktopRoutes:listRolePermissionTargets",
2818
2998
  mapMultiPortalLayoutToUiLayoutForRolePermissionTargets
2819
2999
  );
3000
+ this.app.resourceManager.registerPreActionHandler("multiPortals:create", preventDirectDefaultPortalMutation);
3001
+ this.app.resourceManager.registerPreActionHandler("multiPortals:create", validateMultiPortalUiLayoutUidWrite);
2820
3002
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", captureSkipCreatePortalDirectory);
2821
3003
  this.app.resourceManager.registerPreActionHandler("multiPortals:create", normalizeMultiPortalSlugValues);
2822
- this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventMultiPortalBackingLayoutChange);
3004
+ this.app.resourceManager.registerPreActionHandler("multiPortals:update", preventDirectDefaultPortalMutation);
3005
+ this.app.resourceManager.registerPreActionHandler("multiPortals:update", validateMultiPortalUiLayoutUidWrite);
2823
3006
  this.app.resourceManager.registerPreActionHandler("multiPortals:update", normalizeMultiPortalSlugValues);
3007
+ this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", preventDirectDefaultPortalMutation);
2824
3008
  this.app.resourceManager.registerPreActionHandler(
2825
3009
  "multiPortals:firstOrCreate",
2826
- preventMultiPortalBackingLayoutChange
3010
+ validateMultiPortalUiLayoutUidWrite
2827
3011
  );
2828
3012
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", captureSkipCreatePortalDirectory);
2829
3013
  this.app.resourceManager.registerPreActionHandler("multiPortals:firstOrCreate", normalizeMultiPortalSlugValues);
2830
3014
  this.app.resourceManager.registerPreActionHandler(
2831
3015
  "multiPortals:updateOrCreate",
2832
- preventMultiPortalBackingLayoutChange
3016
+ preventDirectDefaultPortalMutation
3017
+ );
3018
+ this.app.resourceManager.registerPreActionHandler(
3019
+ "multiPortals:updateOrCreate",
3020
+ validateMultiPortalUiLayoutUidWrite
2833
3021
  );
2834
3022
  this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", captureSkipCreatePortalDirectory);
2835
3023
  this.app.resourceManager.registerPreActionHandler("multiPortals:updateOrCreate", normalizeMultiPortalSlugValues);
@@ -2857,12 +3045,15 @@ class PluginMultiPortalServer extends import_server.Plugin {
2857
3045
  actions: ROLE_MULTI_PORTAL_PERMISSION_ACTIONS
2858
3046
  });
2859
3047
  this.app.acl.allow("multiPortals", "listEnabled", "public");
3048
+ this.app.acl.allow("multiPortals", "getDefault", "public");
2860
3049
  this.app.acl.allow("multiPortals", "listAccessible", "loggedIn");
2861
3050
  this.app.resourceManager.use(createPortalDeployUploadMiddleware(), {
2862
3051
  tag: "multiPortalDeployUpload",
2863
3052
  after: "acl"
2864
3053
  });
2865
3054
  this.app.resourceManager.registerActionHandler("multiPortals:listEnabled", listEnabledMultiPortals);
3055
+ this.app.resourceManager.registerActionHandler("multiPortals:getDefault", getDefaultMultiPortal);
3056
+ this.app.resourceManager.registerActionHandler("multiPortals:setDefault", setDefaultMultiPortal);
2866
3057
  this.app.resourceManager.registerActionHandler("multiPortals:listAccessible", listAccessibleMultiPortals);
2867
3058
  this.app.resourceManager.registerActionHandler("multiPortals:deploy", async (ctx, next) => {
2868
3059
  await this.deployPortalDist(ctx, next);
@@ -2887,6 +3078,11 @@ class PluginMultiPortalServer extends import_server.Plugin {
2887
3078
  (0, import_ensureDefaultRoleMultiPortalAccess.applyDefaultRoleMultiPortalAccess)(role);
2888
3079
  });
2889
3080
  this.app.db.on("desktopRoutes.afterCreate", reconcileCreatedDesktopRoutePermissions);
3081
+ this.app.db.on("multiPortals.beforeUpdate", (multiPortal) => {
3082
+ if (multiPortal.get("enabled") === false && multiPortal.get("isDefault") === true) {
3083
+ multiPortal.set("isDefault", null);
3084
+ }
3085
+ });
2890
3086
  this.app.db.on("multiPortals.afterCreate", async (multiPortal, options) => {
2891
3087
  await grantDefaultAccessToNewMultiPortal(this.app.db, multiPortal, options);
2892
3088
  await this.publishAppManifestItem(multiPortal, options);
@@ -2906,9 +3102,6 @@ class PluginMultiPortalServer extends import_server.Plugin {
2906
3102
  }
2907
3103
  await this.removeMultiPortalStorageItem(multiPortal, options);
2908
3104
  });
2909
- this.app.db.on("uiLayouts.afterUpdate", async (uiLayout, options) => {
2910
- await this.publishUiLayoutManifestItems(uiLayout, options);
2911
- });
2912
3105
  }
2913
3106
  async install() {
2914
3107
  const version = await this.app.version.get();