menu
The server-side menu resolver plus its client widget — the back-office main navigation menu: the permission-filtered tree of ontology areas and sections the logged user may reach, the top utility bar (user, language, theme, AI assistant, inspector toggle) and the developer debug-info bar.
See also: Architecture overview · area · Ontology · Tools · dd_object
This page is the developer reference for the menu subsystem: on the
server it is a small, dependency-free resolver module
(src/core/api/handlers/menu.ts, getMenuTreeDatalist()) called from the menu
read action (readMenu() in src/core/api/handlers/dd_core_api.ts); the client is a
widget (menu.js + render/view files) that turns the
resulting datalist into the desktop dropdown tree, the mobile menu and the top
utility bar. The menu is not a section: it stores nothing, owns no record,
and exists only to route the user into areas/sections.
Role
getMenuTreeDatalist() (in src/core/api/handlers/menu.ts) is a plain async
function whose single job is to produce the navigation datalist — a flat
array of {tipo, model, parent, label, config?} items describing the areas
and sections the current user is authorised to open. The sibling
buildInfoData() (in src/core/resolve/environment.ts) assembles the
info_data object for the developer debug bar. Both are invoked by
readMenu() in src/core/api/handlers/dd_core_api.ts, which also stamps the
menu's {context} entry via the shared buildStructureContext(), fixed at
permission level 2.
It sits at the top of the back-office UI, between the ontology (the source of the area/section tree) and the page shell:
| layer | role |
|---|---|
| area | The ontology areas (area_root, area_resource, area_admin, area_thesaurus, …) that are the roots of the menu tree. getMenuTreeDatalist() walks the ontology directly — the walk is inlined in menu.ts, not a separate call. |
| principal / security gates | Resolves who can see what: the caller passes {userId, isGlobalAdmin, isDeveloper} (the request's Principal, see security); getMenuTreeDatalist() filters the areas through these flags plus the per-user authorized-areas table. |
menu.ts resolver (this subsystem, server side) |
Builds the permission-filtered, re-parented datalist; readMenu() packs it with info_data/show_ontology/username into the {context, data} datum. |
menu.js (client) |
Consumes the datum and renders the desktop tree, the mobile menu and the top utility bar; publishes user_navigation events when an item is clicked. |
menu is a singleton-ish page element: the client keeps the menu instance alive
across section navigations (it is in page.js's base_models = ['menu'], so it
is never destroyed on page refresh) and only its section_label is updated as
the user moves between records.
No menu object
There is no menu object on the server — getMenuTreeDatalist() /
buildInfoData() are stateless functions, and the menu's {context} is
produced by the same generic buildStructureContext() every model
(section, area_*, menu, …) goes through, at a hardcoded permission
level of 2.
The menu is fixed to a stable identity quintet (readMenu() passes these
into buildStructureContext() / getMenuTreeDatalist()):
| field | value |
|---|---|
tipo |
dd85 (the ontology node of the menu model) |
section_tipo |
dd1 (the ontology root) |
mode |
'list' (the mode readMenu() requests for the context stamp) |
lang |
the current application language (currentApplicationLang(), request-scoped and set by change_lang) |
Responsibilities
- Enumerate the areas —
getMenuTreeDatalist()walks the ontology's area roots directly (fixed root order:area_root,area_activity,area_resource,area_tool,area_thesaurus,area_graph,area_admin,area_maintenance,area_development,area_ontology), depth-first, keeping only area/section/section_tool nodes. - Permission-filter — keep only the areas the user may open: global admins + developers see everything; everyone else is intersected with the caller's authorized-areas set, and the Maintenance and Development areas are gated to admins/developers regardless of stored permissions.
- Flatten + re-parent the tree — rewrite each item's
parentso that "skip" grouping tipos (getEffectiveMenuSkipTipos(config.menu.skipTipos), runtime-editable via the maintenance area) are removed from the visible tree while their children are lifted up to the nearest non-skipped ancestor (getVisibleParent()). - Resolve special area models — rewrite
section_toolareas into a realsectionplus a tool context, and the two thesaurus virtual areas intoarea_thesauruswith aswap_tipo/ view-mode config. - System info —
buildInfoData()assemblesinfo_data(Dédalo/runtime/ PostgreSQL versions, DB name, entity, server IP, …) for the developer debug bar. - Context —
readMenu()stamps a minimal menudd_objectcontext carrying the menu's own tools (e.g.tool_user_admin) via the sharedbuildStructureContext(). - Client render — build the desktop dropdown tree, the mobile menu and the
top utility bar; cache the API datum in local DB per
(lang, version, user)and invalidate it on logout.
Data model
The menu does not persist anything. Its wire shape is the standard
{context, data} datum: readMenu() (src/core/api/handlers/dd_core_api.ts) returns
{result: {context, data}, msg} for the menu read action.
data — the navigation datalist + info
The single data item has shape:
{
"tipo" : "dd85",
"model" : "menu",
"tree_datalist" : [ /* flat array of menu items, see below */ ],
"info_data" : { /* system info for the debug bar */ },
"show_ontology" : true,
"username" : "alex"
}
A tree_datalist item (one per visible area/section):
{
"tipo" : "rsc197",
"model" : "section",
"parent" : "tch188",
"label" : "People",
"config" : { /* present only for special cases, see below */ }
}
parentis the re-parented parent (skip-grouping tipos already removed), so the client builds the tree by grouping items onparent.configis added only for the special-case rewrites:section_toolarea →modelbecomes'section',tipobecomes theproperties->config->target_section_tipo, andconfig = properties->configwith an addedconfig.tool_context(built bybuildSectionToolContext(),src/core/tools/section_tool_context.ts). This is how a tool-backed menu entry opens the real target section but with tool behaviour layered on. If the named tool is not in the user's tool set, the area is skipped.THESAURUS_VIRTUALS_AREA_TIPO(hierarchy56) →modelbecomes'area_thesaurus',config = { swap_tipo: THESAURUS_TIPO }(dd100). The client swapstipotoswap_tipoon click.THESAURUS_VIRTUALS_MODELS_AREA_TIPO(hierarchy57) → same as above plusthesaurus_view_mode: 'model'and a matchingurl_vars.
context — the menu dd_object
The context carries only the menu's identity and its tools (resolved by
the shared buildStructureContext() tool-resolution step, e.g.
tool_user_admin); it has no properties, css or request_config payload,
unlike a section/component context.
{
"label" : "Menu",
"tipo" : "dd85",
"model" : "menu",
"lang" : "lg-eng",
"mode" : "list",
"permissions" : 2,
"tools" : [ /* tool contexts, e.g. tool_user_admin */ ]
}
Files & structure
client/dedalo/core/menu
├── css
│ └── menu.less # styles (compiled into main.css)
└── js
├── menu.js # client widget: init/build/refresh, cache, lang change
├── render_menu.js # view dispatcher (edit) + render_section_label
├── view_default_edit_menu.js # the top bar layout + debug_info_bar + assistant
├── render_menu_tree.js # desktop dropdown tree (hover/click/keyboard)
└── render_menu_mobile.js # collapsible mobile menu
src/core/api/handlers/menu.ts # server: getMenuTreeDatalist() (tree + re-parenting)
src/core/resolve/environment.ts # server: buildInfoData() (debug-bar system info)
src/core/api/handlers/dd_core_api.ts # server: readMenu() (the menu read action)
Server: dispatch (no instance, no constructor)
There is no menu object on the server — getMenuTreeDatalist() is a
plain, stateless async function, called directly from the menu read action:
// src/core/api/handlers/dd_core_api.ts — readMenu()
const menuContext = await buildStructureContext({
tipo: menuTipo, sectionTipo: source.section_tipo ?? menuTipo,
mode: 'list', lang, permissions: 2, addRequestConfig: false,
})
const { tree_datalist } = await getMenuTreeDatalist({
userId: principal.userId,
isGlobalAdmin: principal.isGlobalAdmin,
isDeveloper: principal.isDeveloper,
})
const dataItem = {
tipo: menuTipo, model: 'menu', tree_datalist,
info_data: await buildInfoData(),
show_ontology: principal.isDeveloper,
username: context.session?.username ?? null,
}
No instance cache to clear
The datalist is built fresh on every page bootstrap — nothing is cached server-side. The expensive output (the datalist) is cached on the client in local DB instead.
Client: lifecycle
menu.js follows the standard client element contract (it borrows
render / destroy from common.prototype):
init(options)— setstipo(defaultdd85),model, the datum, and subscribes to the loginquitevent to flush the local cache.build(autoload=true)— when autoloading, issues areadRQO (source = get_data) and caches the API response in local DB underbuild_cache_id()(menu_cache_<lang>_<version>_<user_id>); then pins the instance'scontext/datafrom the returned datum.edit(options)/list(options)— both render thedefaultview (view_default_edit_menu.render). The menu has only adefaultview.refresh(options)— deletes the local cache (whenbuild_autoload) then calls the genericcommon.refresh.delete_cache()— drops everymenu_cache_*local DB entry (all langs).
Public API / Key methods
Server (src/core/api/handlers/menu.ts, src/core/resolve/environment.ts, src/core/api/handlers/dd_core_api.ts)
| function | module | purpose |
|---|---|---|
getMenuTreeDatalist(viewer?) |
api/handlers/menu.ts |
Build the permission-filtered, re-parented array of menu items (areas + sections), applying the section_tool and thesaurus special-case rewrites. viewer carries {userId, isGlobalAdmin, isDeveloper}; admins/developers get the walk unfiltered. |
getVisibleParent(node, skipByTipo) |
api/handlers/menu.ts (private) |
Recursively resolve an item's visible parent by hopping over any ancestor whose tipo is a configured skip tipo (config.menu.skipTipos, overridable at runtime via getEffectiveMenuSkipTipos()). |
buildInfoData() |
resolve/environment.ts |
Assemble the system-info object (Dédalo/runtime/PostgreSQL versions, DB name, entity, server software/IP) for the developer debug bar. |
readMenu(rqo, context, principal) |
api/handlers/dd_core_api.ts |
The menu read action: stamps the context via the shared buildStructureContext() at a hardcoded permission level of 2, then packs tree_datalist + info_data + show_ontology + username into the single data item. |
The context stamp is generic, not overridden
There is only one context builder (buildStructureContext(), used by
every model); readMenu() simply calls it with mode:'list' and
permissions:2 — there is no menu-specific context code path.
Client (menu.js)
| method | purpose |
|---|---|
init(options) |
Initialise instance fields; subscribe to quit → delete_cache. |
build(autoload=true) |
Load+cache the menu datum (RQO read / get_data), then pin context/data. |
edit(options) / list(options) |
Render the default view (top bar + tree). |
refresh(options={}) |
Drop the local cache and re-build. |
build_cache_id(lang?) |
Compose the local DB key menu_cache_<lang>_<version>_<user_id>. |
delete_cache() |
Remove all menu_cache_* local DB entries. |
open_ontology(e) |
Open the v5 ontology editor in a new tab. |
open_tool_user_admin_handler() |
Open tool_user_admin from the context tools (the username button). |
update_section_label(options) |
Replace the menu's current section label (called by section after render); retries up to 3× if the node is not ready. |
change_lang(options) |
Fire the dd_utils_api change_lang action and publish a change_lang event (then the page reloads). |
Client render files
| file / export | purpose |
|---|---|
render_menu.edit / render_section_label |
View dispatcher; builds the empty section-label slot. |
view_default_edit_menu.render |
The full top bar: quit, Dédalo icon, desktop tree, mobile icon, username, language selectors, theme toggle, AI-assistant button, section-label + inspector toggle, and (for developers) the debug_info_bar. A ResizeObserver switches between the desktop tree and the mobile icon on overflow. |
render_menu_tree.render_tree |
Build the desktop dropdown tree from tree_datalist, grouping items by parent (items_by_parent Map) and rendering levels lazily on hover/click. Wires global click / mousedown / Escape handlers to open/close drop menus. |
render_menu_mobile.render_menu |
Build the collapsible mobile menu (recursive render_menu_node). |
How it fits with the rest of Dédalo
- area is the source of the tree roots;
menu.ts's internalgetOntologyAreas()walk enumeratesarea_root / area_activity / area_resource / area_tool / area_thesaurus / area_graph / area_admin / area_maintenance / area_development / area_ontology(minus the effectivemenu.areasDenyconfig) in that fixed order. The menu only filters and flattens that list; it does not share the walk with the (separately-ported)getAreas()inresolve/security_access_datalist.ts. - The
Principalgates decide visibility: non-admin users see the intersection of all areas with their authorized-areas set; Maintenance and Development are extra-gated byisGlobalAdmin/isDeveloper. - Tools ride in two ways: the menu's own tools
(
tool_user_admin, the AI assistant viatool_assistant) come through the sharedbuildStructureContext()tool-resolution step into the context; andsection_toolareas are rewritten into a real section + atool_contextbuilt inline inmenu.ts(buildSectionToolItem()). - Ontology supplies the area/section nodes, labels
(resolved in the interface language) and the special tipos (
dd1,dd85,dd100,hierarchy56,hierarchy57). - Navigation: clicking an item publishes a
user_navigationevent ({source:{tipo, model, mode:'list', config}}) thatpage.jsconsumes to swap the page element — the menu instance itself is preserved across the swap (it is abase_model). - dd_object: the context is a
dd_object, the same normalized shape every element emits.
Examples
Server: build the navigation datalist
// src/core/api/handlers/dd_core_api.ts — readMenu(), on every menu read request
const { tree_datalist } = await getMenuTreeDatalist({
userId: principal.userId,
isGlobalAdmin: principal.isGlobalAdmin,
isDeveloper: principal.isDeveloper,
})
// [
// { tipo:'dd241', model:'area_resource', parent:'dd1', label:'Resources' },
// { tipo:'rsc197', model:'section', parent:'tch188', label:'People' },
// ...
// ]
// the menu context (identity + tools), added to the page context
const menuContext = await buildStructureContext({
tipo: 'dd85', sectionTipo: 'dd1', mode: 'list', lang, permissions: 2, addRequestConfig: false,
})
Client: instantiate and build the menu
// page.js forces a per-user id_variant so different users never share an instance
const instance_options = {
model : 'menu',
tipo : 'dd85',
mode : 'list',
lang : page_globals.dedalo_application_lang,
id_variant : page_globals.user_id // menu-specific, avoids cross-user id clashes
}
const menu_instance = await get_instance(instance_options)
await menu_instance.build(true) // loads + caches the datum, then renders on render()
Why the per-user id_variant
page.js sets instance_options.id_variant = page_globals.user_id for the
menu model, and the client cache key
(menu_cache_<lang>_<version>_<user_id>) is likewise user-scoped. Both
guard against a persistent worker / shared client cache serving one user's
permission-filtered menu to another — the same state-bleed concern the
server caches address with user-id-prefixed keys.
Related
- area — the ontology areas that are the menu's tree roots.
- Architecture overview — the areas → sections → components → data hierarchy the menu navigates.
- Ontology — the active schema that supplies the nodes and labels.
- Tools —
tool_user_admin,tool_assistant, and thesection_toolrewrite path. - dd_object (ddo) — the normalized context object the menu emits.
- Events — the
user_navigation/quit/change_langevents the menu publishes and subscribes to.