adminizer 5.0.0-build.8 → 5.1.0-build.24

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.
Files changed (232) hide show
  1. package/assets/abnfDiagram-VCTEODGH-DFY3Pztx.js +1 -0
  2. package/assets/add-31tS-g4V.js +1 -0
  3. package/assets/add-group-Cf3zT7WE.js +1 -0
  4. package/assets/add-user-C62wOna-.js +1 -0
  5. package/assets/ai-assistant/agent.es.js +28464 -0
  6. package/assets/app-DPmI-MvN.css +1 -0
  7. package/assets/app-vdHbjOQX.js +138 -0
  8. package/assets/arc-DP4o1zz3.js +1 -0
  9. package/assets/architectureDiagram-5GKGNRK7-BjXW5Zu0.js +36 -0
  10. package/assets/asmcrypto-DZeQH0LC.js +1 -0
  11. package/assets/blockDiagram-I7D4REHJ-BUPnDwN0.js +129 -0
  12. package/assets/c4Diagram-7LVT6UL2-Cmb_dAhd.js +38 -0
  13. package/assets/{catalog-BEvJZYJP.js → catalog-C-y5rMaG.js} +3 -3
  14. package/assets/channel-CkJRUsq_.js +1 -0
  15. package/assets/chunk-2Q5K7J3B-CmS-dWwQ.js +1 -0
  16. package/assets/chunk-5VM5RSS4-Cqs2Ow64.js +15 -0
  17. package/assets/chunk-F27PBJKO-BoMCpJA1.js +1 -0
  18. package/assets/chunk-IMKFNOWR-DlDcPk1x.js +231 -0
  19. package/assets/chunk-JWPE2WC7-DzL_SU8s.js +1 -0
  20. package/assets/chunk-POPQ4Y6H-Cwq2-Vb8.js +1 -0
  21. package/assets/chunk-SVP7TREG-BfRHY5FU.js +88 -0
  22. package/assets/chunk-TICWLB2K-CRP7rHkW.js +206 -0
  23. package/assets/chunk-XXDRQBXY-Bg2jg-98.js +1 -0
  24. package/assets/classDiagram-ZZMXUADV-D4EF3HGj.js +1 -0
  25. package/assets/classDiagram-v2-VYDZK3BY-D4EF3HGj.js +1 -0
  26. package/assets/controls/ckeditor.es.js +9382 -9320
  27. package/assets/controls/handsontable.es.js +11971 -11938
  28. package/assets/controls/jsoneditor.es.js +10442 -10233
  29. package/assets/controls/toast-ui.es.js +1308 -1279
  30. package/assets/cose-bilkent-JH36ORCC-CCJ0SGyO.js +1 -0
  31. package/assets/cynefin-OW5HDTMX-B2Jx5fw3.js +166 -0
  32. package/assets/cynefinDiagram-5FMLGOSQ-DuK0Lb62.js +62 -0
  33. package/assets/cytoscape.esm-DzSz-X2X.js +321 -0
  34. package/assets/dagre-GXQ25YYZ-DnOYexET.js +4 -0
  35. package/assets/dashboard-X8k4X6Sw.js +3 -0
  36. package/assets/debounce-B3mXI_sx.js +1 -0
  37. package/assets/defaultLocale-DX6XiGOO.js +1 -0
  38. package/assets/diagram-S7CK7UJ4-DDgPBHsK.js +30 -0
  39. package/assets/diagram-UQ7AKVKN-qlL0GZs8.js +41 -0
  40. package/assets/diagram-VSXAHHWV-CQbiU1_c.js +3 -0
  41. package/assets/diagram-VX7I27RA-DFDI9_au.js +24 -0
  42. package/assets/diagram-Z3DM3KII-B5C9dtmJ.js +24 -0
  43. package/assets/docs-BiZOD7PP.js +1 -0
  44. package/assets/ebnfDiagram-PWID7BFC-CLMUsXEI.js +1 -0
  45. package/assets/erDiagram-RLTQ6QDP-ax9Xol5-.js +99 -0
  46. package/assets/flowDiagram-HODETNUW-CvaH7l3S.js +168 -0
  47. package/assets/ganttDiagram-EL5Y4UJY-Cga5U19j.js +292 -0
  48. package/assets/gitGraphDiagram-WWUBYQGX-B6o1T3-d.js +106 -0
  49. package/assets/history-CVBJUNSQ.js +1 -0
  50. package/assets/infoDiagram-27XIBGKW-BpWxgBCE.js +2 -0
  51. package/assets/init-Gi6I4Gst.js +1 -0
  52. package/assets/init-user-xSvnFTF0.js +1 -0
  53. package/assets/isObject-DL-BuaBl.js +1 -0
  54. package/assets/ishikawaDiagram-5VMMS53U-zE83db4H.js +70 -0
  55. package/assets/journeyDiagram-3NMN7TZE-DZNVFqrX.js +139 -0
  56. package/assets/kanban-definition-UXKFOSKX-CqxNgk27.js +89 -0
  57. package/assets/katex-HP8lGamR.js +257 -0
  58. package/assets/layout-CN3kt_jt.js +1 -0
  59. package/assets/linear-nvkaQ6BA.js +1 -0
  60. package/assets/{list-DhK0McVn.js → list-CrIU2eZz.js} +2 -2
  61. package/assets/login-CX9xVjCn.js +1 -0
  62. package/assets/manifest.json +834 -58
  63. package/assets/media-manager-BfYftLIH.js +6 -0
  64. package/assets/mermaid.core-C0V1NaVJ.js +310 -0
  65. package/assets/mindmap-definition-YA3MSWOX-Bh7_d9f1.js +96 -0
  66. package/assets/module-DPEvnad3.js +1 -0
  67. package/assets/notification-Dqpt5AIZ.js +20 -0
  68. package/assets/ordinal-Cboi1Yqb.js +1 -0
  69. package/assets/pegDiagram-XKGWAZYB-DMnjm6Z1.js +1 -0
  70. package/assets/pieDiagram-E7YTZNPT-D-RWCgYb.js +39 -0
  71. package/assets/quadrantDiagram-AXDQQJYC-BNBGP7RM.js +7 -0
  72. package/assets/railroadDiagram-O6MQD6OU-BIjVPT01.js +1 -0
  73. package/assets/register-CF4KQ97e.js +1 -0
  74. package/assets/requirementDiagram-BXWQKSXE-VPrtGdjM.js +84 -0
  75. package/assets/ru-CFJ4fysR.js +1 -0
  76. package/assets/sankeyDiagram-P5KCCOFB-ct9mLF3U.js +40 -0
  77. package/assets/sequenceDiagram-WJ2MYXX4-TC06Mbmw.js +162 -0
  78. package/assets/sizeCapture-INFHLROL-BMMwriCf.js +1 -0
  79. package/assets/stateDiagram-D77RDMKH-DtwYRPCX.js +1 -0
  80. package/assets/stateDiagram-v2-MP3YSRHH-C1yB0O5U.js +1 -0
  81. package/assets/swimlanes-42K2YHIH-DE3hRbbr.js +1 -0
  82. package/assets/swimlanesDiagram-VR7AAH4N-Ce4l1sQD.js +8 -0
  83. package/assets/timeline-definition-24CTP7MA-N7s_hfjj.js +120 -0
  84. package/assets/toNumber-DZb-r9sy.js +1 -0
  85. package/assets/use-appearance-DYTIKpT2.js +1 -0
  86. package/assets/user-filters-list-fR0YNJ3l.js +1 -0
  87. package/assets/vennDiagram-4TSXK5OY-Dj5bFQkR.js +34 -0
  88. package/assets/wardleyDiagram-VM6X3IG4-83ax25Z6.js +78 -0
  89. package/assets/welcome-v11MTsRK.js +1 -0
  90. package/assets/with-app-layout-CH6RIib1.js +31 -0
  91. package/assets/xychartDiagram-S5SC5T6Z-CW9Br5Xp.js +7 -0
  92. package/controllers/access-rights/permissionOptions.d.ts +2 -0
  93. package/controllers/access-rights/permissionOptions.js +23 -0
  94. package/controllers/add.js +2 -2
  95. package/controllers/addGroup.js +15 -6
  96. package/controllers/ai/AiAgentController.d.ts +66 -0
  97. package/controllers/ai/AiAgentController.js +435 -0
  98. package/controllers/ai/AiAssistantController.d.ts +0 -4
  99. package/controllers/ai/AiAssistantController.js +4 -78
  100. package/controllers/catalog/FrontendCatalogAdapter.js +7 -8
  101. package/controllers/docs/index.d.ts +22 -0
  102. package/controllers/docs/index.js +126 -0
  103. package/controllers/edit.js +2 -2
  104. package/controllers/editGroup.js +15 -6
  105. package/controllers/filter-fields/savedFilters.js +1 -1
  106. package/controllers/history-actions/HistoryController.js +7 -7
  107. package/controllers/inlineUpdate.js +10 -3
  108. package/controllers/list.js +1 -1
  109. package/controllers/login.js +1 -1
  110. package/controllers/notifications/NotificationController.js +14 -8
  111. package/controllers/view.js +3 -3
  112. package/controllers/widgets/filterInfo.js +1 -1
  113. package/helpers/accessRightsHelper.d.ts +30 -3
  114. package/helpers/accessRightsHelper.js +140 -12
  115. package/helpers/assetVersionHelper.d.ts +14 -0
  116. package/helpers/assetVersionHelper.js +42 -0
  117. package/helpers/configHelper.js +19 -18
  118. package/helpers/controllerHelper.d.ts +1 -0
  119. package/helpers/controllerHelper.js +25 -15
  120. package/helpers/fieldsHelper.d.ts +2 -1
  121. package/helpers/fieldsHelper.js +11 -5
  122. package/helpers/inertiaActionsHelper.d.ts +1 -1
  123. package/helpers/inertiaActionsHelper.js +4 -5
  124. package/helpers/inertiaAddHelper.d.ts +1 -1
  125. package/helpers/inertiaAddHelper.js +7 -6
  126. package/helpers/inertiaGroupHelper.js +10 -2
  127. package/helpers/inertiaListHelper.d.ts +1 -1
  128. package/helpers/inertiaListHelper.js +7 -8
  129. package/helpers/inertiaMenuHelper.d.ts +7 -2
  130. package/helpers/inertiaMenuHelper.js +12 -27
  131. package/helpers/menuHelper.d.ts +11 -1
  132. package/helpers/menuHelper.js +12 -0
  133. package/helpers/modelResourceHelper.d.ts +9 -0
  134. package/helpers/modelResourceHelper.js +43 -0
  135. package/helpers/navigationAccessHelper.d.ts +13 -0
  136. package/helpers/navigationAccessHelper.js +45 -0
  137. package/index.d.ts +11 -0
  138. package/index.js +11 -0
  139. package/interfaces/adminpanelConfig.d.ts +160 -2
  140. package/interfaces/queryCriteria.d.ts +13 -0
  141. package/interfaces/types.d.ts +169 -0
  142. package/lib/Adminizer.d.ts +8 -0
  143. package/lib/Adminizer.js +29 -2
  144. package/lib/DataAccessor.d.ts +100 -3
  145. package/lib/DataAccessor.js +454 -245
  146. package/lib/access-graph/AccessGraphResolver.d.ts +136 -0
  147. package/lib/access-graph/AccessGraphResolver.js +605 -0
  148. package/lib/access-graph/RecordAccessCache.d.ts +23 -0
  149. package/lib/access-graph/RecordAccessCache.js +30 -0
  150. package/lib/access-graph/RecordAccessResolver.d.ts +83 -0
  151. package/lib/access-graph/RecordAccessResolver.js +295 -0
  152. package/lib/access-graph/shared.d.ts +60 -0
  153. package/lib/access-graph/shared.js +121 -0
  154. package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
  155. package/lib/admin-links/AdminLinkHandler.js +183 -0
  156. package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
  157. package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
  158. package/lib/ai-assistant/AbstractAiModelService.d.ts +139 -2
  159. package/lib/ai-assistant/AbstractAiModelService.js +154 -1
  160. package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
  161. package/lib/ai-assistant/AiAgentRunManager.js +109 -0
  162. package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +61 -0
  163. package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +90 -0
  164. package/lib/ai-assistant/AiAssistantHandler.d.ts +1 -0
  165. package/lib/ai-assistant/AiAssistantHandler.js +4 -0
  166. package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
  167. package/lib/ai-assistant/AiAssistantUiMethodHandler.js +144 -0
  168. package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
  169. package/lib/ai-assistant/builtinAgentSkills.js +259 -0
  170. package/lib/ai-assistant/jsonSafe.d.ts +13 -0
  171. package/lib/ai-assistant/jsonSafe.js +81 -0
  172. package/lib/app-manager/AdminizerApp.d.ts +31 -2
  173. package/lib/app-manager/AppManager.js +43 -42
  174. package/lib/app-manager/ConfigLayerHandler.js +4 -0
  175. package/lib/docs/AbstractDocumentation.d.ts +100 -0
  176. package/lib/docs/AbstractDocumentation.js +108 -0
  177. package/lib/docs/DocumentationHandler.d.ts +100 -0
  178. package/lib/docs/DocumentationHandler.js +198 -0
  179. package/lib/docs/FileDocumentation.d.ts +47 -0
  180. package/lib/docs/FileDocumentation.js +189 -0
  181. package/lib/docs/docsAgentSkills.d.ts +10 -0
  182. package/lib/docs/docsAgentSkills.js +93 -0
  183. package/lib/filters/FilterService.js +4 -3
  184. package/lib/history-actions/AbstractHistoryAdapter.d.ts +26 -1
  185. package/lib/history-actions/AbstractHistoryAdapter.js +155 -25
  186. package/lib/media-manager/helpers/MediaManagerHelper.js +9 -7
  187. package/lib/model/AbstractModel.d.ts +48 -0
  188. package/lib/model/AbstractModel.js +14 -4
  189. package/lib/model/ModelHandler.d.ts +50 -2
  190. package/lib/model/ModelHandler.js +117 -8
  191. package/lib/model/adapter/sequelize.d.ts +43 -3
  192. package/lib/model/adapter/sequelize.js +317 -138
  193. package/lib/model/adapter/typeorm.d.ts +16 -0
  194. package/lib/model/adapter/typeorm.js +136 -9
  195. package/lib/notifications/GeneralNotificationService.js +6 -5
  196. package/lib/notifications/SystemNotificationService.js +1 -1
  197. package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
  198. package/lib/ui-i18n/uiTranslationKeys.js +42 -1
  199. package/lib/widgets/widgetHandler.js +10 -6
  200. package/models/Group.d.ts +2 -1
  201. package/package.json +35 -23
  202. package/policies/authPolicies.js +6 -6
  203. package/policies/permissionResolvers.d.ts +1 -0
  204. package/policies/permissionResolvers.js +3 -0
  205. package/system/Router.js +21 -3
  206. package/system/bindAccessRights.js +13 -3
  207. package/system/bindDocs.d.ts +13 -0
  208. package/system/bindDocs.js +77 -0
  209. package/system/bindInertia.js +30 -7
  210. package/system/buildInternalModelAccess.d.ts +8 -0
  211. package/system/buildInternalModelAccess.js +36 -4
  212. package/system/defaults.js +7 -1
  213. package/system/validateSystemModels.js +29 -9
  214. package/assets/add-DED_wpga.js +0 -1
  215. package/assets/add-group-CL80RdYb.js +0 -1
  216. package/assets/add-user-BRmwvQB8.js +0 -1
  217. package/assets/app-DNXiApCk.css +0 -1
  218. package/assets/app-layout-86yxiZOy.js +0 -1
  219. package/assets/app.js +0 -138
  220. package/assets/dashboard-ZP6K2s_8.js +0 -3
  221. package/assets/debounce-BMmXVQ06.js +0 -1
  222. package/assets/history-M61Bcv02.js +0 -1
  223. package/assets/init-user-8aUohVDq.js +0 -1
  224. package/assets/login-CO4laf6m.js +0 -1
  225. package/assets/media-manager-BDbpSSjH.js +0 -6
  226. package/assets/module-DfBneaUl.js +0 -1
  227. package/assets/notification-CVnQf3ef.js +0 -20
  228. package/assets/register-DBYIMXGQ.js +0 -1
  229. package/assets/ru-rYKdQbWF.js +0 -1
  230. package/assets/use-appearance-_EbbjHTh.js +0 -1
  231. package/assets/user-filters-list-DXhGLFTn.js +0 -1
  232. package/assets/welcome-CLxvXkcg.js +0 -1
@@ -0,0 +1,42 @@
1
+ import fs from "fs";
2
+ import path from "node:path";
3
+ let cachedVersion = null;
4
+ /**
5
+ * Adminizer package version. Some built entries keep stable file names because
6
+ * they are imported by a hard-coded path and so cannot be looked up in the
7
+ * manifest (agent.es.js, controls/*.es.js and controls/*.css); for those this
8
+ * version is appended as a query string to invalidate browser caches on release.
9
+ *
10
+ * Only ever put this on a *self-contained* bundle. An entry whose split chunks
11
+ * import it back by name (app.js) must be content-hashed instead — a query
12
+ * string there makes the browser instantiate the module twice.
13
+ */
14
+ export function getAssetVersion() {
15
+ if (cachedVersion !== null) {
16
+ return cachedVersion;
17
+ }
18
+ let version = "";
19
+ // Compiled sources sit one level below the package root in the published
20
+ // package (helpers/) and two below it in the repo (dist/helpers/).
21
+ for (const candidate of ["../package.json", "../../package.json"]) {
22
+ try {
23
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, candidate), "utf-8"));
24
+ if (pkg?.name === "adminizer" && typeof pkg.version === "string") {
25
+ version = pkg.version;
26
+ break;
27
+ }
28
+ }
29
+ catch {
30
+ // Missing or unreadable package.json: try the next candidate.
31
+ }
32
+ }
33
+ cachedVersion = version;
34
+ return version;
35
+ }
36
+ export function withAssetVersion(url) {
37
+ const version = getAssetVersion();
38
+ if (!url || !version || process.env.ADMINIZER_ENV === "dev") {
39
+ return url;
40
+ }
41
+ return `${url}${url.includes("?") ? "&" : "?"}v=${version}`;
42
+ }
@@ -34,23 +34,17 @@ export class ConfigHelper {
34
34
  if (!modelName) {
35
35
  throw new Error("Model name is not defined");
36
36
  }
37
- let config = this.adminizer.config;
38
- let modelConfig;
39
- Object.keys(config.models).forEach((modelResourceName) => {
40
- const model = config.models[modelResourceName];
41
- if (typeof model !== "boolean") {
42
- if (model.model === modelName.toLowerCase()) {
43
- if (typeof config.models[modelResourceName] !== "boolean") {
44
- modelConfig = config.models[modelResourceName];
45
- }
46
- }
47
- }
48
- });
37
+ const resourceName = this.adminizer.modelHandler.getResourceRecord(modelName)?.name
38
+ ?? this.adminizer.modelHandler.resolveResourceByHostModel(modelName);
39
+ const modelConfig = resourceName && typeof this.adminizer.config.models[resourceName] !== "boolean"
40
+ ? this.adminizer.config.models[resourceName]
41
+ : undefined;
42
+ const model = resourceName ? this.adminizer.modelHandler.getResource(resourceName) : undefined;
49
43
  if (modelConfig && modelConfig.identifierField) {
50
44
  return modelConfig.identifierField;
51
45
  }
52
- else if (this.adminizer.modelHandler.model.get(modelName.toLowerCase()).primaryKey) {
53
- return this.adminizer.modelHandler.model.get(modelName.toLowerCase()).primaryKey;
46
+ else if (model?.primaryKey) {
47
+ return model.primaryKey;
54
48
  }
55
49
  else {
56
50
  throw new Error("ConfigHelper > Identifier field was not found");
@@ -87,12 +81,13 @@ export class ConfigHelper {
87
81
  let displayField;
88
82
  try {
89
83
  const associatedModelName = config.type === "association"
90
- ? modelField.model?.toLowerCase()
91
- : modelField.collection?.toLowerCase();
84
+ ? modelField.model
85
+ : modelField.collection;
92
86
  if (!associatedModelName) {
93
87
  throw new Error(`No model/collection defined for association field: ${key}`);
94
88
  }
95
- const associatedModel = adminizer.modelHandler.model.get(associatedModelName);
89
+ const resourceName = adminizer.modelHandler.resolveAssociationResource(associatedModelName, modelField.resourceName);
90
+ const associatedModel = resourceName ? adminizer.modelHandler.getResource(resourceName) : undefined;
96
91
  if (!associatedModel) {
97
92
  throw new Error(`Can not add relations to unloaded models; Config: ${JSON.stringify(config, null, 2)}`);
98
93
  }
@@ -139,7 +134,13 @@ export class ConfigHelper {
139
134
  additionalLinks: [
140
135
  ...builtinLinks,
141
136
  ...(config.navbar?.additionalLinks || [])
142
- ]
137
+ ],
138
+ // Merged per section name so an app declaring its own sections
139
+ // keeps the built-in ones, while still being able to override them.
140
+ sections: {
141
+ ...defaultConfig.navbar?.sections,
142
+ ...config.navbar?.sections
143
+ }
143
144
  }
144
145
  };
145
146
  // Normalize auth config if it's a boolean
@@ -51,6 +51,7 @@ export declare class ControllerHelper {
51
51
  * @returns {?string}
52
52
  */
53
53
  static findModelResourceName(req: ReqType): string;
54
+ private static resolveConfiguredResourceName;
54
55
  /**
55
56
  * Searches for config from admin panel
56
57
  *
@@ -89,19 +89,27 @@ export class ControllerHelper {
89
89
  * @returns {?string}
90
90
  */
91
91
  static findModelResourceName(req) {
92
- if (req.params.modelResourceName) {
93
- return req.params.modelResourceName;
92
+ const models = req.adminizer.config.models;
93
+ const requestedName = req.params.modelResourceName
94
+ ?? req.params.model
95
+ ?? req.originalUrl.split('/')[3];
96
+ return this.resolveConfiguredResourceName(models, requestedName);
97
+ }
98
+ static resolveConfiguredResourceName(models, requestedName) {
99
+ if (!models || !requestedName) {
100
+ throw new Error(`Model "${requestedName}" not found`);
94
101
  }
95
- if (req.params.model) {
96
- return req.params.model;
102
+ if (Object.prototype.hasOwnProperty.call(models, requestedName)) {
103
+ return requestedName;
97
104
  }
98
- const urlParts = req.originalUrl.split('/');
99
- const modelResourceName = urlParts[3];
100
- const models = req.adminizer.config.models;
101
- if (!models || !Object.keys(models).some(key => key.toLowerCase() === modelResourceName.toLowerCase())) {
102
- throw new Error(`Model "${modelResourceName}" not found`);
105
+ const candidates = Object.keys(models).filter((key) => key.toLowerCase() === requestedName.toLowerCase());
106
+ if (candidates.length === 1) {
107
+ return candidates[0];
108
+ }
109
+ if (candidates.length > 1) {
110
+ throw new Error(`Model resource "${requestedName}" is ambiguous: ${candidates.join(", ")}`);
103
111
  }
104
- return Object.keys(models).find(key => key.toLowerCase() === modelResourceName.toLowerCase());
112
+ throw new Error(`Model "${requestedName}" not found`);
105
113
  }
106
114
  /**
107
115
  * Searches for config from admin panel
@@ -116,12 +124,14 @@ export class ControllerHelper {
116
124
  Adminizer.log.error('No models configuration found');
117
125
  return null;
118
126
  }
119
- const foundKey = Object.keys(models).find(key => key.toLowerCase() === modelResourceName.toLowerCase());
120
- if (!foundKey) {
121
- Adminizer.log.error(`No such route exists: ${modelResourceName}`);
127
+ try {
128
+ const foundKey = this.resolveConfiguredResourceName(models, modelResourceName);
129
+ return this._normalizeModelConfig(foundKey, models[foundKey]);
130
+ }
131
+ catch (error) {
132
+ Adminizer.log.error(error);
122
133
  return null;
123
134
  }
124
- return this._normalizeModelConfig(foundKey, models[foundKey]);
125
135
  }
126
136
  /**
127
137
  * Will get action config from configuration file depending to given action
@@ -197,7 +207,7 @@ export class ControllerHelper {
197
207
  modelResource.config = this.findModelConfig(req, modelResourceName);
198
208
  // Find and add the model itself to the ModelResource
199
209
  if (this._isValidModelConfig(modelResource.config)) {
200
- modelResource.model = req.adminizer.modelHandler.model.get(modelResource.config.model);
210
+ modelResource.model = req.adminizer.modelHandler.getResource(modelResourceName);
201
211
  }
202
212
  // Return the completed ModelResource object
203
213
  return modelResource;
@@ -1,5 +1,6 @@
1
1
  import { ActionType, BaseFieldConfig, FieldsTypes, ModelConfig } from "../interfaces/adminpanelConfig.js";
2
2
  import { Attribute } from "../lib/model/AbstractModel.js";
3
+ import { RecordAccessCache } from "../lib/access-graph/RecordAccessCache.js";
3
4
  export type Field = {
4
5
  config: BaseFieldConfig & {
5
6
  /** @deprecated record should not be in config anymore */
@@ -30,5 +31,5 @@ export declare class FieldsHelper {
30
31
  * @param action
31
32
  * @deprecated this method has overload association tree on each request that is not optimize
32
33
  */
33
- static loadAssociations(req: ReqType, fields: Fields, action?: ActionType): Promise<Fields>;
34
+ static loadAssociations(req: ReqType, fields: Fields, action?: ActionType, recordAccessCache?: RecordAccessCache): Promise<Fields>;
34
35
  }
@@ -1,4 +1,5 @@
1
1
  import { DataAccessor } from "../lib/DataAccessor.js";
2
+ import { RecordAccessCache } from "../lib/access-graph/RecordAccessCache.js";
2
3
  import { Adminizer } from "../lib/Adminizer.js";
3
4
  export class FieldsHelper {
4
5
  /**
@@ -11,7 +12,11 @@ export class FieldsHelper {
11
12
  * @deprecated this method has overload association tree on each request that is not optimize
12
13
  */
13
14
  // TODO: make separate request for all dropdown relations by api call
14
- static async loadAssociations(req, fields, action) {
15
+ static async loadAssociations(req, fields, action, recordAccessCache = new RecordAccessCache()) {
16
+ // `getFieldsConfig()` returns undefined when the user has no rights on the model at all
17
+ if (!fields) {
18
+ return {};
19
+ }
15
20
  /**
16
21
  * Load all associated records for given field key
17
22
  *
@@ -30,9 +35,9 @@ export class FieldsHelper {
30
35
  Adminizer.log.error('No model found for field: ', fields[key]);
31
36
  return;
32
37
  }
33
- const resolvedModelName = req.adminizer.modelHandler.resolveModelName(modelName);
34
- let Model = req.adminizer.modelHandler.model.get(resolvedModelName);
35
- if (!Model) {
38
+ const resolvedModelName = req.adminizer.modelHandler.resolveAssociationResource(modelName, fields[key].model.resourceName);
39
+ let Model = resolvedModelName ? req.adminizer.modelHandler.getResource(resolvedModelName) : undefined;
40
+ if (!Model || !resolvedModelName) {
36
41
  return;
37
42
  }
38
43
  let list;
@@ -46,7 +51,8 @@ export class FieldsHelper {
46
51
  model: Model,
47
52
  uri: `${req.adminizer.config.routePrefix}/model/${resolvedModelName}`
48
53
  };
49
- let dataAccessor = new DataAccessor(req.adminizer, req.user, modelResource, "view");
54
+ // One cache for every dropdown of the page: the record-access lookups are identical
55
+ let dataAccessor = new DataAccessor(req.adminizer, req.user, modelResource, "view", recordAccessCache);
50
56
  list = await Model.find({}, dataAccessor);
51
57
  }
52
58
  catch (e) {
@@ -7,4 +7,4 @@ export interface Actions {
7
7
  title: string;
8
8
  icon: string;
9
9
  }
10
- export default function inertiaActionsHelper(actionType: ActionType, modelResource: ModelResource, req: ReqType): Actions[];
10
+ export default function inertiaActionsHelper(actionType: ActionType, modelResource: ModelResource, req: ReqType): Promise<Actions[]>;
@@ -1,11 +1,10 @@
1
- export default function inertiaActionsHelper(actionType, modelResource, req) {
1
+ export default async function inertiaActionsHelper(actionType, modelResource, req) {
2
2
  let resActions = [];
3
3
  if (req.adminizer.menuHelper.hasGlobalActions(modelResource.config, actionType)) {
4
4
  const actions = req.adminizer.menuHelper.getGlobalActions(modelResource.config, actionType);
5
5
  if (actions && actions.length > 0) {
6
- actions.forEach(function (action) {
7
- const context = `global action "${action.title}" (${action.id}) on model "${modelResource.name}"`;
8
- if (req.adminizer.accessRightsHelper.hasPermission(action.accessRightsToken, req.user, context)) {
6
+ for (const action of actions) {
7
+ if (await req.adminizer.accessRightsHelper.hasPermission(action.accessRightsToken, req.user)) {
9
8
  resActions.push({
10
9
  link: action.link,
11
10
  id: action.id,
@@ -14,7 +13,7 @@ export default function inertiaActionsHelper(actionType, modelResource, req) {
14
13
  icon: action.icon
15
14
  });
16
15
  }
17
- });
16
+ }
18
17
  }
19
18
  }
20
19
  return resActions;
@@ -23,7 +23,7 @@ interface FieldProps extends Record<string | number | symbol, unknown> {
23
23
  postLink: string;
24
24
  model: string;
25
25
  }
26
- export default function inertiaAddHelper(req: ReqType, modelResource: ModelResource, fields: Fields, record?: Record<string, string | boolean | number | string[]>, view?: boolean): FieldProps;
26
+ export default function inertiaAddHelper(req: ReqType, modelResource: ModelResource, fields: Fields, record?: Record<string, string | boolean | number | string[]>, view?: boolean): Promise<FieldProps>;
27
27
  export declare function getControlsOptions(fieldConfig: Field["config"], req: ReqType, type: ControlType, defaultControlName: string, context?: string): {
28
28
  name: string;
29
29
  config: any;
@@ -1,5 +1,6 @@
1
1
  import inertiaActionsHelper from "./inertiaActionsHelper.js";
2
2
  import { isObject } from "./JsUtils.js";
3
+ import { withAssetVersion } from "./assetVersionHelper.js";
3
4
  import { Adminizer } from "../lib/Adminizer.js";
4
5
  const missingControlWarnings = new Set();
5
6
  /**
@@ -14,7 +15,7 @@ function getWeekNumber(date) {
14
15
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
15
16
  return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
16
17
  }
17
- export default function inertiaAddHelper(req, modelResource, fields, record, view = false) {
18
+ export default async function inertiaAddHelper(req, modelResource, fields, record, view = false) {
18
19
  const actionType = 'add';
19
20
  let props = {
20
21
  edit: !!record,
@@ -36,7 +37,7 @@ export default function inertiaAddHelper(req, modelResource, fields, record, vie
36
37
  postLink: record ? `${modelResource.uri}/edit/${record.id}` : `${modelResource.uri}/add`,
37
38
  model: modelResource.name.toLocaleLowerCase()
38
39
  };
39
- props.actions = inertiaActionsHelper(actionType, modelResource, req);
40
+ props.actions = await inertiaActionsHelper(actionType, modelResource, req);
40
41
  const config = req.adminizer.configHelper.getConfig();
41
42
  for (const key of Object.keys(fields)) {
42
43
  if ((!config.showORMtime) && (key === 'createdAt' || key === 'updatedAt'))
@@ -62,7 +63,7 @@ export default function inertiaAddHelper(req, modelResource, fields, record, vie
62
63
  let canCreateRelated = false;
63
64
  const controlContext = `${modelResource.name}.${key}`;
64
65
  //@ts-ignore TODO: fix field type
65
- if (modelResource.config.model && req.adminizer.configHelper.isId(field, modelResource.config.model)) {
66
+ if (req.adminizer.configHelper.isId(field, modelResource.name)) {
66
67
  disabled = true;
67
68
  }
68
69
  if (['string', 'password', 'date', 'datetime', 'time', 'integer', 'number', 'float', 'email', 'month', 'week', 'range'].includes(type)) {
@@ -118,7 +119,7 @@ export default function inertiaAddHelper(req, modelResource, fields, record, vie
118
119
  ? req.adminizer.modelHandler.resolveModelName(rawRelatedModel)
119
120
  : undefined;
120
121
  if (relatedModel && req.user) {
121
- canCreateRelated = req.adminizer.accessRightsHelper.hasPermission(`create-${relatedModel}-model`, req.user);
122
+ canCreateRelated = await req.adminizer.accessRightsHelper.hasPermission(`create-${relatedModel}-model`, req.user);
122
123
  }
123
124
  else if (relatedModel && !req.adminizer.config.auth?.enable) {
124
125
  canCreateRelated = true;
@@ -195,8 +196,8 @@ export function getControlsOptions(fieldConfig, req, type, defaultControlName, c
195
196
  ...(control?.getConfig() || {}), // Base config of the editor
196
197
  ...(shouldUseFieldConfig ? fieldOptions?.config || {} : {}),
197
198
  },
198
- path: control.getJsPath(),
199
- cssPath: control.getCssPath(),
199
+ path: withAssetVersion(control.getJsPath()),
200
+ cssPath: withAssetVersion(control.getCssPath()),
200
201
  };
201
202
  // If items are provided, use them instead of the CKEditor defaults.
202
203
  if (type === 'wysiwyg'
@@ -1,3 +1,4 @@
1
+ import { parseGroupPermissionGrant } from "./accessRightsHelper.js";
1
2
  export function inertiaGroupHelper(modelResource, req, users, groupedTokens, group, view = false) {
2
3
  let props = {
3
4
  edit: !!group,
@@ -55,8 +56,15 @@ export function inertiaGroupHelper(modelResource, req, users, groupedTokens, gro
55
56
  label: token.name,
56
57
  tooltip: token.description,
57
58
  name: `token-checkbox-${token.id}`,
58
- value: group ? (group.tokens && group.tokens?.includes(token.id)) : false,
59
- type: 'checkbox'
59
+ value: group ? Boolean(group.tokens?.some((value) => value === token.id || parseGroupPermissionGrant(value)?.tokenId === token.id)) : false,
60
+ type: 'checkbox',
61
+ options: token.getOptions ? {
62
+ permissionOptions: {
63
+ rights: group?.tokens
64
+ ?.map(parseGroupPermissionGrant)
65
+ .find((value) => value?.tokenId === token.id)?.rights ?? [],
66
+ },
67
+ } : undefined,
60
68
  });
61
69
  }
62
70
  props.groupedTokens.push({
@@ -28,5 +28,5 @@ interface listProps extends Record<string | number | symbol, unknown> {
28
28
  defaultPageSize: number;
29
29
  pageSizeOptions: number[];
30
30
  }
31
- export declare function inertiaListHelper(modelResource: ModelResource, req: ReqType, fields: Fields, activeFilterName?: string): listProps;
31
+ export declare function inertiaListHelper(modelResource: ModelResource, req: ReqType, fields: Fields, activeFilterName?: string): Promise<listProps>;
32
32
  export {};
@@ -1,5 +1,5 @@
1
1
  import inertiaActionsHelper from "./inertiaActionsHelper.js";
2
- export function inertiaListHelper(modelResource, req, fields, activeFilterName) {
2
+ export async function inertiaListHelper(modelResource, req, fields, activeFilterName) {
3
3
  const actionType = 'list';
4
4
  // Check if filters are enabled for this model
5
5
  // Preferred: model-level `models.<Model>.filters`; fallback: legacy top-level `modelFilters.<Model>`
@@ -31,23 +31,22 @@ export function inertiaListHelper(modelResource, req, fields, activeFilterName)
31
31
  defaultPageSize: req.adminizer.config.list?.defaultPageSize ?? 50,
32
32
  pageSizeOptions: [5, 20, 50],
33
33
  };
34
- if (modelResource.config.add && req.adminizer.accessRightsHelper.hasPermission(`create-${modelResource.name}-model`, req.user, `CRUD create on model "${modelResource.name}"`)) {
34
+ if (modelResource.config.add && await req.adminizer.accessRightsHelper.hasPermission(`create-${modelResource.name}-model`, req.user)) {
35
35
  props.crudActions.createTitle = req.i18n.__('create');
36
36
  }
37
- if (modelResource.config.edit && req.adminizer.accessRightsHelper.hasPermission(`update-${modelResource.name}-model`, req.user, `CRUD update on model "${modelResource.name}"`)) {
37
+ if (modelResource.config.edit && await req.adminizer.accessRightsHelper.hasPermission(`update-${modelResource.name}-model`, req.user)) {
38
38
  props.crudActions.editTitle = req.i18n.__('Edit');
39
39
  }
40
- if (modelResource.config.view && req.adminizer.accessRightsHelper.hasPermission(`read-${modelResource.name}-model`, req.user, `CRUD read on model "${modelResource.name}"`)) {
40
+ if (modelResource.config.view && await req.adminizer.accessRightsHelper.hasPermission(`read-${modelResource.name}-model`, req.user)) {
41
41
  props.crudActions.viewsTitle = req.i18n.__('View');
42
42
  }
43
- if (modelResource.config.remove && req.adminizer.accessRightsHelper.hasPermission(`delete-${modelResource.name}-model`, req.user, `CRUD delete on model "${modelResource.name}"`)) {
43
+ if (modelResource.config.remove && await req.adminizer.accessRightsHelper.hasPermission(`delete-${modelResource.name}-model`, req.user)) {
44
44
  props.crudActions.deleteTitle = req.i18n.__('Delete');
45
45
  }
46
- props.actions = inertiaActionsHelper(actionType, modelResource, req);
46
+ props.actions = await inertiaActionsHelper(actionType, modelResource, req);
47
47
  if (req.adminizer.menuHelper.hasInlineActions(modelResource.config, 'list')) {
48
48
  for (const inlineAction of req.adminizer.menuHelper.getInlineActions(modelResource.config, 'list')) {
49
- const context = `inline action "${inlineAction.title}" (${inlineAction.id}) on model "${modelResource.name}"`;
50
- if (req.adminizer.accessRightsHelper.hasPermission(inlineAction.accessRightsToken, req.user, context)) {
49
+ if (await req.adminizer.accessRightsHelper.hasPermission(inlineAction.accessRightsToken, req.user)) {
51
50
  props.inlineActions.push({
52
51
  icon: inlineAction.icon,
53
52
  id: inlineAction.id,
@@ -1,12 +1,17 @@
1
1
  import { Adminizer } from "../lib/Adminizer.js";
2
+ import { NavbarSectionConfig } from "../interfaces/adminpanelConfig.js";
2
3
  import { MenuItem } from "./menuHelper.js";
3
4
  export declare class InertiaMenuHelper {
4
5
  private adminizer;
5
6
  constructor(adminizer: Adminizer);
6
- getMenuItems(req: ReqType): MenuItem[];
7
- private filterAccessibleItems;
7
+ getMenuItems(req: ReqType): Promise<MenuItem[]>;
8
8
  private translateMenuItem;
9
9
  private translateHrefItem;
10
+ /**
11
+ * Navbar section metadata keyed by the *translated* section name, so the UI
12
+ * can look it up with the already-translated `section` of a menu item.
13
+ */
14
+ getSections(req: ReqType): Record<string, NavbarSectionConfig>;
10
15
  getBrandTitle(): string;
11
16
  getLogoutUrl(): string;
12
17
  }
@@ -1,35 +1,12 @@
1
+ import { listAccessibleMenuItems } from "./navigationAccessHelper.js";
1
2
  export class InertiaMenuHelper {
2
3
  adminizer;
3
4
  constructor(adminizer) {
4
5
  this.adminizer = adminizer;
5
6
  }
6
- getMenuItems(req) {
7
- const menu = [];
8
- for (const menuItem of this.adminizer.menuHelper.getMenuItems(req.user)) {
9
- const filteredActions = this.filterAccessibleItems(menuItem.actions ?? [], req);
10
- const menuItemTokens = filteredActions.map(item => {
11
- return item.accessRightsToken;
12
- }).filter(item => {
13
- return item;
14
- });
15
- if (menuItem.accessRightsToken)
16
- menuItemTokens.push(menuItem.accessRightsToken);
17
- if (this.adminizer.accessRightsHelper.enoughPermissions(menuItemTokens, req.user)) {
18
- menu.push(this.translateMenuItem(req, {
19
- ...menuItem,
20
- actions: filteredActions
21
- }));
22
- }
23
- }
24
- return menu;
25
- }
26
- filterAccessibleItems(items, req) {
27
- return items
28
- .filter((item) => !item.accessRightsToken || this.adminizer.accessRightsHelper.hasPermission(item.accessRightsToken, req.user))
29
- .map((item) => ({
30
- ...item,
31
- subItems: item.subItems ? this.filterAccessibleItems(item.subItems, req) : item.subItems
32
- }));
7
+ async getMenuItems(req) {
8
+ return (await listAccessibleMenuItems(this.adminizer, req.user))
9
+ .map((menuItem) => this.translateMenuItem(req, menuItem));
33
10
  }
34
11
  translateMenuItem(req, menuItem) {
35
12
  return {
@@ -47,6 +24,14 @@ export class InertiaMenuHelper {
47
24
  subItems: item.subItems?.map((subItem) => this.translateHrefItem(req, subItem)),
48
25
  };
49
26
  }
27
+ /**
28
+ * Navbar section metadata keyed by the *translated* section name, so the UI
29
+ * can look it up with the already-translated `section` of a menu item.
30
+ */
31
+ getSections(req) {
32
+ const sections = this.adminizer.menuHelper.getSections();
33
+ return Object.fromEntries(Object.entries(sections).map(([name, section]) => [req.i18n.__(name), section]));
34
+ }
50
35
  getBrandTitle() {
51
36
  return this.adminizer.menuHelper.getBrandTitle();
52
37
  }
@@ -4,7 +4,7 @@
4
4
  * @constructor
5
5
  */
6
6
  import { User } from "../models/User.js";
7
- import { ActionType, AdminpanelConfig, HrefConfig, ModelConfig } from "../interfaces/adminpanelConfig.js";
7
+ import { ActionType, AdminpanelConfig, HrefConfig, ModelConfig, NavbarSectionConfig } from "../interfaces/adminpanelConfig.js";
8
8
  export type MenuItem = {
9
9
  link: string;
10
10
  title: string;
@@ -28,6 +28,16 @@ export declare class MenuHelper {
28
28
  * @returns {string}
29
29
  */
30
30
  getBrandTitle(): string;
31
+ /**
32
+ * Presentation metadata of navbar sections, keyed by section name.
33
+ *
34
+ * Only sections declared under `navbar.sections` are present: a section
35
+ * used by links but never configured simply has no entry, and the UI falls
36
+ * back to its default rendering.
37
+ *
38
+ * @returns {Record<string, NavbarSectionConfig>}
39
+ */
40
+ getSections(): Record<string, NavbarSectionConfig>;
31
41
  /**
32
42
  * Check if global actions buttons added to action
33
43
  *
@@ -63,6 +63,18 @@ export class MenuHelper {
63
63
  }
64
64
  return 'Adminizer';
65
65
  }
66
+ /**
67
+ * Presentation metadata of navbar sections, keyed by section name.
68
+ *
69
+ * Only sections declared under `navbar.sections` are present: a section
70
+ * used by links but never configured simply has no entry, and the UI falls
71
+ * back to its default rendering.
72
+ *
73
+ * @returns {Record<string, NavbarSectionConfig>}
74
+ */
75
+ getSections() {
76
+ return { ...(this.config.navbar?.sections ?? {}) };
77
+ }
66
78
  /**
67
79
  * Check if global actions buttons added to action
68
80
  *
@@ -0,0 +1,9 @@
1
+ import type { ModelConfig } from '../interfaces/adminpanelConfig.js';
2
+ import type { ModelResource } from '../interfaces/types.js';
3
+ import type { Adminizer } from '../lib/Adminizer.js';
4
+ /** Config defaults of a model resource declared as `true` in the panel config. */
5
+ export declare function normalizeModelConfig(name: string, config: ModelConfig | boolean): ModelConfig;
6
+ /** Every configured model resource whose model is actually registered. */
7
+ export declare function listModelResources(adminizer: Adminizer): ModelResource[];
8
+ /** Resolves a resource by its Adminizer name, then by a uniquely registered host model name. */
9
+ export declare function resolveModelResource(adminizer: Adminizer, modelName: string): ModelResource | undefined;
@@ -0,0 +1,43 @@
1
+ /** Config defaults of a model resource declared as `true` in the panel config. */
2
+ export function normalizeModelConfig(name, config) {
3
+ const baseConfig = {
4
+ model: name,
5
+ icon: "description",
6
+ title: name,
7
+ list: true,
8
+ add: true,
9
+ edit: true,
10
+ remove: true,
11
+ view: true,
12
+ };
13
+ if (typeof config === "boolean") {
14
+ return config ? baseConfig : { ...baseConfig, list: false, add: false, edit: false, remove: false, view: false };
15
+ }
16
+ return { ...baseConfig, ...config };
17
+ }
18
+ /** Every configured model resource whose model is actually registered. */
19
+ export function listModelResources(adminizer) {
20
+ const resources = [];
21
+ for (const [configName, configValue] of Object.entries(adminizer.config.models ?? {})) {
22
+ const config = normalizeModelConfig(configName, configValue);
23
+ const model = adminizer.modelHandler.getResource(configName);
24
+ if (!model)
25
+ continue;
26
+ resources.push({
27
+ name: configName,
28
+ uri: `${adminizer.config.routePrefix}/model/${configName}`,
29
+ config,
30
+ model,
31
+ });
32
+ }
33
+ return resources;
34
+ }
35
+ /** Resolves a resource by its Adminizer name, then by a uniquely registered host model name. */
36
+ export function resolveModelResource(adminizer, modelName) {
37
+ const requestedName = modelName.trim();
38
+ const resourceName = adminizer.modelHandler.getResourceRecord(requestedName)?.name
39
+ ?? adminizer.modelHandler.resolveResourceByHostModel(requestedName);
40
+ return resourceName
41
+ ? listModelResources(adminizer).find((resource) => resource.name === resourceName)
42
+ : undefined;
43
+ }
@@ -0,0 +1,13 @@
1
+ import type { HrefConfig } from '../interfaces/adminpanelConfig.js';
2
+ import type { Adminizer } from '../lib/Adminizer.js';
3
+ import type { User } from '../models/User.js';
4
+ import type { MenuItem } from './menuHelper.js';
5
+ /** Drops the sub-items this user may not open, recursively. */
6
+ export declare function filterAccessibleHrefItems(adminizer: Adminizer, user: User, items: HrefConfig[]): Promise<HrefConfig[]>;
7
+ /**
8
+ * The navigation menu a user may actually open: configured models, additional
9
+ * links and app-contributed pages, filtered with the very same rules the
10
+ * sidebar uses. Titles are left untranslated, so callers that need i18n (the
11
+ * Inertia page props) translate on top of this.
12
+ */
13
+ export declare function listAccessibleMenuItems(adminizer: Adminizer, user: User): Promise<MenuItem[]>;
@@ -0,0 +1,45 @@
1
+ /** Drops the sub-items this user may not open, recursively. */
2
+ export async function filterAccessibleHrefItems(adminizer, user, items) {
3
+ const accessible = [];
4
+ for (const item of items) {
5
+ if (item.accessRightsToken && !await adminizer.accessRightsHelper.hasPermission(item.accessRightsToken, user)) {
6
+ continue;
7
+ }
8
+ accessible.push({
9
+ ...item,
10
+ subItems: item.subItems ? await filterAccessibleHrefItems(adminizer, user, item.subItems) : item.subItems,
11
+ });
12
+ }
13
+ return accessible;
14
+ }
15
+ /**
16
+ * The navigation menu a user may actually open: configured models, additional
17
+ * links and app-contributed pages, filtered with the very same rules the
18
+ * sidebar uses. Titles are left untranslated, so callers that need i18n (the
19
+ * Inertia page props) translate on top of this.
20
+ */
21
+ export async function listAccessibleMenuItems(adminizer, user) {
22
+ const menu = [];
23
+ for (const menuItem of adminizer.menuHelper.getMenuItems(user)) {
24
+ const actions = await filterAccessibleHrefItems(adminizer, user, menuItem.actions ?? []);
25
+ const tokens = actions.map((item) => item.accessRightsToken).filter(Boolean);
26
+ if (menuItem.accessRightsToken)
27
+ tokens.push(menuItem.accessRightsToken);
28
+ if (await adminizer.accessRightsHelper.enoughPermissions(tokens, user)) {
29
+ menu.push({ ...menuItem, actions });
30
+ }
31
+ }
32
+ for (const link of await adminizer.adminLinkHandler.list(user)) {
33
+ menu.push({
34
+ id: link.id,
35
+ link: link.link,
36
+ title: link.title,
37
+ type: link.type === 'blank' ? 'blank' : 'self',
38
+ actions: null,
39
+ icon: null,
40
+ accessRightsToken: link.accessRightsToken ?? null,
41
+ section: link.section || 'Platform',
42
+ });
43
+ }
44
+ return menu;
45
+ }