@abinnovision/payloadcms-mcpx 2.0.0-beta.13 → 2.0.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +50 -35
  2. package/dist/admin/capability-matrix.d.mts +4 -3
  3. package/dist/admin/capability-matrix.mjs +262 -111
  4. package/dist/admin/capability-toggles.mjs +84 -40
  5. package/dist/admin/confirmations-panel.mjs +242 -0
  6. package/dist/admin/confirmations.d.mts +15 -0
  7. package/dist/admin/confirmations.mjs +70 -0
  8. package/dist/admin/index.d.mts +2 -1
  9. package/dist/admin/index.mjs +2 -1
  10. package/dist/api-keys/capability-matrix.d.mts +5 -4
  11. package/dist/api-keys/capability-matrix.mjs +33 -12
  12. package/dist/api-keys/collection.mjs +3 -3
  13. package/dist/api-keys/confirmation-view.mjs +6 -0
  14. package/dist/api-keys/fields.mjs +20 -3
  15. package/dist/builtin-tool-names.mjs +3 -1
  16. package/dist/capabilities.mjs +9 -4
  17. package/dist/define-tool.d.mts +4 -4
  18. package/dist/define-tool.mjs +8 -1
  19. package/dist/endpoint/confirmations.mjs +124 -0
  20. package/dist/endpoint/handler.mjs +2 -2
  21. package/dist/endpoint/server.mjs +22 -18
  22. package/dist/endpoint/upload.mjs +5 -4
  23. package/dist/grants/confirmation.mjs +106 -0
  24. package/dist/{upload → grants}/grant.mjs +39 -16
  25. package/dist/options.mjs +32 -6
  26. package/dist/plugin.d.mts +2 -1
  27. package/dist/plugin.mjs +5 -2
  28. package/dist/tools/builtin.mjs +5 -1
  29. package/dist/tools/create-document.mjs +1 -1
  30. package/dist/tools/delete-document.mjs +157 -0
  31. package/dist/tools/find-versions.mjs +1 -1
  32. package/dist/tools/get-document.mjs +1 -1
  33. package/dist/tools/list-capabilities.mjs +2 -2
  34. package/dist/tools/patch-document.mjs +2 -2
  35. package/dist/tools/publish-document.mjs +1 -1
  36. package/dist/tools/run-confirmed.mjs +126 -0
  37. package/dist/tools/shared.mjs +4 -0
  38. package/dist/tools/validate-document.mjs +2 -2
  39. package/dist/types.d.mts +130 -19
  40. package/dist/upload/file.mjs +3 -3
  41. package/dist/write/draft-guard.mjs +11 -9
  42. package/dist/write/publish-intent.mjs +16 -6
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -61,7 +61,7 @@ The plugin adds:
61
61
  - An `mcpx-api-keys` collection in the admin group "MCP", holding the keys.
62
62
  - A draft guard on every collection and global that turns MCP writes into draft saves.
63
63
 
64
- The key form uses two admin components, so regenerate the import map:
64
+ The key form uses two admin components, three with `delete`, so regenerate the import map:
65
65
 
66
66
  ```sh
67
67
  payload generate:importmap
@@ -86,24 +86,25 @@ Claude Desktop and the MCP Inspector are covered in
86
86
 
87
87
  ## Options
88
88
 
89
- | Option | Default | Description |
90
- | ---------------------------- | ----------------------------------------- | ------------------------------------------------------------------- |
91
- | `collections` | required | Collections to expose. `true` means `{}`, everything it supports. |
92
- | `collections.<slug>.read` | `true` | Expose the read tools for this collection. |
93
- | `collections.<slug>.write` | `true` | Expose the write tools. Without drafts, a write goes live. |
94
- | `collections.<slug>.publish` | `true` with drafts | Expose `publishDocument`. Needs `versions.drafts` and `write`. |
95
- | `globals` | `{}` | Globals to expose, with the same three options. |
96
- | `userCollection` | `config.admin.user`, then `users` | Auth collection whose users the keys act as. |
97
- | `apiKeys.slug` | `mcpx-api-keys` | Slug of the key collection. |
98
- | `apiKeys.setupGuide` | `true` | Add the "Connect a client" tab to saved keys. |
99
- | `apiKeys.overrideCollection` | none | Function that receives the key collection and returns it. |
100
- | `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
101
- | `limits.maxLimit` | `25` | Highest `limit` a client may pass to a list tool. |
102
- | `limits.maxDepth` | `1` | Highest `depth` a client may pass to a read tool. |
103
- | `tools` | `[]` | Custom tools, see [`docs/custom-tools.md`](./docs/custom-tools.md). |
104
- | `auth.resolve` | none | Replace or wrap the key lookup. |
105
- | `serverInfo` | `payloadcms-mcpx` and the package version | `{ name, version }` reported to clients. |
106
- | `diagnostics` | `true` | Expose the server version plus repository and issue links. |
89
+ | Option | Default | Description |
90
+ | ---------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `collections` | required | Collections to expose. `true` means `{}`, everything it supports. |
92
+ | `collections.<slug>.read` | `true` | Expose the read tools for this collection. |
93
+ | `collections.<slug>.write` | `true` | Expose the write tools. Needs `read`. Without drafts, a write goes live. |
94
+ | `collections.<slug>.publish` | `true` with drafts | Expose `publishDocument`. Needs `versions.drafts` and `write`. |
95
+ | `collections.<slug>.delete` | `false` | Expose `deleteDocument`. Needs `read`. Each delete is approved in the admin. `"unattended"` also offers trashing without approval; needs `trash`. |
96
+ | `globals` | `{}` | Globals to expose, with `read`, `write` and `publish`. |
97
+ | `userCollection` | `config.admin.user`, then `users` | Auth collection whose users the keys act as. |
98
+ | `apiKeys.slug` | `mcpx-api-keys` | Slug of the key collection. |
99
+ | `apiKeys.setupGuide` | `true` | Add the "Connect a client" tab to saved keys. |
100
+ | `apiKeys.overrideCollection` | none | Function that receives the key collection and returns it. |
101
+ | `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
102
+ | `limits.maxLimit` | `25` | Highest `limit` a client may pass to a list tool. |
103
+ | `limits.maxDepth` | `1` | Highest `depth` a client may pass to a read tool. |
104
+ | `tools` | `[]` | Custom tools, see [`docs/custom-tools.md`](./docs/custom-tools.md). |
105
+ | `auth.resolve` | none | Replace or wrap the key lookup. |
106
+ | `serverInfo` | `payloadcms-mcpx` and the package version | `{ name, version }` reported to clients. |
107
+ | `diagnostics` | `true` | Expose the server version plus repository and issue links. |
107
108
 
108
109
  The config only takes capabilities away: `true` exposes everything the entity supports, and a key's
109
110
  checkboxes decide what each key may do. `publish: false` keeps MCP writes as drafts. On an entity
@@ -115,17 +116,19 @@ every check.
115
116
 
116
117
  ## Tools
117
118
 
118
- | Tool | What it does | Needs |
119
- | ------------------ | ------------------------------------------------------------------ | ------------------ |
120
- | `listCapabilities` | Lists what this key may do. Clients call it first. | any key |
121
- | `describeSchema` | Describes the fields at one schema path, with the paths below it. | `read` |
122
- | `findDocuments` | Queries a collection with a Payload `where`, `sort` and `select`. | `read` |
123
- | `getDocument` | Reads a document, a subtree of it, an old version or a diff. | `read` |
124
- | `findVersions` | Lists the version history of a document or global, without bodies. | `read` |
125
- | `patchDocument` | Applies JSON Patch operations to the current draft, or a new file. | `write` |
126
- | `createDocument` | Creates a draft from a seed, with a file in an upload collection. | `write` |
127
- | `validateDocument` | Lists what blocks publishing, without saving. | `write` |
128
- | `publishDocument` | Publishes the current draft. | `write`, `publish` |
119
+ | Tool | What it does | Needs |
120
+ | ------------------ | ------------------------------------------------------------------ | --------- |
121
+ | `listCapabilities` | Lists what this key may do. Clients call it first. | any key |
122
+ | `describeSchema` | Describes the fields at one schema path, with the paths below it. | `read` |
123
+ | `findDocuments` | Queries a collection with a Payload `where`, `sort` and `select`. | `read` |
124
+ | `getDocument` | Reads a document, a subtree of it, an old version or a diff. | `read` |
125
+ | `findVersions` | Lists the version history of a document or global, without bodies. | `read` |
126
+ | `patchDocument` | Applies JSON Patch operations to the current draft, or a new file. | `write` |
127
+ | `createDocument` | Creates a draft from a seed, with a file in an upload collection. | `write` |
128
+ | `validateDocument` | Lists what blocks publishing, without saving. | `write` |
129
+ | `publishDocument` | Publishes the current draft. | `publish` |
130
+ | `deleteDocument` | Asks to delete a document, which the key's user approves. | `delete` |
131
+ | `runConfirmed` | Runs the approved calls. | `delete` |
129
132
 
130
133
  Collections and globals use the same tools. A tool that addresses one document takes either
131
134
  `collection` and `id`, or `global` alone. [`docs/concepts.md`](./docs/concepts.md#tools) lists
@@ -138,11 +141,20 @@ A key can do something only when both the config and the key allow it.
138
141
  The plugin config sets the upper bound: which collections and globals are exposed, and whether
139
142
  each one is readable, writable and publishable. A key cannot go past it.
140
143
 
141
- Each key carries one checkbox per exposed entity and operation (`read`, `write`, `publish`), and
142
- one per custom tool. The admin panel shows them as a matrix with a row per entity and a column per
143
- operation. A cell the config does not expose shows a dash. `publish` exists only where the config
144
- exposes `write` and `publish` on an entity with drafts, and counts only when `write` is ticked as
145
- well. A write checkbox on an entity without drafts is marked "Writes go live immediately."
144
+ Each key carries one checkbox per exposed entity and operation (`read`, `write`, `publish`,
145
+ `delete`, `deleteUnattended`), and
146
+ one per custom tool. The admin panel shows them as a matrix with a row per entity, an Access
147
+ control (None, Read, Write, Publish) and a Delete control (Off, Approval, Trash). Each access level
148
+ includes the ones before it, and a level the config does not expose has no segment. `publish`
149
+ exists only where the config exposes `write` and `publish` on an entity with drafts, and counts
150
+ only when `write` is ticked as well. `write` counts only when `read` is ticked, so a key never
151
+ writes what it cannot read. An entity without drafts carries a "Live" badge, as its writes go live
152
+ immediately.
153
+ `delete` exists only where the config sets `delete: true` on a collection, and counts only when
154
+ `read` is ticked as well. Setting access to None clears it. A delete runs only after the key's user
155
+ approved it on the key's edit view, except where the config sets `delete: "unattended"` and the
156
+ key's Delete control is set to Trash: there the document moves to trash at once. See
157
+ [deleting](./docs/concepts.md#deleting).
146
158
 
147
159
  `tools/list` follows the key. A tool the key cannot use is not listed, and each `collection` and
148
160
  `global` argument lists only the slugs the key may use with that tool. A checkbox added by a later
@@ -167,6 +179,9 @@ To restrict that, use `apiKeys.overrideCollection` as shown in
167
179
  - A file is sent in a second step, a `PUT`, and downloaded with a `GET`, each with a short-lived,
168
180
  single-use grant. Only collections with `upload.mimeTypes` take files. Downloads go through the
169
181
  collection's own file access. See [uploads](./docs/security.md#uploads).
182
+ - A delete waits for the key's user to approve it in the admin panel and is then run by the same
183
+ key. Collections with `trash` move the document to trash. See
184
+ [confirmations](./docs/security.md#confirmations).
170
185
  - Custom tools are trusted code and must apply access control themselves.
171
186
 
172
187
  Open limitations: a filter through a relation into a readable collection ignores that collection's
@@ -25,9 +25,10 @@ interface McpxCapabilityMatrixProps {
25
25
  withinTab?: boolean;
26
26
  }
27
27
  /**
28
- * What an API key may do, as one table per namespace: a row per entity, a
29
- * column per operation and a bulk toggle in each column header. The nested
30
- * checkbox fields back every cell, so the stored document is unaffected.
28
+ * What an API key may do, as one table per namespace: a row per entity with
29
+ * segmented Access and Delete controls, and an "All" row that sets every row.
30
+ * The nested checkbox fields back every control, so the stored document is
31
+ * unaffected.
31
32
  */
32
33
  export declare const McpxCapabilityMatrix: React.FC<McpxCapabilityMatrixProps>;
33
34
  //#endregion
@@ -1,17 +1,28 @@
1
1
  "use client";
2
2
  import { CAPABILITIES_FIELD } from "../capabilities.mjs";
3
- import { CAPABILITY_OPERATIONS, capabilityPaths, cellPath, toolPath } from "../api-keys/capability-matrix.mjs";
4
- import { buildToggleActions, columnState, rowState, toolsState } from "./capability-toggles.mjs";
3
+ import { CAPABILITY_OPERATIONS, STORED_OPERATIONS, capabilityPaths, toolPath } from "../api-keys/capability-matrix.mjs";
4
+ import { accessLevelOf, accessLevelsOf, allAccessLevel, allDeleteMode, buildToggleActions, deleteModeOf, toolsState } from "./capability-toggles.mjs";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
- import { CheckboxInput, FieldDescription, FieldLabel, useForm, useFormFields } from "@payloadcms/ui";
6
+ import { Button, CheckboxInput, FieldDescription, FieldLabel, Pill, useForm, useFormFields } from "@payloadcms/ui";
7
7
  import { useCallback, useMemo } from "react";
8
8
  //#region src/admin/capability-matrix.tsx
9
- const NOT_EXPOSED = "The plugin config does not expose this.";
10
9
  const BASE_CLASS = "mcpx-capabilities";
10
+ const LIVE_WRITE = "Writes go live immediately.";
11
11
  const SHEET = `
12
12
  .${BASE_CLASS} tbody tr:hover { background: var(--theme-elevation-50); }
13
- .${BASE_CLASS}__toggle:hover { text-decoration: underline; }
14
13
  .${BASE_CLASS} section:last-of-type { margin-bottom: 0; }
14
+ .${BASE_CLASS}__segments {
15
+ display: inline-flex;
16
+ border: 1px solid var(--theme-elevation-150);
17
+ border-radius: var(--style-radius-s);
18
+ background: var(--theme-input-bg);
19
+ }
20
+ .${BASE_CLASS}__segments .btn { border: 0; border-radius: 0; white-space: nowrap; }
21
+ .${BASE_CLASS}__segments .btn + .btn { border-inline-start: 1px solid var(--theme-elevation-150); }
22
+ .${BASE_CLASS}__segments .btn:first-child { border-start-start-radius: inherit; border-end-start-radius: inherit; }
23
+ .${BASE_CLASS}__segments .btn:last-child { border-start-end-radius: inherit; border-end-end-radius: inherit; }
24
+ .${BASE_CLASS}__segments .btn--style-transparent { --hover-bg: var(--theme-elevation-100); }
25
+ .${BASE_CLASS}__segments .${BASE_CLASS}__danger { --bg-color: var(--theme-error-500); --hover-bg: var(--theme-error-600); }
15
26
  `;
16
27
  const CELL_PADDING = "calc(var(--base) * 0.6)";
17
28
  const EDGE_PADDING = "calc(var(--base) * 0.8)";
@@ -57,30 +68,35 @@ const styles = {
57
68
  fontWeight: "normal",
58
69
  wordBreak: "break-word"
59
70
  },
60
- rowButton: {
61
- padding: 0,
62
- border: 0,
63
- background: "none",
64
- color: "inherit",
65
- font: "inherit",
66
- cursor: "pointer",
67
- textAlign: "start",
68
- textDecoration: "none"
69
- },
70
71
  hint: {
71
72
  display: "block",
72
73
  color: "var(--theme-elevation-400)"
73
74
  },
75
+ badge: {
76
+ display: "inline-flex",
77
+ marginInlineStart: "calc(var(--base) * 0.4)",
78
+ verticalAlign: "middle",
79
+ cursor: "help"
80
+ },
81
+ visuallyHidden: {
82
+ position: "absolute",
83
+ width: "1px",
84
+ height: "1px",
85
+ overflow: "hidden",
86
+ clip: "rect(0 0 0 0)",
87
+ whiteSpace: "nowrap"
88
+ },
74
89
  cell: {
75
90
  padding: CELL_PADDING,
76
91
  verticalAlign: "middle",
77
92
  textAlign: "center"
78
93
  },
79
- lastCell: { paddingInlineEnd: EDGE_PADDING },
80
- dash: {
81
- color: "var(--theme-elevation-400)",
82
- cursor: "help"
83
- }
94
+ controlCell: {
95
+ padding: CELL_PADDING,
96
+ verticalAlign: "middle",
97
+ textAlign: "start"
98
+ },
99
+ lastCell: { paddingInlineEnd: EDGE_PADDING }
84
100
  };
85
101
  const Box = ({ label, onChange, readOnly, state }) => /*#__PURE__*/ jsx(CheckboxInput, {
86
102
  checked: state === "on",
@@ -89,116 +105,250 @@ const Box = ({ label, onChange, readOnly, state }) => /*#__PURE__*/ jsx(Checkbox
89
105
  partialChecked: state === "mixed",
90
106
  readOnly
91
107
  });
108
+ const descriptionOf = (operation) => STORED_OPERATIONS.find((candidate) => candidate.id === operation)?.description;
109
+ const ACCESS_OPTIONS = [{
110
+ id: "none",
111
+ label: "None",
112
+ title: "No access"
113
+ }, ...CAPABILITY_OPERATIONS.filter((operation) => operation.id !== "delete").map((operation) => ({
114
+ id: operation.id,
115
+ label: operation.label,
116
+ title: operation.description
117
+ }))];
118
+ const DELETE_OPTIONS = [
119
+ {
120
+ id: "off",
121
+ label: "Off",
122
+ title: "No delete"
123
+ },
124
+ {
125
+ id: "approve",
126
+ label: "Approval",
127
+ title: descriptionOf("delete")
128
+ },
129
+ {
130
+ id: "trash",
131
+ label: "Trash",
132
+ title: descriptionOf("deleteUnattended")
133
+ }
134
+ ];
135
+ const ARROW_STEPS = {
136
+ ArrowDown: 1,
137
+ ArrowLeft: -1,
138
+ ArrowRight: 1,
139
+ ArrowUp: -1
140
+ };
141
+ const Segments = ({ included = [], label, onSelect, options, readOnly, selected }) => {
142
+ const focusable = options.some((option) => option.id === selected) ? selected : options[0]?.id;
143
+ const onKeyDown = (event) => {
144
+ const step = ARROW_STEPS[event.key];
145
+ if (readOnly || step === void 0) return;
146
+ event.preventDefault();
147
+ const radios = [...event.currentTarget.querySelectorAll("[role=\"radio\"]")];
148
+ const current = radios.findIndex((radio) => radio === document.activeElement);
149
+ const next = Math.min(Math.max(current + step, 0), radios.length - 1);
150
+ const option = options[next];
151
+ if (option) {
152
+ radios[next]?.focus();
153
+ onSelect(option.id);
154
+ }
155
+ };
156
+ return /*#__PURE__*/ jsx("div", {
157
+ "aria-label": label,
158
+ className: `${BASE_CLASS}__segments`,
159
+ onKeyDown,
160
+ role: "radiogroup",
161
+ children: options.map((option) => {
162
+ const checked = option.id === selected;
163
+ return /*#__PURE__*/ jsx(Button, {
164
+ buttonStyle: checked ? "primary" : included.includes(option.id) ? "pill" : "transparent",
165
+ className: checked && option.id === "trash" ? `${BASE_CLASS}__danger` : "",
166
+ disabled: readOnly,
167
+ extraButtonProps: {
168
+ "aria-checked": checked,
169
+ role: "radio",
170
+ tabIndex: option.id === focusable ? 0 : -1,
171
+ title: option.title
172
+ },
173
+ margin: false,
174
+ onClick: () => {
175
+ onSelect(option.id);
176
+ },
177
+ size: "small",
178
+ children: option.label
179
+ }, option.id);
180
+ })
181
+ });
182
+ };
183
+ const includedBelow = (options, level) => {
184
+ const selected = options.findIndex((option) => option.id === level);
185
+ return selected < 1 ? [] : options.slice(1, selected).map((option) => option.id);
186
+ };
187
+ const accessOptions = (rows) => ACCESS_OPTIONS.filter((option) => rows.some((row) => accessLevelsOf(row).includes(option.id)));
92
188
  const LABEL_WIDTH = "calc(var(--base) * 9)";
93
189
  const OPERATION_WIDTH = "calc(var(--base) * 3.5)";
94
- const Columns = ({ count }) => /*#__PURE__*/ jsxs("colgroup", { children: [/*#__PURE__*/ jsx("col", { style: { width: LABEL_WIDTH } }), Array.from({ length: count }, (_, index) => /*#__PURE__*/ jsx("col", { style: { width: OPERATION_WIDTH } }, index))] });
95
- const cellStyle = (isLast) => isLast ? {
190
+ const ACCESS_WIDTH = "calc(var(--base) * 13)";
191
+ const DELETE_WIDTH = "calc(var(--base) * 10)";
192
+ const Columns = ({ widths }) => /*#__PURE__*/ jsxs("colgroup", { children: [/*#__PURE__*/ jsx("col", { style: { width: LABEL_WIDTH } }), widths.map((width, index) => /*#__PURE__*/ jsx("col", { style: { width } }, index))] });
193
+ const lastCellStyle = {
96
194
  ...styles.cell,
97
195
  ...styles.lastCell
98
- } : styles.cell;
99
- const Dash = () => /*#__PURE__*/ jsx("span", {
100
- "aria-label": NOT_EXPOSED,
101
- style: styles.dash,
102
- title: NOT_EXPOSED,
103
- children: "—"
196
+ };
197
+ const controlStyle = (isLast) => isLast ? {
198
+ ...styles.controlCell,
199
+ ...styles.lastCell
200
+ } : styles.controlCell;
201
+ const LiveBadge = () => /*#__PURE__*/ jsx("span", {
202
+ style: styles.badge,
203
+ title: LIVE_WRITE,
204
+ children: /*#__PURE__*/ jsxs(Pill, {
205
+ pillStyle: "warning",
206
+ size: "small",
207
+ children: [/*#__PURE__*/ jsx("span", {
208
+ "aria-hidden": "true",
209
+ children: "Live"
210
+ }), /*#__PURE__*/ jsx("span", {
211
+ style: styles.visuallyHidden,
212
+ children: LIVE_WRITE
213
+ })]
214
+ })
104
215
  });
105
216
  const NamespaceTable = ({ matrix, namespace, path, readOnly, toggle, values }) => {
106
217
  const rows = matrix[namespace.id];
107
- const columns = CAPABILITY_OPERATIONS.filter((operation) => rows.some((row) => row[operation.id]));
218
+ const withDelete = rows.some((row) => row.delete);
219
+ const allOptions = accessOptions(rows);
220
+ const allLevel = allAccessLevel(matrix, path, namespace.id, values);
221
+ const entries = namespace.title.toLowerCase();
108
222
  return /*#__PURE__*/ jsx("section", {
109
223
  style: styles.section,
110
224
  children: /*#__PURE__*/ jsxs("table", {
111
225
  style: styles.table,
112
226
  children: [
113
- /*#__PURE__*/ jsx(Columns, { count: columns.length }),
227
+ /*#__PURE__*/ jsx(Columns, { widths: withDelete ? [ACCESS_WIDTH, DELETE_WIDTH] : [ACCESS_WIDTH] }),
114
228
  /*#__PURE__*/ jsxs("thead", {
115
229
  style: styles.head,
116
- children: [/*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
117
- scope: "col",
118
- style: {
119
- ...styles.headTitle,
120
- ...styles.topStart
121
- },
122
- children: namespace.title
123
- }), columns.map((operation, column) => /*#__PURE__*/ jsx("th", {
124
- scope: "col",
125
- style: column === columns.length - 1 ? {
126
- ...styles.headOperation,
127
- ...styles.topEnd
128
- } : styles.headOperation,
129
- title: operation.description,
130
- children: operation.label
131
- }, operation.id))] }), /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
132
- scope: "row",
133
- style: {
134
- ...styles.headLabel,
135
- ...styles.rule
136
- },
137
- children: "All"
138
- }), columns.map((operation, column) => {
139
- const state = columnState(matrix, path, namespace.id, operation.id, values);
140
- return /*#__PURE__*/ jsx("td", {
230
+ children: [/*#__PURE__*/ jsxs("tr", { children: [
231
+ /*#__PURE__*/ jsx("th", {
232
+ scope: "col",
141
233
  style: {
142
- ...cellStyle(column === columns.length - 1),
234
+ ...styles.headTitle,
235
+ ...styles.topStart
236
+ },
237
+ children: namespace.title
238
+ }),
239
+ /*#__PURE__*/ jsx("th", {
240
+ scope: "col",
241
+ style: {
242
+ ...styles.headOperation,
243
+ textAlign: "start",
244
+ ...withDelete ? {} : styles.topEnd
245
+ },
246
+ children: "Access"
247
+ }),
248
+ withDelete ? /*#__PURE__*/ jsx("th", {
249
+ scope: "col",
250
+ style: {
251
+ ...styles.headOperation,
252
+ textAlign: "start",
253
+ ...styles.topEnd
254
+ },
255
+ children: "Delete"
256
+ }) : null
257
+ ] }), /*#__PURE__*/ jsxs("tr", { children: [
258
+ /*#__PURE__*/ jsx("th", {
259
+ scope: "row",
260
+ style: {
261
+ ...styles.headLabel,
143
262
  ...styles.rule
144
263
  },
145
- children: /*#__PURE__*/ jsx(Box, {
146
- label: `${operation.label} every ${namespace.title.toLowerCase()} entry`,
147
- onChange: () => {
264
+ children: "All"
265
+ }),
266
+ /*#__PURE__*/ jsx("td", {
267
+ style: {
268
+ ...controlStyle(!withDelete),
269
+ ...styles.rule
270
+ },
271
+ children: /*#__PURE__*/ jsx(Segments, {
272
+ included: includedBelow(allOptions, allLevel),
273
+ label: `Access for every ${entries} entry`,
274
+ onSelect: (level) => {
148
275
  toggle({
149
- kind: "column",
276
+ kind: "access",
150
277
  namespace: namespace.id,
151
- operation: operation.id,
152
- value: state !== "on"
278
+ level
153
279
  });
154
280
  },
281
+ options: allOptions,
155
282
  readOnly,
156
- state
283
+ selected: allLevel
157
284
  })
158
- }, operation.id);
159
- })] })]
285
+ }),
286
+ withDelete ? /*#__PURE__*/ jsx("td", {
287
+ style: {
288
+ ...controlStyle(true),
289
+ ...styles.rule
290
+ },
291
+ children: /*#__PURE__*/ jsx(Segments, {
292
+ label: `Delete for every ${entries} entry`,
293
+ onSelect: (mode) => {
294
+ toggle({
295
+ kind: "deleteMode",
296
+ namespace: namespace.id,
297
+ mode
298
+ });
299
+ },
300
+ options: DELETE_OPTIONS.filter((option) => option.id !== "trash"),
301
+ readOnly,
302
+ selected: allDeleteMode(matrix, path, namespace.id, values)
303
+ })
304
+ }) : null
305
+ ] })]
160
306
  }),
161
307
  /*#__PURE__*/ jsx("tbody", { children: rows.map((row) => {
162
- const state = rowState(path, namespace.id, row, values);
163
- return /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsxs("th", {
164
- scope: "row",
165
- style: styles.rowHeader,
166
- children: [/*#__PURE__*/ jsx("button", {
167
- className: `${BASE_CLASS}__toggle`,
168
- disabled: readOnly,
169
- onClick: () => {
170
- toggle({
171
- kind: "row",
172
- namespace: namespace.id,
173
- fieldName: row.fieldName,
174
- value: state !== "on"
175
- });
176
- },
177
- style: styles.rowButton,
178
- title: `Toggle every capability for ${row.label}`,
179
- type: "button",
180
- children: row.label
181
- }), row.hint ? /*#__PURE__*/ jsx("span", {
182
- style: styles.hint,
183
- children: row.hint
184
- }) : null]
185
- }), columns.map((operation, column) => /*#__PURE__*/ jsx("td", {
186
- style: cellStyle(column === columns.length - 1),
187
- children: row[operation.id] ? /*#__PURE__*/ jsx(Box, {
188
- label: `${operation.label} ${row.label}`,
189
- onChange: () => {
190
- toggle({
191
- kind: "cell",
192
- namespace: namespace.id,
193
- fieldName: row.fieldName,
194
- operation: operation.id,
195
- value: values[cellPath(path, namespace.id, row.fieldName, operation.id)] !== true
196
- });
197
- },
198
- readOnly,
199
- state: values[cellPath(path, namespace.id, row.fieldName, operation.id)] === true ? "on" : "off"
200
- }) : /*#__PURE__*/ jsx(Dash, {})
201
- }, operation.id))] }, row.fieldName);
308
+ const options = accessOptions([row]);
309
+ const level = accessLevelOf(path, namespace.id, row, values);
310
+ return /*#__PURE__*/ jsxs("tr", { children: [
311
+ /*#__PURE__*/ jsxs("th", {
312
+ scope: "row",
313
+ style: styles.rowHeader,
314
+ children: [row.label, row.live ? /*#__PURE__*/ jsx(LiveBadge, {}) : null]
315
+ }),
316
+ /*#__PURE__*/ jsx("td", {
317
+ style: controlStyle(!withDelete),
318
+ children: /*#__PURE__*/ jsx(Segments, {
319
+ included: includedBelow(options, level),
320
+ label: `Access for ${row.label}`,
321
+ onSelect: (next) => {
322
+ toggle({
323
+ kind: "access",
324
+ namespace: namespace.id,
325
+ fieldName: row.fieldName,
326
+ level: next
327
+ });
328
+ },
329
+ options,
330
+ readOnly,
331
+ selected: level
332
+ })
333
+ }),
334
+ withDelete ? /*#__PURE__*/ jsx("td", {
335
+ style: controlStyle(true),
336
+ children: row.delete ? /*#__PURE__*/ jsx(Segments, {
337
+ label: `Delete for ${row.label}`,
338
+ onSelect: (mode) => {
339
+ toggle({
340
+ kind: "deleteMode",
341
+ namespace: namespace.id,
342
+ fieldName: row.fieldName,
343
+ mode
344
+ });
345
+ },
346
+ options: DELETE_OPTIONS.filter((option) => option.id !== "trash" || row.deleteUnattended),
347
+ readOnly,
348
+ selected: deleteModeOf(path, namespace.id, row, values)
349
+ }) : null
350
+ }) : null
351
+ ] }, row.fieldName);
202
352
  }) })
203
353
  ]
204
354
  })
@@ -211,7 +361,7 @@ const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
211
361
  children: /*#__PURE__*/ jsxs("table", {
212
362
  style: styles.table,
213
363
  children: [
214
- /*#__PURE__*/ jsx(Columns, { count: 1 }),
364
+ /*#__PURE__*/ jsx(Columns, { widths: [OPERATION_WIDTH] }),
215
365
  /*#__PURE__*/ jsxs("thead", {
216
366
  style: styles.head,
217
367
  children: [/*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
@@ -237,7 +387,7 @@ const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
237
387
  children: "All"
238
388
  }), /*#__PURE__*/ jsx("td", {
239
389
  style: {
240
- ...cellStyle(true),
390
+ ...lastCellStyle,
241
391
  ...styles.rule
242
392
  },
243
393
  children: /*#__PURE__*/ jsx(Box, {
@@ -261,7 +411,7 @@ const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
261
411
  children: tool.description
262
412
  })]
263
413
  }), /*#__PURE__*/ jsx("td", {
264
- style: cellStyle(true),
414
+ style: lastCellStyle,
265
415
  children: /*#__PURE__*/ jsx(Box, {
266
416
  label: `Enable ${tool.name}`,
267
417
  onChange: () => {
@@ -280,9 +430,10 @@ const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
280
430
  });
281
431
  };
282
432
  /**
283
- * What an API key may do, as one table per namespace: a row per entity, a
284
- * column per operation and a bulk toggle in each column header. The nested
285
- * checkbox fields back every cell, so the stored document is unaffected.
433
+ * What an API key may do, as one table per namespace: a row per entity with
434
+ * segmented Access and Delete controls, and an "All" row that sets every row.
435
+ * The nested checkbox fields back every control, so the stored document is
436
+ * unaffected.
286
437
  */ const McpxCapabilityMatrix = ({ field, matrix, path = CAPABILITIES_FIELD, readOnly = false, withinTab = false }) => {
287
438
  const { dispatchFields, setModified } = useForm();
288
439
  const paths = useMemo(() => capabilityPaths(matrix, path), [matrix, path]);