Skip to content

Commit 27b5ac6

Browse files
committed
feat(ui): add row-context actions and bodyShape to Action
1 parent a3ab2d4 commit 27b5ac6

6 files changed

Lines changed: 134 additions & 2 deletions

File tree

apps/cloud/wrangler.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ compatibility_flags = ["nodejs_compat"]
4545
# rebuild + re-push to ship a new version, then run `wrangler deploy`.
4646
[[containers]]
4747
class_name = "CloudContainer"
48-
image = "registry.cloudflare.com/2846eb40a60f4738e292b90dcd8cce10/objectstack-cloud:0b90abfe"
48+
image = "registry.cloudflare.com/2846eb40a60f4738e292b90dcd8cce10/objectstack-cloud:a3ab2d4a"
4949
max_instances = 3
5050
instance_type = "standard-1"
5151

packages/platform-objects/src/identity/sys-invitation.object.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ export const SysInvitation = ObjectSchema.create({
4040
{ field: 'role', required: true },
4141
],
4242
},
43+
{
44+
name: 'cancel_invitation',
45+
label: 'Cancel Invitation',
46+
icon: 'x-circle',
47+
variant: 'danger',
48+
mode: 'delete',
49+
locations: ['list_item'],
50+
type: 'api',
51+
target: '/api/v1/auth/organization/cancel-invitation',
52+
recordIdParam: 'invitationId',
53+
confirmText: 'Cancel this invitation? The recipient will no longer be able to accept it.',
54+
successMessage: 'Invitation canceled',
55+
refreshAfter: true,
56+
},
4357
],
4458

4559
listViews: {

packages/platform-objects/src/identity/sys-member.object.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,42 @@ export const SysMember = ObjectSchema.create({
2020
description: 'Organization membership records',
2121
titleFormat: '{user_id} in {organization_id}',
2222
compactLayout: ['user_id', 'organization_id', 'role'],
23-
23+
24+
// Row-level actions: better-auth `organization/update-member-role` and
25+
// `organization/remove-member`. Generic CRUD is suppressed on better-auth
26+
// managed tables, so these are the canonical edit/delete entry points.
27+
actions: [
28+
{
29+
name: 'update_member_role',
30+
label: 'Change Role',
31+
icon: 'shield',
32+
mode: 'edit',
33+
locations: ['list_item'],
34+
type: 'api',
35+
target: '/api/v1/auth/organization/update-member-role',
36+
recordIdParam: 'memberId',
37+
successMessage: 'Member role updated',
38+
refreshAfter: true,
39+
params: [
40+
{ field: 'role', required: true, defaultFromRow: true },
41+
],
42+
},
43+
{
44+
name: 'remove_member',
45+
label: 'Remove Member',
46+
icon: 'user-minus',
47+
variant: 'danger',
48+
mode: 'delete',
49+
locations: ['list_item'],
50+
type: 'api',
51+
target: '/api/v1/auth/organization/remove-member',
52+
recordIdParam: 'memberIdOrEmail',
53+
confirmText: 'Remove this member from the organization? They will lose access to all org resources.',
54+
successMessage: 'Member removed',
55+
refreshAfter: true,
56+
},
57+
],
58+
2459
fields: {
2560
id: Field.text({
2661
label: 'Member ID',

packages/platform-objects/src/identity/sys-organization.object.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,39 @@ export const SysOrganization = ObjectSchema.create({
4141
{ field: 'logo' },
4242
],
4343
},
44+
{
45+
name: 'update_organization',
46+
label: 'Edit Organization',
47+
icon: 'pencil',
48+
mode: 'edit',
49+
locations: ['list_item'],
50+
type: 'api',
51+
target: '/api/v1/auth/organization/update',
52+
recordIdParam: 'organizationId',
53+
// better-auth `organization/update` nests editable fields under `data`.
54+
bodyShape: { wrap: 'data' },
55+
successMessage: 'Organization updated',
56+
refreshAfter: true,
57+
params: [
58+
{ field: 'name', required: true, defaultFromRow: true },
59+
{ field: 'slug', required: true, defaultFromRow: true },
60+
{ field: 'logo', defaultFromRow: true },
61+
],
62+
},
63+
{
64+
name: 'delete_organization',
65+
label: 'Delete Organization',
66+
icon: 'trash-2',
67+
variant: 'danger',
68+
mode: 'delete',
69+
locations: ['list_item'],
70+
type: 'api',
71+
target: '/api/v1/auth/organization/delete',
72+
recordIdParam: 'organizationId',
73+
confirmText: 'Delete this organization? All members will lose access immediately. This cannot be undone.',
74+
successMessage: 'Organization deleted',
75+
refreshAfter: true,
76+
},
4477
],
4578

4679
listViews: {

packages/platform-objects/src/identity/sys-session.object.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ export const SysSession = ObjectSchema.create({
4444
successMessage: 'All other sessions revoked',
4545
refreshAfter: true,
4646
},
47+
{
48+
name: 'revoke_session',
49+
label: 'Revoke Session',
50+
icon: 'log-out',
51+
variant: 'danger',
52+
mode: 'delete',
53+
locations: ['list_item'],
54+
type: 'api',
55+
target: '/api/v1/auth/revoke-session',
56+
// better-auth `revoke-session` keys off the session token, not the id.
57+
recordIdParam: 'token',
58+
recordIdField: 'token',
59+
confirmText: 'Revoke this session? The user will be signed out from that device.',
60+
successMessage: 'Session revoked',
61+
refreshAfter: true,
62+
},
4763
],
4864

4965
listViews: {

packages/spec/src/ui/action.zod.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ export const ActionParamSchema = lazySchema(() => z.object({
5555
helpText: z.string().optional(),
5656
/** Default value for the dialog input. */
5757
defaultValue: z.unknown().optional(),
58+
/**
59+
* When true, the param's default value is pulled from the current row record
60+
* (key = the resolved field name) when the action runs from a list_item
61+
* context. Useful for edit dialogs that pre-fill from the selected row.
62+
*/
63+
defaultFromRow: z.boolean().optional(),
5864
}).refine(
5965
(p) => Boolean(p.name) || Boolean(p.field),
6066
{ message: 'ActionParam requires either "name" or "field"' },
@@ -190,6 +196,34 @@ export const ActionSchema = lazySchema(() => z.object({
190196
/** Bulk Operations */
191197
bulkEnabled: z.boolean().optional().describe('Whether this action can be applied to multiple selected records'),
192198

199+
/**
200+
* Row-context: when the action runs from a list_item location, this body key
201+
* receives the row's id (or the field named by `recordIdField`). Defaults to
202+
* `id` when omitted but `recordIdField` is set; otherwise no injection.
203+
*/
204+
recordIdParam: z.string().optional().describe('Body key to inject the row id into when running from a list_item context.'),
205+
/**
206+
* Row field whose value seeds `recordIdParam`. Defaults to `'id'` when
207+
* `recordIdParam` is set. Use this when the body key expects a non-id value
208+
* (e.g. `token` for `revoke-session`).
209+
*/
210+
recordIdField: z.string().optional().describe('Row field whose value seeds recordIdParam. Defaults to "id".'),
211+
/**
212+
* Request-body shape. `'flat'` (default) sends collected params at the top
213+
* level. `{ wrap: 'data' }` nests the user-collected params under that key
214+
* (used by better-auth `organization/update`), while `recordIdParam` and
215+
* other top-level keys stay flat.
216+
*/
217+
bodyShape: z.union([
218+
z.literal('flat'),
219+
z.object({ wrap: z.string() }),
220+
]).optional().describe('Body wrapping: flat (default) or { wrap: key } to nest user-collected params under a key.'),
221+
/**
222+
* Semantic mode hint — UI / runtime can use this to pick confirm copy,
223+
* default variants, success messaging. Pure metadata; no runtime branching.
224+
*/
225+
mode: z.enum(['create', 'edit', 'delete', 'custom']).optional().describe('Semantic mode of the action.'),
226+
193227
/** Execution */
194228
timeout: z.number().optional().describe('Maximum execution time in milliseconds for the action'),
195229

0 commit comments

Comments
 (0)