Skip to content

Reference picker

Import MoltaroReferencePickerDialog from @moltaro/ui to reuse the native table, renderers, filters, search, sorting, and paging. The dialog loads pages; it does not download the whole catalog. All API paths below resolve against the configured workspace API, never against moltaro.com.

This component requires a host build that exports it. After upgrading that build, run Check → Build → Activate and fully reload the WebApp. HostContractVersion remains 1, but the host fingerprint has changed. Existing saved projects are not rewritten.

ContextRequired identityWhat selection proves
FormLookupSource entity, saved Form Surface ID, item ID, expected Form revisionThe authoritative field selection endpoint accepted this value in the supplied context
GenericTarget entity; optional table surface keyA protected read could resolve the record at selection time

For FormLookup, obtain the runtime Form through GET /api/workspace/entity/{sourceEntityId}/ui/form, with surfaceKey when needed. Use its Model.Id, Model.RowVersion, and the intended saved item ID. Pass the same optional key as formSurfaceKey. For Update, also provide sourceInstanceId. Supply only the selected item’s permitted user-owned drivers in currentValues; this is not the entire draft.

The dialog verifies the runtime Form and binding, performs lookup preflight, and resolves the target from server metadata. It never accepts a caller-chosen target for this context. Before emitting a choice it calls the authoritative selection route, including the separate Tree route. A stale Form revision blocks selection until the page obtains and passes a fresh context. Do not retry with an empty revision or switch to Generic after an error.

Generic selection is independent of any source field. It does not prove that the record is active or eligible for a form. Saving still runs server authorization and validation. Use an explicit stable purpose per catalog within the page to persist local filters. Without a purpose, local filter persistence is disabled. User, page, target, and surface isolation still apply.

PropMeaning
v-modelWhether the dialog is open
contextTyped WorkspaceUiReferencePickerContext
selectedIdCurrent record ID, or null; the page remains its owner
attachExplicit usePageContext().overlayTarget
titleOptional localized title
disabled / readOnlyFurther restrict interaction; neither grants server permission
clearableDefaults to false; clear is shown only when allowed
refreshKeyChange a string/number to invalidate and reload the context

select emits a WorkspaceUiReferenceSelection containing entityId, id, nullable displayName, and nullable subtitle. It never exposes an internal grid row. The dialog emits select and then closes. Explicit clear emits clear and then closes. Closing without a result emits cancel once. Closing from the parent or unmounting an open dialog also emits cancel once; neither emits cancel after a completed select or clear. Loading, errors, and context refresh never write a value into the parent draft.

For saved Forms, clear is deliberately conservative: statically required fields, or bindings with RequiredWhen, EditableWhen, or VisibleWhen conditions, do not expose it. It also waits for a ready context without pending work or an error. A client flag cannot override requiredness or permissions.

state-change reports loading, nullable error, contextRefreshRequired, and selectedValueState (Empty, Loading, Resolved, Unavailable, or Error). An off-page selection receives a separate bounded protected label read. An unavailable value is not shown as resolved; label hydration is not field eligibility validation. Network errors offer retry. Aborted and superseded requests cannot commit a choice. Changes to context, drivers, selected ID, user, refresh key, closing, and unmount invalidate pending work.

The first public version supports one value, Flat/Tree tables, and readable Projection targets. It explicitly rejects List/LongList, separate Hierarchy input, inverse/multiple bindings, and Table-filter contexts. Creating records or editing manual order is unavailable in this wrapper, including nested reference dialogs; existing native consumers keep their own capabilities.

This component can be placed in a compiled Workspace UI project. Its parent supplies the saved IDs and revision from the runtime Form response and owns the draft values. Replace the props with your page’s actual form state.

<script setup lang="ts">
import { MoltaroReferencePickerDialog, type WorkspaceUiReferencePickerContext, type WorkspaceUiReferenceSelection } from '@moltaro/ui'
import { usePageContext } from '@moltaro/workspace-ui'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { VBtn } from 'vuetify/components'
const props = defineProps<{
sourceEntityId: string
formSurfaceId: string
beneficiaryItemId: string
formRevision: string
sourceInstanceId?: string | null
settlementId: string | null
beneficiaryId: string | null
}>()
const emit = defineEmits<{
(event: 'update:beneficiary-id', value: string | null): void
}>()
const page = usePageContext()
const { t } = useI18n()
const opened = ref(false)
const context = computed<WorkspaceUiReferencePickerContext>(() => ({
kind: 'FormLookup',
sourceEntityId: props.sourceEntityId,
formSurfaceId: props.formSurfaceId,
formItemId: props.beneficiaryItemId,
expectedFormSurfaceRowVersion: props.formRevision,
sourceInstanceId: props.sourceInstanceId,
currentValues: { Settlement: props.settlementId },
}))
function select (value: WorkspaceUiReferenceSelection) {
emit('update:beneficiary-id', value.id)
}
</script>
<template>
<VBtn @click="opened = true">{{ t('assistance.chooseBeneficiary') }}</VBtn>
<MoltaroReferencePickerDialog
v-model="opened"
:attach="page.overlayTarget"
clearable
:context="context"
:selected-id="beneficiaryId"
@clear="emit('update:beneficiary-id', null)"
@select="select"
/>
</template>

Add assistance.chooseBeneficiary to every project locale. Configure this saved item as Lookup and allow Settlement as its eligibility driver. In the parent’s explicit user driver-change handler, clear the dependent beneficiary when the settlement changes. Do not reset it in an unconditional watch of hydrated props: loading a saved record must preserve its saved settlement/beneficiary pair. Keep this cascade reset in one place. The wrapper passes the explicit attachment through its menus and child dialogs; closing/unmounting it closes those owned overlays and discards late callbacks.

For an independent catalog, use:

const context: WorkspaceUiReferencePickerContext = {
kind: 'Generic',
targetEntityId: catalogEntityId,
tableSurfaceKey: 'default',
purpose: 'catalog-browser',
}

Keep that result separate from a Form field unless the Form’s own selection and save path validate it.

After changing a reference, preview the page’s user-owned draft through the existing form-evaluation endpoint. Wait for both an empty FieldErrors and empty LoadingFieldKeys. Treat Patch, ReferencePatchValues, defaults, and effects as server output; do not copy them back into user-origin inputs. Save the intended user inputs with the actual definition/record revision, then read the record again.

Preserve an immutable mutation payload and OperationKey across an unknown network outcome. Resolve it by exact replay before sending another Create. Adopt a successful command’s identity/revision immediately: failure of the following GET is a readback failure, not a failed write. Retry that GET alone. A Receipt is a successful committed write with restricted result visibility. See reliable automation.

Native List still loads at most 200 candidates. LongList still returns at most 25 search results. Their hints report an incomplete result using TotalCount; they do not silently become full catalogs. Choose Lookup in Constructor when users need to browse every permitted page. Existing configurations are not converted automatically.

Verify a choice near the beginning, middle, and end of a catalog larger than 250 records; search, clear, cancel, off-page current selection, and changing a driver while a request is pending. Repeat with an ordinary role and a restricted role, then at 375px with keyboard/Escape. Confirm that filters and menus remain inside the page-owned overlay, focus returns to the opener, and navigation leaves no overlay or scroll lock.