Skip to content

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.

typescript
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.

typescript
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.

typescript
const rmaSummary = apps.defineSlot('apps/returns/rma/summary')

apps/returns/rma/line/inspection

Rendered inside the selected RMA line's inspection section.

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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:

  1. Code calls pushPendingChange(changeId, commands, messageTemplate, messageDynamic). The change is appended to a local history and emitted as an applyCommands preview against the actor.
  2. state is refreshed with the previewed result. No data has been written to the actor yet.
  3. The user can undo / redo through the history, which re-previews against the actor on each step.
  4. The host's toolbar eventually invokes save() (writes the buffered commands to the actor and re-queries), complete() (writes plus transitions the ticket to completed), or reject() (writes plus transitions to rejected).

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 Extensionsapps.defineSlot / apps.defineService / portal.registerService mechanics.
  • Returns Graph — Underlying ticket and line graph schema reflected in the view contexts.
  • Ticket commands — The Commands.Ticket.TicketCommand types that pushPendingChange accepts.

© 2024 Hantera AB. All rights reserved.