Pages, components, and stores
A Workspace UI page is a Vue single-file component under src/pages,
registered through UI Studio’s typed Pages registry and served to end users
at /apps/<pageKey> with the runtime page title “Workspace page”. The
registration is stored canonically in workspace-ui.json for ZIP and
Configuration API portability; browser authors do not edit that JSON directly.
Project code
imports its own files through the @workspace-ui alias and talks to the
host only through @moltaro/workspace-ui composables and the
@moltaro/ui design system;
see Project structure
for the manifest and file-tree rules.
Pages and page logic
Section titled “Pages and page logic”The workspace-ui-page and workspace-ui-page-logic source templates
establish the recommended split: the component src/pages/<Name>Page.vue
owns the template and wiring, while a colocated composable
src/pages/use<Name>Page.ts owns derived state, so it stays testable and
the SFC stays thin:
import { usePageContext } from '@moltaro/workspace-ui'import { computed } from 'vue'import { useI18n } from 'vue-i18n'
export function useOrderReviewPage () { const { t } = useI18n() const page = usePageContext() const title = computed(() => page.route.params.instanceId ? t('pages.order-review.recordTitle', { id: page.route.params.instanceId }) : t('pages.order-review.title')) return { title }}Reusable presentation pieces live under src/components as ordinary Vue
components with typed props; they receive data from pages and do not call
host composables themselves.
Pinia stores
Section titled “Pinia stores”Stores live under src/stores and must take their id from
createStoreId(namespace, storeId), which produces
workspace-ui:<namespace>:<id> and keeps project stores isolated from the
host application’s Pinia ids. Both segments must be lowercase kebab-case;
anything else throws at store definition time.
import { createStoreId } from '@moltaro/workspace-ui'import { defineStore } from 'pinia'
export const useOrdersStore = defineStore(createStoreId('orders', 'list'), () => { // state, actions})Sub-routes under the page key
Section titled “Sub-routes under the page key”A manifest page may declare Route.Path — a relative path with no leading
or trailing slash, where literal segments are lowercase kebab-case and
parameter segments are :name with unique names. The full URL is
/apps/<pageKey>/<nested-path>; parameter values arrive in
usePageContext().route.params, the query string in route.query, and the
raw remainder in route.nestedPath.
{ "Key": "order-review", "Component": "src/pages/OrderReviewPage.vue", "TitleKey": "pages.order-review.title", "Layout": "Default", "RequiredPermissions": [], "Route": { "Path": "items/:itemId" }}Navigation
Section titled “Navigation”Pages never touch the host router. useNavigation() exposes
resolveHref(target), open(target, { replace? }), and back() over a
closed target union — the host resolves each target to its own routes:
Home,Catalogs,WorkInbox— no arguments;EntityList(entityDefinitionId),EntityInstance(entityDefinitionId,instanceId),Catalog(catalogDefinitionId);User(userId),Role(roleId);WorkspaceUiPage(pageKey, optionalparamsandquery) — another page of the same project, including itsRoute.Pathparameters.
Localization and formatting
Section titled “Localization and formatting”The project carries five locale files — src/locales/de.json, en.json,
es.json, pl.json, uk.json — read through standard vue-i18n
(destructure t from useI18n()). The manifest TitleKey (by convention
pages.<key>.title) resolves against the same bundles for menu entries and
the page heading. For values, useFormatting() applies workspace-aware
formatting — formatDate, formatTime, formatDateTime, formatDateOnly,
formatTimeOnly, formatMonthYear, formatWeekday, formatDayMonth,
formatNumber — plus reactive locale and timeZone refs that follow the
effective user and workspace settings.
Host composables
Section titled “Host composables”| Composable | Provides |
|---|---|
useMoltaroContext() | Read-only computed user (id, roles, permissions, locale, time zone), workspace (display name, locale, time zone, region), locale, and theme (name, isDark) |
usePageContext() | pageKey, layout (Default or Fullscreen), scopeId, route params/query/nested path, overlayTarget for Vuetify menus, and artifact identity (artifactId, artifactHash, compilationId, sourceChecksum, hostContractVersion) |
usePermissions() / hasPermission, hasAnyPermission, hasAllPermissions | Permission checks against the current user |
useNavigation() | Typed navigation targets, see above |
useNotifications() | notify({ message, severity?, timeoutMs? }) snackbars |
useFormatting() | Workspace-aware date and number formatting |
useDevLog() | info/warning/error author log entries with an event name, message, and bounded scalar properties, surfaced in project diagnostics |
Manifest RequiredPermissions gate menu visibility and client-side
navigation to a page. Server-side authorization stays authoritative: every
Moltaro API call is checked on the server, so pages should still handle
denied and not-found responses.
Example: entity list with server-side paging
Section titled “Example: entity list with server-side paging”The built-in Entity list page generator emits an API client module, a
namespaced store, and a page rendering MoltaroGridBase with server-side
paging through
MoltaroApiClient.
Trimmed excerpt:
import { MoltaroApiClient } from '@moltaro/workspace-ui'
const api = new MoltaroApiClient()export function loadOrdersPage (entity: string, request: unknown, signal: AbortSignal) { return api.requestData<OrdersPage>( '/api/workspace/entity/' + encodeURIComponent(entity) + '/instances/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), signal })}<script setup lang="ts"> import { MoltaroGridBase, type MoltaroGridColumnDefinition } from '@moltaro/ui' import { useDevLog } from '@moltaro/workspace-ui' import { useOrdersStore } from '@workspace-ui/stores/ordersStore' import { computed, ref, watch } from 'vue'
const devLog = useDevLog() const store = useOrdersStore() const entityDefinitionId = '...' const page = ref(1) const columns: MoltaroGridColumnDefinition<Record<string, unknown>>[] = [ { colId: 'name', headerName: 'Name', sortable: false, valueGetter: context => (context.data as OrderRow | undefined)?.Fields['name']?.Value }, ] const pageCount = computed(() => Math.max(1, Math.ceil(store.total / 25))) function reload () { void store.load(entityDefinitionId, () => ({ Page: page.value, PageSize: 25, ArchiveMode: 0, Filter: null, Sort: [], Include: [] }), devLog) } watch(page, reload, { immediate: true })</script><template> <MoltaroGridBase :column-defs="columns" dom-layout="autoHeight" :loading="store.loading" :row-data="store.rows" /> <v-pagination v-model="page" :length="pageCount" /></template>The generated store aborts superseded requests, guards against stale
responses with a sequence number, and reports failures through useDevLog.
A row-click handler completes the flow by calling
navigation.open({ kind: 'EntityInstance', entityDefinitionId, instanceId }).
The instances/query request body is the standard entity query shape —
Page, PageSize, ArchiveMode, Filter, Sort, and Include — and each
returned row is an entity instance with Id, Number, DisplayName, and a
Fields map keyed by field key, where every entry carries State and Value
(so a cell reads row.Fields['name']?.Value). The full request/response
contract, including the paging envelope and the filter/sort grammar, is in the
Integration quickstart and the
Runtime API reference; the built-in
Entity-list and Entity-details page generators emit this data code for you.