Returns Portal Extensions
The Returns app exposes portal extension points so other apps can contribute UI into the claim and RMA views. Two kinds of extension points are available:
- Slots — passive insertion points for Vue components. Defined via
apps.defineSlot(id). - Services — typed contracts that providers can implement. Defined via
apps.defineService<Contract>(id).
Other apps consume them with portal.registerView(...), portal.registerService(...), etc. — see Portal Extensions for the general mechanism. This page is a reference for the IDs and contracts the Returns app declares.
Slots
Slots are rendered by the Returns app's views in fixed locations. A slot component receives no host-provided context — any other component context, props, or injected values are an internal implementation detail and may change without notice.
apps/returns/claim/summary
Rendered inside the claim summary panel, beneath the standard claim fields.
const claimSummary = apps.defineSlot('apps/returns/claim/summary')Typical use: an app contributing additional metadata about the claim (e.g. linked external references) into the summary sidebar.
apps/returns/claim/line/resolution
Rendered inside the selected claim line's resolution section, immediately below the resolution dropdown and above the standard custom-fields block.
const claimLineResolution = apps.defineSlot('apps/returns/claim/line/resolution')Typical use: an app contributing UI specific to its own resolution types — preview widgets, helper buttons, validation badges — that complement the registry-driven custom field editors.
apps/returns/rma/summary
Rendered inside the RMA summary panel, beneath the standard RMA fields.
const rmaSummary = apps.defineSlot('apps/returns/rma/summary')apps/returns/rma/line/inspection
Rendered inside the selected RMA line's inspection section.
const rmaLineInspection = apps.defineSlot('apps/returns/rma/line/inspection')Typical use: an app contributing UI used during physical inspection — barcode scanners, condition checklists, photo-capture widgets — that augments the standard quantity-and-factor inputs.
Services
Services are typed contracts. A provider registers an implementation of the contract, and the consuming view calls the contract's methods.
The Returns app declares two services, one per ticket type, both providing inline warnings that appear in the view header. Each provider returns a list of warnings, each consisting of a Vue component to render and a reactive isActive flag the host watches to show/hide the warning.
apps/returns/claimWarningProvider
import { apps } from '@hantera/portal-app'
import { ComputedRef } from 'vue'
interface ClaimWarningProvider {
build(context: ClaimViewContext): {
isActive: ComputedRef<boolean>
component: any // Vue component
}
}
const warningProvider = apps.defineService<ClaimWarningProvider>(
'apps/returns/claimWarningProvider'
)The provider's build method is invoked once when the claim view is mounted. The returned component is rendered inside the claim header whenever isActive.value is true, and unmounted when it transitions back to false. The component receives no props — render any details you need from the same ClaimViewContext you captured in build.
apps/returns/rmaWarningProvider
import { apps } from '@hantera/portal-app'
import { ComputedRef } from 'vue'
interface RmaWarningProvider {
build(context: RmaViewContext): {
isActive: ComputedRef<boolean>
component: any // Vue component
}
}
const rmaWarningProvider = apps.defineService<RmaWarningProvider>(
'apps/returns/rmaWarningProvider'
)Identical shape, applied to the RMA view.
View Contexts
The two warning providers receive a view context that exposes the loaded claim/RMA state and a few mutation helpers. The contexts implement the same general pattern: a reactive state representing the current (possibly previewed) ticket, a buffered-change history with undo/redo, and explicit save / complete / reject / refresh operations that the host invokes from its toolbar.
ClaimViewContext
import { Ref } from 'vue'
import type { Commands, Models } from '@hantera/portal-app'
interface ClaimViewContext {
// === Current ticket state (reactive) ===
// Reflects the latest preview of the claim, including unsaved changes.
readonly state: Claim
// === Identity ===
readonly isNew: boolean
readonly selectedLineId: Ref<string | undefined>
// === Reactive history flags (for toolbar) ===
readonly historyState: {
hasPending: boolean
canUndo: boolean
canRedo: boolean
}
readonly workState: {
isSaving: boolean
isRefreshing: boolean
}
// === Buffered mutations ===
pushPendingChange(
changeId: string | undefined,
commands: Commands.Ticket.TicketCommand[],
messageTemplate: string,
messageDynamic?: Record<string, any>
): void
undo(): boolean
redo(): boolean
// === Server roundtrips ===
refresh(): Promise<void>
save(): Promise<void>
complete(): Promise<void>
reject(rejectReason?: string, rejectMessage?: string): Promise<void>
}
interface Claim {
ticketId: string
ticketNumber: string
channelKey: string
tags: string[]
ticketState: 'open' | 'completed' | 'rejected'
dynamic: Record<string, any>
createdAt: Date
closedAt: Date
createdBy?: { identityId: string; type: string; name?: string }
closedBy?: { identityId: string; type: string; name?: string }
order: {
orderId: string
orderNumber: string
currencyCode: string
orderLines: OrderLine[]
locale?: string
notes?: string
customerNumber?: string
orderTotal: Decimal
}
claimLines: ClaimLine[]
activityLogs: Models.Graph.ActivityLog[]
rejectReason?: string
rejectMessage?: string
isPaused?: boolean
}ClaimLine, OrderLine, and related types follow the field surface documented in Graph.
RmaViewContext
interface RmaViewContext {
readonly state: Rma
readonly isNew: boolean
readonly selectedLineId: Ref<string | undefined>
readonly historyState: {
hasPending: boolean
canUndo: boolean
canRedo: boolean
}
readonly workState: {
isSaving: boolean
isRefreshing: boolean
}
pushPendingChange(
changeId: string | undefined,
commands: Commands.Ticket.TicketCommand[],
messageTemplate: string,
messageDynamic?: Record<string, any>
): void
undo(): boolean
redo(): boolean
refresh(): Promise<void>
save(): Promise<void>
complete(): Promise<void>
}
interface Rma {
ticketId: string
ticketNumber: string
channelKey: string
tags: string[]
ticketState: 'open' | 'completed' | 'rejected'
dynamic: Record<string, any>
createdAt: Date
closedAt: Date
order: {
orderId: string
orderNumber: string
orderLines: OrderLine[]
currencyCode: string
}
claim?: {
ticketId: string
ticketNumber: string
ticketState: 'open' | 'completed' | 'rejected'
}
rmaLines: RmaLine[]
activityLogs: Models.Graph.ActivityLog[]
}Buffered-change model
Both contexts use the same pattern for mutating the underlying ticket:
- Code calls
pushPendingChange(changeId, commands, messageTemplate, messageDynamic). The change is appended to a local history and emitted as anapplyCommandspreview against the actor. stateis refreshed with the previewed result. No data has been written to the actor yet.- The user can
undo/redothrough the history, which re-previews against the actor on each step. - The host's toolbar eventually invokes
save()(writes the buffered commands to the actor and re-queries),complete()(writes plus transitions the ticket tocompleted), orreject()(writes plus transitions torejected).
The optional changeId deduplicates rapid successive edits of the same field — a change pushed with an existing changeId replaces the previous one in the history, keeping the undo stack shallow. Pass undefined for one-off changes that should always create their own undo step.
messageTemplate and messageDynamic together form the human-readable description that's written to the ticket's activity log when save / complete / reject flushes the buffered commands. They follow the standard activity-log template format.
See Also
- Portal Extensions —
apps.defineSlot/apps.defineService/portal.registerServicemechanics. - Returns Graph — Underlying ticket and line graph schema reflected in the view contexts.
- Ticket commands — The
Commands.Ticket.TicketCommandtypes thatpushPendingChangeaccepts.