Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions content/docs/ai/skills.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ ObjectStack provides **nine domain-specific skills**, one per protocol domain

The authoritative list (names, domains, and "use when / do not use" boundaries) is generated from each skill's `SKILL.md` frontmatter. See the **[AI Skills Reference](/docs/ai/skills-reference)** for the full catalog with per-skill detail.

The same package also carries **`objectui`**, the skill for the `@object-ui/*` React
packages rather than for metadata: rendering JSON schemas with `SchemaRenderer`, writing
renderer plugins, data-source integration, and testing, i18n and mobile layout in a React host.
Its source is [`skills/objectui/`](https://github.com/objectstack-ai/objectstack/tree/main/skills/objectui):
a `SKILL.md` entry point, `rules/` the agent always applies, `guides/` it loads per task, and an
`evals/` prompt per guide that catches regressions in agent behaviour. Its frontmatter defers
data modelling, APIs, automation, formulas and agents to the domain skills above, so the two
coexist in one agent. It installs and updates with the rest of `@objectstack/skills`.

<Callout type="info">
Lifecycle hooks, plugin development, and project bootstrap are **not** separate skills — they live in **Data** (record hooks) and **Platform** (plugins, kernel events, `defineStack`).
</Callout>
Expand Down
353 changes: 353 additions & 0 deletions content/docs/data-modeling/field-types.mdx

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions content/docs/ui/actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,26 @@ strict params gate admits either bag without a word.
ADR-0059), so any `FieldType` works — a `file` param shows a real upload
control (`multiple` / `accept` / `maxSize` honored), `lookup` a record
picker, `date` a date picker, `richtext` the rich-text editor, and so on.
A field-backed param resolves the same way when the action is launched from a
list's selection bar. Values reach the action as the widget emits them
(`number` a number, `date` `YYYY-MM-DD`, a lookup the record id(s), a `file`
the uploaded `sys_file` id(s), arrays when `multiple`), and the dialog's
**Confirm** stays disabled while an upload is in flight. A param's `visible`
is a CEL predicate over `features`, `current_user` and `data` that omits the
param when false.
- **`confirmText`** — confirmation dialog before running.
- **`successMessage` / `errorMessage` / `refreshAfter`** — post-run feedback
and an automatic data refresh.
`successMessage` is a template whose holes are `{{ }}` over `result` (the
handler's answer) and `record` (the record the action ran on):
`'Invitation sent to {{ result.email }}'`; a `${…}` token in it is refused. A
`type: 'api'` or `'script'` action whose handler can succeed in more than one
way adds `outcomeMessages`, one entry per snake_case `outcome` the handler
returns, each of which may read `${result.*}`; the toast shows the entry the
answer names, else `successMessage`, else a generic "Action completed
successfully". A `message` in the server's answer is shown only when the copy
asks for it (`{{ result.message }}`). Post-success navigation is
`onSuccess: { navigate, openIn }`.
- **`undoable`** — on a single-record update, the success toast offers an
**Undo** that restores the record's prior field values (`Ctrl+Z` works too).
The runtime only captures the prior values when this flag is set, so an
Expand Down Expand Up @@ -437,6 +454,24 @@ is a spec proposal for a properly named key, not a values map under this one.
[formulas guide](/docs/data-modeling/formulas) for CEL syntax.
- **`requiresFeature`** ties visibility to a feature flag (compiled into a
`visible` predicate).
- **Relation fields bind as their id.** A `lookup`, `master_detail`, `user` or
`tree` field reads in a predicate as the id the server stores, never as the
record it points at, on every surface (a record page that expanded the
relation for display collapses it again before evaluating). So "only the
record's manager sees this" is `current_user.id == record.manager`;
`record.manager.id` faults, because `record.manager` is a string. An empty
relation is `null`, a clean `false`.
- **`current_user.can(object, verb)`** asks whether the signed-in user holds an
object permission, from the effective permissions the Console already holds
for its built-in Edit and Delete, so a custom action that replaces one of
them can be gated the same way: `visible: "current_user.can('account', 'delete')"`.
The verb comes from the closed `OBJECT_PERMISSION_VERBS` table in
`@objectstack/spec/security`; a verb outside it is a fault, not `false`.
`current_user.hasCapability('<name>')` asks the capability question from the
same payload. Until that payload has loaded both fault, so a `visible` gate
stays closed and a `disabled` gate renders the button disabled until the
answer arrives. In a UI gate they decide only what is shown: the server
still enforces the request the action sends, as `requiredPermissions` does.

### Authorization inside an action

Expand Down
65 changes: 64 additions & 1 deletion content/docs/ui/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ nested `actionDef` block — `actionName` is **not** a top-level key on the item
design** — the action owns its own parameter contract, so the app schema does
not validate what goes inside it.

This is how an app-level action ("Quick Create", "Log a Call") is placed: the item
names a declared action, it does not carry the action's definition. The Console
renders it in the sidebar, and on click looks the name up among the declared
actions and runs it through the same runner as a toolbar button, so confirmations,
parameter dialogs and permission checks apply. An item that names no declared action
shows an error when clicked rather than doing nothing.

### Component Navigation

Renders a registered component. `componentRef` is a component-registry key, not
Expand Down Expand Up @@ -277,6 +284,54 @@ rejected — a divider has nothing to label, gate, or badge.)
| `requiresObject` | `string` | Hide/disable unless the named object is registered |
| `requiresService` | `string` | Hide/disable unless the named kernel service is registered |

`label` is optional. An entry without one shows its target's **current** label,
resolved when the navigation renders: the view's label for an entry that names a
labelled view, else the object's or dashboard's label, else the target's machine
name. A renamed object, view or dashboard therefore shows its new name on the next
load with no navigation edit. Other entries fall back to their target's name
(`pageName`, `reportName`, the `url`) and a group to its `id`, so give those a
`label`. A `label` you write renders verbatim; never write an empty one, omit the
key instead.

## Designing the navigation

The same requirement can often be met three ways: a plain object entry, an object
entry anchored to a named view, or a page that embeds views. They are not
interchangeable. For an app with a `project` object:

| You write | The user lands on | What comes with it |
| :--- | :--- | :--- |
| `{ type: 'object', objectName: 'project' }` | `/apps/my_app/project`, the object's default view | The whole object shell: view switcher, object actions, a create button, record routing, search and recents |
| `… viewName: 'project.by_status'` | `/apps/my_app/project/view/project.by_status` | The same shell, opened on a named view; the user can still switch |
| `… filters: { status: 'open' }` | `/apps/my_app/project/data?filter[status]=open`, the bare data surface | The conditions as removable chips over everything permissions allow, bound to no saved view; **Save as view** turns the slice into a named view |
| `{ type: 'page', pageName: 'project_overview' }` | `/apps/my_app/page/project_overview` | Only the page. View switching, actions and record links are assembled by hand |

A page can imitate the other two, but it loses the object shell, and every later
improvement to that shell (new actions, view switching, permission trimming) skips
it. Use the least powerful construct that expresses the requirement:

1. **One plain `object` entry per core business object**, with no `viewName`: the
default view is the view metadata's business, so navigation stays decoupled from
presentation.
2. **`viewName` when the entry is a named perspective** ("By Status", "Due This
Week"): declare a named view and anchor the entry to it. It sets the entry point;
it does not lock the user in.
3. **`filters` for one-off or parameterized slices** (a drill-through, "assigned to
me"): put the condition in the URL instead of authoring a view, and promote it to
a named view only once it is curated and reused.
4. **A page only for composition one view cannot express**: several objects side by
side, KPI cards beside lists, onboarding content. A page that wraps one object's
one view is an anti-pattern.
5. **A `dashboard` for metrics and charts, a `report` for tabular analysis**, rather
than pages of chart blocks.
6. **One entry per target.** Don't offer an object through both a plain entry and a
page wrapping its default view. If an object needs several entries, make each of
them a named-view entry; mixing styles breaks the sidebar's active-state
highlighting.

In a typical business app most entries are `object` entries; pages are for the home
screen, onboarding and cross-object workbenches.

## Areas

An **area** partitions one app's navigation by business domain — Sales, Service,
Expand Down Expand Up @@ -683,9 +738,17 @@ branding: {
| Property | Type | Description |
| :--- | :--- | :--- |
| `primaryColor` | `string` | Primary brand color (hex) |
| `logo` | `string` | Logo image URL |
| `accentColor` | `string` | Accent color (hex), for highlights and active states |
| `logo` | `string` | Logo image URL: one square app icon, SVG preferred, at least 64 px. Each surface scales it; there is no second size or wordmark slot |
| `favicon` | `string` | Favicon URL |

`branding` is the whole of an app's theming in metadata: the Console maps
`primaryColor` and `accentColor` onto its `--primary` and `--accent` CSS variables.
There is no theme document to author (the standalone `themes` collection was retired
in 17.1). Both the app and `branding` are strict: a top-level `logo` or `theme` is
refused with a pointer to `branding`, and a misspelled key inside `branding` is
refused rather than dropped.

## Complete Example

{/* os:check */}
Expand Down
Loading
Loading