Skip to content

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.

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.

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
})

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" }
}

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, optional params and query) — another page of the same project, including its Route.Path parameters.

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.

ComposableProvides
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, hasAllPermissionsPermission 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.