ObjectStackObjectStack

record:alert — Conditional Banners on Record Pages

Surface contextual notices (unverified email, expired trial, locked account…) at the top of any record page using the slotted page schema.

record:alert is a UI renderer for slotted record pages. It produces a banner-style notice (think Salesforce Lightning info bar) anchored between the page header and the highlights strip. Use it any time you need to draw the user's attention to a state that needs action — without forcing them to hunt for the relevant control.

Common use cases:

  • Unverified email reminder on the user record page.
  • Expired-trial warning on the org record page.
  • Locked-account banner that explains why the user can't sign in.
  • Compliance disclosure ("This record is governed by SOC2 retention policy").

Schema

{
  type: 'record:alert',
  properties: {
    severity?: 'info' | 'warning' | 'error' | 'success',   // default 'info'
    title?: string,
    body?: string,
    icon?: string,                                          // lucide icon name (overrides severity default)
    visible?: string | { dialect, source } | boolean,       // CEL/template predicate
    action?: {
      actionName: string,                                   // resolves from the object's actions[]
      label?: string,                                       // overrides action.label
      variant?: 'default' | 'destructive' | 'outline' | 'ghost' | 'link',
    },
    dismissible?: boolean,                                  // X button in top-right
    dismissKey?: string,                                    // localStorage key suffix; defaults to title
  }
}

Where it mounts

record:alert is registered as a slot component in the slotted page schema. Add it to the alerts slot of your *.page.ts:

import { definePage } from '@objectstack/spec/ui';

export const SysUserDetailPage = definePage({
  name: 'sys_user_detail',
  label: 'User',
  object: 'sys_user',
  type: 'record',
  kind: 'slotted',   // required: slotted pages override individual slots
  regions: [],
  slots: {
    alerts: [
      {
        type: 'record:alert',
        properties: {
          severity: 'warning',
          icon: 'mail',
          title: '邮箱未验证',
          body: '验证你的邮箱以接收密码重置和重要的系统通知。',
          visible: 'record.id == os.user.id && record.email_verified == false',
          action: {
            actionName: 'resend_verification_email',
            label: '重新发送验证邮件',
          },
          dismissible: false,
        },
      },
    ],
    // …other slots: header, actions, highlights, details, tabs, discussion
  },
});

The alerts slot is rendered between the page header (header/actions) and the highlights strip — the standard enterprise UX placement for contextual notices.

Visibility predicate

The visible predicate uses the same CEL pipeline every condition in the framework does. The evaluation scope (buildScope in @objectstack/formula) binds:

IdentifierSource
recordThe loaded record (e.g. record.email_verified)
os.userThe authenticated user — os.user.id, …
os.orgThe active organization
os.envEnvironment values

Examples:

visible: 'record.id == os.user.id && record.email_verified == false'
visible: 'record.status == "locked"'
visible: 'record.trial_expires_at != null && record.trial_expires_at < now()'

Rules:

  • A missing predicate → always visible.
  • An empty record (page still loading) → hidden, so you never see a flash of stale alert before data arrives.
  • A predicate that throws → hidden (same behavior as action visibility).

CTA wiring

The optional action.actionName resolves against the object's actions[] (the same array that backs record:quick_actions). When clicked, the renderer dispatches the resolved action, which means it inherits the action's declared behavior from the action schema:

  • Confirm dialog (confirmText)
  • Param collection (params: [...])
  • Success toast (successMessage)
  • Refresh after execution (refreshAfter)
  • Result dialog (resultDialog)

…the exact same pipeline as the buttons in the page header. No need to re-declare handlers.

Severity → defaults

SeverityDefault iconTailwind paletteARIA role
infoInfosky bluestatus (polite live region)
warningAlertTriangleamberstatus (polite live region)
successCheckCircle2emeraldstatus (polite live region)
errorAlertCircledestructive redalert (assertive live region)

Override the icon by passing properties.icon with any lucide icon name.

Dismissible alerts

Set dismissible: true to render the X button in the top-right corner. The dismissed state persists in localStorage under:

os.record-alert:<objectName>:<recordId>:<dismissKey || title>

The key is scoped to the (object, record) pair so:

  • Dismissing the unverified-email alert on user A does not silence it for user B.
  • Dismissing it on the current record does not silence the same alert on a different record an admin views afterward.

Pick a stable dismissKey when the title is i18n'd — otherwise translation changes invalidate the dismiss state.

When to choose record:alert vs other primitives

NeedUse
One-line warning at the top of a record pagerecord:alert
Toolbar/menu with multiple related actionsrecord:quick_actions
Read-only status badge in the headerrecord:highlights
Validation error on a specific fieldfield:* errors slot
App-wide notice (across all pages)App-level banner plugin (not metadata-driven)
  • record:quick_actions — action toolbar that pairs naturally with record:alert (the alert nudges the user; the toolbar lets them act).
  • ui/page.zod.ts — the slotted page schema definition, including the alerts slot.

On this page