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
15 changes: 15 additions & 0 deletions .changeset/27520-record-page-dates-cards-comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@object-ui/plugin-detail': patch
'@object-ui/app-shell': patch
---

fix(record-page): one date format per record page, appended cards line up with the page's own, and comment text reads in the foreground colour

Clause-②: no
Metadata-protocol: none

Three record-page readability fixes from the 18.3.0 RC walkthrough:

- **One date format.** A `record:details` section row now writes the year on a current-year date, as the highlights strip above it already does. A project page read "End Date Dec 31, 2026" in the strip and "Start Date Oct 2" in a section; both now read with the year. Grid cells keep the face that drops the current year.
- **Aligned cards.** The panels the record page appends after an authored page (the Attachments and Approvals fallbacks, and the "Created … Updated …" footer) take the same horizontal inset as the page body, so they no longer draw wider than the page's own cards.
- **Comment contrast.** A feed entry's body text uses the foreground colour, as a reply's body already did. The actor, the timestamp and the other meta text stay muted.
14 changes: 10 additions & 4 deletions docs/qa/platform-checklist/areas/records-forms.json
Original file line number Diff line number Diff line change
Expand Up @@ -2056,7 +2056,7 @@
"title": "Action buttons surface at exactly their declared locations — list toolbar, list row, record header/more/related/section — and each dispatches for real",
"since": "v15",
"status": "active",
"revision": 17,
"revision": 18,
"priority": "P1",
"surface": "browser",
"personas": [
Expand All @@ -2065,7 +2065,7 @@
"fixtures": {
"app": "showcase",
"requires": [
"the per-location action fleet on showcase_task (examples/app-showcase/src/ui/actions/index.ts): showcase_bulk_reassign (list_item+list_toolbar, flow), showcase_quick_view (list_item, modal), showcase_mark_done (list_item+record_header+record_section, a declarative write on the default script route — operation 'update', patch {done: true, progress: 100} — visible 'has(record.done) && record.done != true'), showcase_log_time (record_header+record_related+record_section, form), showcase_open_docs (record_more, url), showcase_recalc_selection (record_more, api — declares record_more ONLY, which is what keeps it off the toolbar; objectui#3142), showcase_new_task (locations: [] — the fleet's headless specimen, read by the empty-locations probe), showcase_reopen_task (list_item only, operation 'update' patch {done: false}, visible 'has(record.done) && record.done == true' — Mark Done's mirror, so a done task's row menu offers Reopen where a not-done one offers Mark Done; a click on it is refused by the showcase_guard_task_reopen hook by design, which records-forms.script-action-hook-refusal-toast drives)",
"the per-location action fleet on showcase_task (examples/app-showcase/src/ui/actions/index.ts): showcase_bulk_reassign (list_item+list_toolbar, flow), showcase_quick_view (list_item, modal), showcase_mark_done (list_item+record_header, a declarative write on the default script route — operation 'update', patch {done: true, progress: 100} — visible 'has(record.done) && record.done != true'), showcase_log_time (record_header+record_related, form), showcase_open_docs (record_more, url), showcase_recalc_estimate (record_section only, api — the Task Detail quick-actions bar's action), showcase_recalc_selection (record_more, api — declares record_more ONLY, which is what keeps it off the toolbar; objectui#3142), showcase_new_task (locations: [] — the fleet's headless specimen, read by the empty-locations probe), showcase_reopen_task (list_item only, operation 'update' patch {done: false}, visible 'has(record.done) && record.done == true' — Mark Done's mirror, so a done task's row menu offers Reopen where a not-done one offers Mark Done; a click on it is refused by the showcase_guard_task_reopen hook by design, which records-forms.script-action-hook-refusal-toast drives)",
"seeded tasks in both done and not-done states for the CEL visibility both-sides check"
],
"provisioning": {
Expand All @@ -2074,7 +2074,7 @@
},
"knownGaps": [
"NO PIN COVERS A FULL CLAUSE — `automated` was dropped at revision 13, so this P1 item is hand-driven (Tier 2). The two objectui live specs it cited are adjacent: e2e/live/list-row-action-cel.spec.ts opens the first task row's menu and asserts only that at most one Mark Done renders (not both sides of acceptance[3], nor its fail-closed half), and e2e/live/action-modal.spec.ts only that the host's row Edit opens and closes a modal (one dispatch kind of acceptance[2], on no declared action). Tier 1 run #24598 scored every clause not-run on them. Re-add `automated` only with a pin that asserts a full clause.",
"[CLOSED by #22030 (PR #22036, 60ccda5a24), lifted by #22039 — the gap text is rewritten to say where each location now lives] a stock showcase boot hosts all six locations on showcase_task, including the three this gap used to block. record_header and record_more: Task Detail (showcase_task_detail, the record page the console opens for any task), whose first node is a page:header. That header renders the action ids it lists, each placed by its own locations, followed by the host's Edit / Share / Delete. Inline (record_header) on a desktop viewport, where the header's default inline budget is 3 (1 on mobile; a record_header action past the budget folds into ⋯): showcase_mark_done (script, shown only while the task is not done), showcase_log_time (form) and showcase_archive_task (script, disabled until the task is done). Under the ⋯ overflow (record_more without record_header): showcase_open_docs (url), showcase_recalc_estimate (api) and showcase_recalc_selection (api). The Task Detail quick-actions bar (record_section) names its actions explicitly (actionNames: mark_done, log_time and archive_task, in examples/app-showcase/src/ui/pages/task-detail.page.ts), so showcase_recalc_estimate, which declares record_section too, is absent there by page authoring, not a missing placement (run #25817). record_related: Project Detail (showcase_project_detail, the record page for any project) → the Related tab (?tab=related), which holds the Tasks related list, a record:related_list for showcase_task through its project FK; open a project that has tasks. It is an authored node, not the synthesizer's strip: the page's tabs override still replaces that strip, and this tab puts the list back with no actions key, so its rows carry showcase_log_time beside the task's list_item actions, and its header renders the list_toolbar action (Reassign…), as in 18.1 (run #25817): that list's own toolbar placement, not an extra one. The Tasks tab beside it is the record:line_items grid, which hosts no record_related action. An expected action missing from one of these hosts is a missing placement, never blocked(fixture). The hosts were read from objectui source at pin a58626c88dc8 and from the served metadata, not from a browser render, so the first run against them is their first browser evidence",
"[CLOSED by #22030 (PR #22036, 60ccda5a24), lifted by #22039 — the gap text is rewritten to say where each location now lives] a stock showcase boot hosts all six locations on showcase_task, including the three this gap used to block. record_header and record_more: Task Detail (showcase_task_detail, the record page the console opens for any task), whose first node is a page:header. That header renders the action ids it lists, each placed by its own locations, followed by the host's Edit / Share / Delete. Inline (record_header) on a desktop viewport, where the header's default inline budget is 3 (1 on mobile; a record_header action past the budget folds into ⋯): showcase_mark_done (script, shown only while the task is not done), showcase_log_time (form) and showcase_archive_task (script, disabled until the task is done). Under the ⋯ overflow (record_more without record_header): showcase_open_docs (url) and showcase_recalc_selection (api). The Task Detail quick-actions bar (record_section) names the task's record_section set, showcase_recalc_estimate (api), in examples/app-showcase/src/ui/pages/task-detail.page.ts; no action the header draws is also in the bar, so each action renders once on the page (#27520 row 9). record_related: Project Detail (showcase_project_detail, the record page for any project) → the Related tab (?tab=related), which holds the Tasks related list, a record:related_list for showcase_task through its project FK; open a project that has tasks. It is an authored node, not the synthesizer's strip: the page's tabs override still replaces that strip, and this tab puts the list back with no actions key, so its rows carry showcase_log_time beside the task's list_item actions, and its header renders the list_toolbar action (Reassign…), as in 18.1 (run #25817): that list's own toolbar placement, not an extra one. The Tasks tab beside it is the record:line_items grid, which hosts no record_related action. An expected action missing from one of these hosts is a missing placement, never blocked(fixture). The hosts were read from objectui source at pin a58626c88dc8 and from the served metadata, not from a browser render, so the first run against them is their first browser evidence",
"CLOSED by the qaact-placement-specimen recipe (#26625) for acceptance[4] half (a) and acceptance[3]'s fail-closed half. A runtime-authored action is still not joined into the served object's actions[] (QA run #23163 A4: its positive control, the same probe with a location, rendered nowhere), and authoring refuses a throwing `visible` (QA run #26456), so ⛔ do not author either probe at runtime. The recipe installs both specimens through the package loader, whose actions the served object does carry. Score half (a) on qaact_no_locations and the fail-closed half on qaact_visible_throws, each only on a surface where the recipe's qaact_control renders: the list row menu and the Project Detail Related-tab row menus. Task Detail's header, ⋯ overflow and quick-actions bar list their action ids, so the control is absent there and they are no evidence for either half (the recipe's knownGaps[0]). Half (b) stays on showcase_new_task as before."
]
},
Expand All @@ -2084,7 +2084,7 @@
"record_header — showcase_mark_done / showcase_log_time in the detail title bar",
"record_more — showcase_open_docs / showcase_recalc_selection under the ⋯ overflow",
"record_related — showcase_log_time on each row of the related-list section inside a record (the Tasks related list on a showcase_project record), and on no row of the showcase_task list view",
"record_section — showcase_mark_done / showcase_log_time in the Task Detail quick-actions bar (record:quick_actions resolves through the location filter)",
"record_section — showcase_recalc_estimate in the Task Detail quick-actions bar (record:quick_actions resolves through the location filter)",
"empty-locations semantics probe — an action with NO locations key renders on NO surface, and `locations: []` likewise (objectui#3142 collapsed the renderers onto one membership predicate, `actionRendersAt`; placement is a declaration, which is why recalc_selection has to name record_more)"
],
"steps": [
Expand Down Expand Up @@ -2247,6 +2247,12 @@
"date": "2026-10-31",
"change": "fixtures.requires[0], steps[5] and acceptance[3]'s clause name Mark Done's real predicate and list showcase_reopen_task. From QA run #27441 (W3-C3b, at c4c515ad; wave anchor #27377, wave-3 accuracy slice S1). showcase_mark_done's `visible` has been 'has(record.done) && record.done != true' since #8990 (has() guards the sparse list_item face, and != true never faults on a projected null), not '!record.done', and it is a declarative operation 'update' write on the default script route. showcase_reopen_task, the hook-refusal specimen, is its list_item-only mirror ('has(record.done) && record.done == true'), so a done task's row menu shows Reopen where the not-done one shows Mark Done; steps[5] says not to count it. Read at base a0f865491d: examples/app-showcase/src/ui/actions/index.ts. The both-sides contract is unchanged",
"ref": "#27441"
},
{
"revision": 18,
"date": "2026-11-01",
"change": "The Task Detail page draws each task action once (#27520 row 9: the walkthrough saw Mark Done, Log Time and Archive both in the header and as a button row in the body). The quick-actions bar now carries the task's record_section set alone, showcase_recalc_estimate, which declares record_section only, and Mark Done, Log Time and Archive drop record_section. fixtures.requires[0], the record_section variant and knownGaps[1] re-pointed: record_section is still hosted on showcase_task, by Recalculate Estimate in the bar, and the ⋯ overflow keeps open_docs (url) and recalc_selection (api). Read from the examples source at this branch, not a browser run; no clause changed",
"ref": "#27520"
}
],
"enumSource": {
Expand Down
6 changes: 5 additions & 1 deletion examples/app-showcase/src/system/translations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,11 @@ export const ShowcaseTranslationBundle = {
showcase_quick_view: { label: '快速查看' },
showcase_log_time: { label: '记录工时' },
showcase_new_task: { label: '新建任务' },
showcase_archive_task: { label: '归档', successMessage: "{{ record.title | default:'任务' }}已归档(演示——未修改任何数据)。" },
showcase_archive_task: {
label: '归档',
successMessage: "{{ record.title | default:'任务' }}已归档(演示——未修改任何数据)。",
disabledReason: '请先将任务标记为完成。',
},
},
// Section headings of the six form-view projections in
// `ui/views/task.view.ts` (edit / tabbed / wizard / split / quick).
Expand Down
23 changes: 15 additions & 8 deletions examples/app-showcase/src/ui/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,9 @@ export const MarkDoneAction = defineAction({
// still reads an unset `done` as "not done". `has()` alone is therefore the
// whole guard here. Rule: `packages/objectql/src/declared-fields.ts`.
visible: 'has(record.done) && record.done != true',
// `record_section` so the Task Detail page's `record:quick_actions` bar
// (which names this action) resolves it — the engine location-filters even
// explicitly-named actions, mirroring the platform's own sys-user pages.
locations: ['list_item', 'record_header', 'record_section'],
// No `record_section`: the Task Detail header already draws it, and its
// quick-actions bar carries only what the header does not (one action row).
locations: ['list_item', 'record_header'],
refreshAfter: true,
});

Expand Down Expand Up @@ -190,7 +189,11 @@ export const RecalcEstimateAction = defineAction({
execution: 'perRecord',
// Names the task (see MarkDoneAction's note on the hole and its fallback).
successMessage: "Estimate recalculated for {{ record.title | default:'the task' }}.",
locations: ['record_more', 'record_section'],
// `record_section` only: the Task Detail page's `record:quick_actions` bar
// names it, and the engine location-filters even explicitly-named actions.
// Not `record_more` too, so the page draws it once, in that bar, and not
// again under the header's ⋯ overflow.
locations: ['record_section'],
// The endpoint is record-scoped and rejects a body without an id. On a
// record surface the page supplies it; invoked from a LIST ROW (see the
// `legacy_row_actions` view) nothing else can, so name the body key the
Expand Down Expand Up @@ -268,8 +271,7 @@ export const LogTimeAction = defineAction({
// implicitly claims the `default` key), so pointing a form action there opens
// a list as a form — now a build error (#2554).
target: 'showcase_task.form',
// `record_section` so it surfaces in the Task Detail quick-actions bar too.
locations: ['record_header', 'record_related', 'record_section'],
locations: ['record_header', 'record_related'],
refreshAfter: true,
});

Expand Down Expand Up @@ -496,7 +498,12 @@ export const ArchiveTaskAction = defineAction({
// Disabled while the task is not done — visible either way. #8990: `has()`
// for the sparse face; `!=` against a literal cannot fault, so no `!= null`.
disabled: 'has(record.done) && record.done != true',
locations: ['record_header', 'record_section'],
// What the greyed button says while `disabled` holds: why it is
// unavailable and what unlocks it. Without it the console falls back to
// "Not available for this record", which names neither. Display only; it
// never gates the dispatch.
disabledReason: 'Mark the task done first.',
locations: ['record_header'],
refreshAfter: false,
});

Expand Down
15 changes: 8 additions & 7 deletions examples/app-showcase/src/ui/pages/task-detail.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { definePage } from '@objectstack/spec/ui';
* • `record:alert` — a conditional banner shown only while the task is
* In Review (demonstrates the ADR-0089 component-node
* `visibleWhen` predicate, `has()`-guarded).
* • `record:quick_actions` — object Actions surfaced as inline buttons.
* • `record:quick_actions` — object Actions surfaced as inline buttons: the
* task's `record_section` set, which the header
* does not draw, so each action shows once.
* • `record:highlights` + `record:details` — the standard compact + section
* layout.
* `kind: 'full'` — this page fully owns the record layout, as the Project
Expand Down Expand Up @@ -57,9 +59,8 @@ export const TaskDetailPage = definePage({
'showcase_mark_done',
'showcase_log_time',
'showcase_archive_task',
// record_more only — the ⋯ overflow (url, api, api).
// record_more only — the ⋯ overflow (url, api).
'showcase_open_docs',
'showcase_recalc_estimate',
'showcase_recalc_selection',
],
},
Expand Down Expand Up @@ -110,10 +111,10 @@ export const TaskDetailPage = definePage({
properties: {
location: 'record_section',
align: 'start',
// showcase_archive_task is the `disabled`-predicate specimen:
// visible on every task, greyed until `record.done` (contrast with
// showcase_mark_done's `visible`, which HIDES once done).
actionNames: ['showcase_mark_done', 'showcase_log_time', 'showcase_archive_task'],
// The task's `record_section` set, and nothing the header above
// draws: one action row per action, never the same buttons twice.
// Mark Done, Log Time and Archive live in the header only.
actionNames: ['showcase_recalc_estimate'],
},
},
{
Expand Down
Loading
Loading