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
2 changes: 2 additions & 0 deletions .changeset/12032-approval-requests-data-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ Approval requests can now be read by the standard list and record views, and `Ap
- **A console data source for approval requests.** `apps/console/src/services/approvalRequestsDataSource.ts` routes reads of `sys_approval_request` to the approvals routes through `ApiDataSource`, and everything else to the console's own adapter. Each source serves one server-side scope: awaiting me, submitted by me, or all. Rows come back as the approvals service serves them, so `viewer` and (on a single read) `decision_progress` are fields a view can bind. On a list read, `$top`, `$skip` and `$search` become the route's `limit`, `offset` and `q`, and `$select` is dropped. Every other parameter, such as a filter, a sort or an expansion, is refused with an `UNSUPPORTED_QUERY_PARAM` error, so the list shows its error panel and never shows unfiltered rows. Nothing mounts the source yet; the approvals list and detail pages that will use it come later.

Nothing is added to any package entry: no export, prop, type member or language-pack key.

**Superseded in part (objectui#12045):** the console now mounts this source on the `sys_approval_request` record route, so the sentence above saying nothing mounts it no longer holds when both changes release together.
15 changes: 15 additions & 0 deletions .changeset/12045-approval-decision-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@object-ui/fields': minor
'@object-ui/app-shell': patch
'@object-ui/plugin-detail': patch
'@object-ui/console': patch
---

A pointer field draws the record it points at, the console reads an approval request's record page through the approvals routes, and the approval decision panel is built for the request page (objectui#12045, B1 of objectui#2763).

- **The approval decision panel (`@object-ui/app-shell`, module-internal).** One panel that draws the request's decision progress (`DecisionProgressIndicator`) above the request's own declared decision actions (`DeclaredActionsBar` at `record_section`, decided through `ActionParamDialog`). It reads the bound request and takes no authorable props; outside a `sys_approval_request` record page it renders nothing. After a decision it invalidates the request record and its timeline instead of remounting. It is not registered as a component type yet: the type `record:approval_decision` is proposed to the spec on objectstack-ai/objectstack#22472, and its registration lands with the spec row.
- **`referenceVia` pointer pairs (`@object-ui/fields`).** `resolveRecordPointer(field, row)` returns the record a `text` field declaring `referenceVia` points at on one row (`{ objectName, recordId }`, the new `RecordPointer` type), and `RECORD_POINTER_CARD_TYPE` is the registry key a resolved pair is drawn with. The package's default under that key draws the record id as text, as the field drew before.
- **The record details grid draws the pair (`@object-ui/plugin-detail`).** A field declaring `referenceVia` is resolved from the row and drawn with the pointer face; with `@object-ui/app-shell` loaded that face is the record preview card, so `sys_approval_request.record_id` and the other pointer fields show the record they point at. A pair whose row leaves either half blank draws the stored text.
- **The console's approval request page.** The console's record route for `sys_approval_request` mounts the record page over the routed approvals source, so the request carries the `viewer` block its declared actions gate on and the decision tally. The source now reads one request's `sys_approval_action` timeline from `GET /approvals/requests/:id/actions`, applying the read's `$orderby`, `$top` and `$skip` itself, dropping `$select` and `$expand`, and refusing any other parameter with `UNSUPPORTED_QUERY_PARAM`. On a request's own page the record view no longer asks for approvals opened on the request itself.

No language-pack key is added, and `CellRendererProps` is unchanged.
62 changes: 59 additions & 3 deletions apps/console/src/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
*/

import { lazy, Suspense, useMemo } from 'react';
import { Route, useParams, useLocation, Navigate } from 'react-router-dom';
import { DefaultAppContent, LoadingScreen } from '@object-ui/app-shell';
import { Route, useParams, useLocation, useNavigate, Navigate } from 'react-router-dom';
import { DefaultAppContent, LoadingScreen, RecordDetailView, useAdapter, useMetadata } from '@object-ui/app-shell';
import { MePermissionsProvider } from '@object-ui/permissions';
import { createAuthenticatedFetch } from '@object-ui/auth';
import type { DataSource } from '@object-ui/types';
import { LocalizationFetchProvider } from './LocalizationFetchProvider';
import { WorkspaceTimezonePrompt } from './pages/settings/WorkspaceTimezonePrompt';
import { APPROVAL_REQUEST_OBJECT, createApprovalRequestsDataSource } from './services/approvalRequestsDataSource';

const AppManagementPage = lazy(() => import('./pages/system/AppManagementPage').then(m => ({ default: m.AppManagementPage })));
const ProfilePage = lazy(() => import('./pages/system/ProfilePage').then(m => ({ default: m.ProfilePage })));
Expand Down Expand Up @@ -273,6 +275,57 @@ export const systemRoutes = (
</>
);

/**
* Every `sys_approval_request` record page reads through the approvals routes
* (objectui#12045, B1 of objectui#2763).
*
* The generic record route (`:objectName/record/:recordId`, declared by
* `DefaultAppContent` in `@object-ui/app-shell`) hands `RecordDetailView` the
* console's adapter, the data API. For an approval request that read is the
* wrong door: the data API attaches neither the `viewer` block the request's
* declared decision actions gate on nor the `decision_progress` tally, and it
* refuses the request's timeline (`sys_approval_action`) to an ordinary
* approver. The approvals routes answer all three for anyone who is a party to
* the request. So this route, more specific than the generic one and therefore
* the one React Router picks for this object, mounts the same view over the
* routed approvals source (`createApprovalRequestsDataSource`): the request is
* read from `GET /approvals/requests/:id`, its timeline from
* `GET /approvals/requests/:id/actions`, and every other read the page makes
* goes to the adapter unchanged.
*
* The page lists no requests of its own; the source's `all` scope only means
* one would be read within the approvals service's own visibility. Records of
* this object are engine-owned, so the page offers no edit; the handler opens
* the record's edit route, which is where the generic route's page-mode edit
* goes.
*/
function ApprovalRequestRecordRoute() {
const adapter = useAdapter();
const { objects } = useMetadata();
const navigate = useNavigate();
const dataSource = useMemo<DataSource | null>(
() =>
adapter
? createApprovalRequestsDataSource({ host: adapter, scope: { kind: 'all' } })
: null,
[adapter],
);
return (
<RecordDetailView
dataSource={dataSource}
objects={objects}
objectNameOverride={APPROVAL_REQUEST_OBJECT}
onEdit={() => navigate('edit')}
/>
);
}

// Exported for `services/approvalRequestsDataSource.recordRoute.test.tsx`,
// which mounts this exact fragment beside the generic record route.
export const approvalRequestRoutes = (
<Route path={`${APPROVAL_REQUEST_OBJECT}/record/:recordId`} element={<ApprovalRequestRecordRoute />} />
);

export function AppContent() {
const serverUrl = import.meta.env.VITE_SERVER_URL || '';
const endpoint = `${serverUrl}/api/v1/auth/me/permissions`;
Expand Down Expand Up @@ -308,7 +361,10 @@ export function AppContent() {
errorFallback={(err, retry) => <LoadingScreen error={err.message} onRetry={retry} />}
>
<LocalizationFetchProvider endpoint={localizationEndpoint}>
<DefaultAppContent extraRoutes={systemRoutes} extraRoutesNoApp={systemRoutes} />
<DefaultAppContent
extraRoutes={<>{systemRoutes}{approvalRequestRoutes}</>}
extraRoutesNoApp={systemRoutes}
/>
</LocalizationFetchProvider>
{/* objectui#11758 — asks an administrator once to set a still-default
workspace timezone. Here, inside `MePermissionsProvider`, because
Expand Down
Loading
Loading