@abinnovision/payloadcms-mcpx 2.0.0-beta.8 → 2.0.0

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 (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +56 -36
  3. package/dist/admin/capability-matrix.d.mts +17 -7
  4. package/dist/admin/capability-matrix.mjs +374 -272
  5. package/dist/admin/capability-toggles.mjs +79 -42
  6. package/dist/admin/confirmations-panel.mjs +232 -0
  7. package/dist/admin/confirmations.d.mts +15 -0
  8. package/dist/admin/confirmations.mjs +71 -0
  9. package/dist/admin/endpoint-url.mjs +7 -0
  10. package/dist/admin/index.d.mts +2 -1
  11. package/dist/admin/index.mjs +2 -1
  12. package/dist/admin/setup-guide.d.mts +6 -4
  13. package/dist/admin/setup-guide.mjs +51 -33
  14. package/dist/api-keys/capability-matrix.d.mts +12 -10
  15. package/dist/api-keys/capability-matrix.mjs +17 -37
  16. package/dist/api-keys/collection.mjs +18 -19
  17. package/dist/api-keys/confirmation-view.mjs +7 -0
  18. package/dist/api-keys/fields.mjs +67 -53
  19. package/dist/auth/resolve.mjs +79 -30
  20. package/dist/capabilities.d.mts +2 -0
  21. package/dist/capabilities.mjs +69 -15
  22. package/dist/define-tool.d.mts +10 -6
  23. package/dist/define-tool.mjs +8 -1
  24. package/dist/endpoint/confirmations.mjs +124 -0
  25. package/dist/endpoint/handler.mjs +25 -27
  26. package/dist/endpoint/http.mjs +27 -0
  27. package/dist/endpoint/server.mjs +52 -27
  28. package/dist/endpoint/upload.mjs +104 -0
  29. package/dist/grants/confirmation.mjs +75 -0
  30. package/dist/grants/grant.mjs +137 -0
  31. package/dist/guards.mjs +11 -2
  32. package/dist/i18n.mjs +3 -1
  33. package/dist/index.d.mts +2 -2
  34. package/dist/index.mjs +1 -1
  35. package/dist/options.mjs +73 -62
  36. package/dist/plugin.d.mts +3 -2
  37. package/dist/plugin.mjs +19 -4
  38. package/dist/request.mjs +5 -1
  39. package/dist/result.mjs +11 -1
  40. package/dist/schema/admin-hidden.mjs +4 -4
  41. package/dist/schema/describe.mjs +12 -18
  42. package/dist/schema/index.mjs +5 -6
  43. package/dist/schema/lexical-pointer.mjs +8 -8
  44. package/dist/schema/lexical.mjs +21 -15
  45. package/dist/schema/outline.mjs +4 -5
  46. package/dist/schema/path.mjs +13 -4
  47. package/dist/schema/pointer.mjs +11 -17
  48. package/dist/schema/shape.mjs +39 -51
  49. package/dist/schema/walk.mjs +68 -21
  50. package/dist/tools/builtin.mjs +7 -1
  51. package/dist/tools/create-document.mjs +38 -26
  52. package/dist/tools/delete-document.mjs +154 -0
  53. package/dist/tools/describe-schema.mjs +7 -5
  54. package/dist/tools/document.mjs +147 -21
  55. package/dist/tools/duplicate-document.mjs +50 -0
  56. package/dist/tools/find-documents.mjs +15 -20
  57. package/dist/tools/find-versions.mjs +11 -15
  58. package/dist/tools/get-document.mjs +31 -15
  59. package/dist/tools/links.mjs +53 -0
  60. package/dist/tools/list-capabilities.mjs +17 -7
  61. package/dist/tools/patch-document.mjs +60 -54
  62. package/dist/tools/publish-document.mjs +23 -30
  63. package/dist/tools/query-paths.mjs +4 -2
  64. package/dist/tools/run-confirmed.mjs +114 -0
  65. package/dist/tools/shared.mjs +66 -50
  66. package/dist/tools/validate-document.mjs +10 -11
  67. package/dist/tools/versions.mjs +24 -10
  68. package/dist/types.d.mts +228 -60
  69. package/dist/upload/file.mjs +177 -0
  70. package/dist/version.mjs +4 -2
  71. package/dist/write/draft-guard.mjs +46 -37
  72. package/dist/write/patch.mjs +4 -4
  73. package/dist/write/publish-intent.mjs +25 -11
  74. package/dist/write/row-ids.mjs +6 -2
  75. package/dist/write/write-data.mjs +18 -31
  76. package/package.json +15 -14
  77. package/dist/builtin-tool-names.mjs +0 -16
@@ -1,73 +1,110 @@
1
- import { CAPABILITY_OPERATIONS, cellPath, toolPath } from "../api-keys/capability-matrix.mjs";
1
+ import { ACCESS_CHAIN, STORED_OPERATIONS, requiresOf } from "../capabilities.mjs";
2
+ import { cellPath, rowOperations, toolPath } from "../api-keys/capability-matrix.mjs";
2
3
  //#region src/admin/capability-toggles.ts
3
- const stateOf = (paths, values) => {
4
- if (paths.length === 0) return "off";
5
- const granted = paths.filter((path) => values[path] === true).length;
4
+ /**
5
+ * How far a key may go with an entity. Each level includes the ones before it.
6
+ */ const ACCESS_LEVELS = ["none", ...ACCESS_CHAIN.map(({ id }) => id)];
7
+ const toolGranted = (values, basePath, name) => values[toolPath(basePath, name)] === true;
8
+ const cellGranted = (values, basePath, namespace, row, operation) => values[cellPath(basePath, namespace, row.fieldName, operation)] === true;
9
+ /**
10
+ * One value per stored operation.
11
+ */ const byOperation = (valueOf) => Object.fromEntries(STORED_OPERATIONS.map(({ id }) => [id, valueOf(id)]));
12
+ const toolsState = (matrix, basePath, values) => {
13
+ const granted = matrix.tools.filter((tool) => toolGranted(values, basePath, tool.name)).length;
6
14
  if (granted === 0) return "off";
7
- return granted === paths.length ? "on" : "mixed";
15
+ return granted === matrix.tools.length ? "on" : "mixed";
8
16
  };
9
17
  /**
10
- * Whether a column's bulk toggle reads as on, off or indeterminate. Cells the
11
- * config does not expose are ignored: two granted cells and one dash read as on.
12
- */ const columnState = (matrix, basePath, namespace, operation, values) => stateOf(matrix[namespace].filter((row) => row[operation]).map((row) => cellPath(basePath, namespace, row.fieldName, operation)), values);
18
+ * The levels a row offers: none, then each operation the config exposes.
19
+ */ const accessLevelsOf = (row) => ACCESS_LEVELS.filter((level) => level === "none" || row[level]);
13
20
  /**
14
- * Whether an entity's row toggle reads as on, off or indeterminate, over the
15
- * operations the config exposes for that row.
16
- */ const rowState = (basePath, namespace, row, values) => stateOf(CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)), values);
17
- const toolsState = (matrix, basePath, values) => stateOf(matrix.tools.map((tool) => toolPath(basePath, tool.name)), values);
21
+ * The highest level whose flags are all granted, counting only operations the
22
+ * config exposes. A write without a read reads as none, as on the server.
23
+ */ const accessLevelOf = (basePath, namespace, row, values) => {
24
+ let reached = "none";
25
+ for (const { id } of ACCESS_CHAIN) {
26
+ if (!row[id] || !cellGranted(values, basePath, namespace, row, id)) break;
27
+ reached = id;
28
+ }
29
+ return reached;
30
+ };
31
+ /**
32
+ * The delete mode a row's stored flags read as.
33
+ */ const deleteModeOf = (basePath, namespace, row, values) => {
34
+ const granted = (operation) => cellGranted(values, basePath, namespace, row, operation);
35
+ if (!granted("read") || !granted("delete")) return "off";
36
+ return row.deleteUnattended && granted("deleteUnattended") ? "trash" : "approve";
37
+ };
38
+ const prerequisitesOf = (operation) => {
39
+ const required = requiresOf(operation);
40
+ return required ? [required, ...prerequisitesOf(required)] : [];
41
+ };
18
42
  const reconcile = (next, touched) => {
19
- if (touched.has("publish") && next.publish) return {
20
- ...next,
21
- write: true
22
- };
23
- if (touched.has("write") && !next.write) return {
24
- ...next,
25
- publish: false
26
- };
27
- return next;
43
+ const result = { ...next };
44
+ for (const operation of touched) if (result[operation]) for (const required of prerequisitesOf(operation)) result[required] = true;
45
+ else for (const { id } of STORED_OPERATIONS) if (prerequisitesOf(id).includes(operation)) result[id] = false;
46
+ return result;
28
47
  };
29
48
  const rowActions = (basePath, namespace, row, values, changes) => {
30
- const current = {
31
- read: values[cellPath(basePath, namespace, row.fieldName, "read")] === true,
32
- write: values[cellPath(basePath, namespace, row.fieldName, "write")] === true,
33
- publish: values[cellPath(basePath, namespace, row.fieldName, "publish")] === true
34
- };
49
+ const current = byOperation((operation) => cellGranted(values, basePath, namespace, row, operation));
35
50
  const touched = /* @__PURE__ */ new Set();
36
51
  const next = { ...current };
37
- for (const operation of CAPABILITY_OPERATIONS) {
52
+ for (const operation of rowOperations(row)) {
38
53
  const change = changes[operation.id];
39
- if (change !== void 0 && row[operation.id]) {
54
+ if (change !== void 0) {
40
55
  next[operation.id] = change;
41
56
  touched.add(operation.id);
42
57
  }
43
58
  }
44
59
  const reconciled = reconcile(next, touched);
45
- return CAPABILITY_OPERATIONS.filter((operation) => row[operation.id] && reconciled[operation.id] !== current[operation.id]).map((operation) => ({
60
+ return rowOperations(row).filter((operation) => reconciled[operation.id] !== current[operation.id]).map((operation) => ({
46
61
  type: "UPDATE",
47
62
  path: cellPath(basePath, namespace, row.fieldName, operation.id),
48
63
  value: reconciled[operation.id]
49
64
  }));
50
65
  };
66
+ const grantsOf = (level) => {
67
+ const rank = ACCESS_LEVELS.indexOf(level);
68
+ return Object.fromEntries(ACCESS_CHAIN.map(({ id }, i) => [id, rank > i]));
69
+ };
51
70
  /**
52
- * The form-state updates one click implies, as literal actions. Every toggle
53
- * (cell, row, column or tool) comes through here, so the publish rule applies
54
- * to bulk grants too.
71
+ * The form-state updates one click implies, as literal actions. Every click
72
+ * comes through here, so the dependency rules apply to the "All" controls too:
73
+ * picking Write ticks read, and lowering access to none clears delete.
55
74
  *
56
- * Only cells whose value changes get an action, so a bulk toggle that changes
57
- * nothing does not mark the form modified.
75
+ * Only cells whose value changes get an action, so a click that changes
76
+ * nothing does not mark the form modified. A level above a row's ceiling
77
+ * grants what the row exposes and no more.
58
78
  */ const buildToggleActions = (matrix, basePath, values, intent) => {
59
- if (intent.kind === "tool" || intent.kind === "tools") return matrix.tools.filter((tool) => intent.kind === "tools" || tool.name === intent.name).filter((tool) => values[toolPath(basePath, tool.name)] === true !== intent.value).map((tool) => ({
79
+ if (intent.kind === "tool" || intent.kind === "tools") return matrix.tools.filter((tool) => intent.kind === "tools" || tool.name === intent.name).filter((tool) => toolGranted(values, basePath, tool.name) !== intent.value).map((tool) => ({
60
80
  type: "UPDATE",
61
81
  path: toolPath(basePath, tool.name),
62
82
  value: intent.value
63
83
  }));
64
- const rows = matrix[intent.namespace].filter((row) => intent.kind === "column" || row.fieldName === intent.fieldName);
65
- const changes = intent.kind === "row" ? {
66
- read: intent.value,
67
- write: intent.value,
68
- publish: intent.value
69
- } : { [intent.operation]: intent.value };
84
+ const rows = matrix[intent.namespace].filter((row) => intent.fieldName === void 0 || row.fieldName === intent.fieldName);
85
+ const changes = intent.kind === "access" ? grantsOf(intent.level) : {
86
+ delete: intent.mode !== "off",
87
+ deleteUnattended: intent.mode === "trash"
88
+ };
70
89
  return rows.flatMap((row) => rowActions(basePath, intent.namespace, row, values, changes));
71
90
  };
91
+ const firstNoop = (candidates, matrix, basePath, values, intentOf) => candidates.find((candidate) => buildToggleActions(matrix, basePath, values, intentOf(candidate)).length === 0);
92
+ /**
93
+ * The level a namespace's "All" control shows: the one that would change
94
+ * nothing if picked, so rows capped below it still count. `undefined` while
95
+ * the rows differ.
96
+ */ const allAccessLevel = (matrix, basePath, namespace, values) => firstNoop(ACCESS_LEVELS, matrix, basePath, values, (level) => ({
97
+ kind: "access",
98
+ namespace,
99
+ level
100
+ }));
101
+ /**
102
+ * The delete mode the "All" control shows, by the same rule. It offers no
103
+ * trash, so a row that trashes directly reads as mixed.
104
+ */ const allDeleteMode = (matrix, basePath, namespace, values) => firstNoop(["off", "approve"], matrix, basePath, values, (mode) => ({
105
+ kind: "deleteMode",
106
+ namespace,
107
+ mode
108
+ }));
72
109
  //#endregion
73
- export { buildToggleActions, columnState, rowState, toolsState };
110
+ export { accessLevelOf, accessLevelsOf, allAccessLevel, allDeleteMode, buildToggleActions, deleteModeOf, toolsState };
@@ -0,0 +1,232 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Button } from "@payloadcms/ui";
4
+ //#region src/admin/confirmations-panel.tsx
5
+ /**
6
+ * The element id of a call's row, which the page scrolls to.
7
+ */ const confirmationRowId = (handle) => `mcpx-confirmation-${handle}`;
8
+ const CELL_PADDING = "calc(var(--base) * 0.4) calc(var(--base) * 0.6)";
9
+ const styles = {
10
+ panel: {
11
+ marginBottom: "var(--base)",
12
+ padding: "calc(var(--base) * 0.75)",
13
+ border: "1px solid var(--theme-elevation-150)",
14
+ borderRadius: "3px",
15
+ background: "var(--theme-elevation-50)"
16
+ },
17
+ heading: { margin: 0 },
18
+ lead: {
19
+ margin: "calc(var(--base) * 0.25) 0 calc(var(--base) * 0.75)",
20
+ color: "var(--theme-elevation-500)"
21
+ },
22
+ group: { marginBottom: "calc(var(--base) * 0.75)" },
23
+ groupHeading: { margin: "0 0 calc(var(--base) * 0.25)" },
24
+ table: {
25
+ width: "100%",
26
+ borderCollapse: "collapse",
27
+ textAlign: "left"
28
+ },
29
+ head: {
30
+ padding: CELL_PADDING,
31
+ color: "var(--theme-elevation-400)",
32
+ fontWeight: "normal",
33
+ borderBottom: "1px solid var(--theme-border-color)"
34
+ },
35
+ cell: {
36
+ padding: CELL_PADDING,
37
+ verticalAlign: "middle",
38
+ borderBottom: "1px solid var(--theme-elevation-100)"
39
+ },
40
+ highlighted: { background: "var(--theme-elevation-100)" },
41
+ muted: { color: "var(--theme-elevation-500)" },
42
+ status: {
43
+ display: "block",
44
+ color: "var(--theme-elevation-500)",
45
+ fontSize: "0.85em"
46
+ },
47
+ permanent: {
48
+ color: "var(--theme-error-500)",
49
+ fontWeight: 600
50
+ },
51
+ actions: {
52
+ display: "flex",
53
+ gap: "calc(var(--base) * 0.25)",
54
+ justifyContent: "flex-end"
55
+ },
56
+ footer: {
57
+ display: "flex",
58
+ gap: "calc(var(--base) * 0.5)"
59
+ }
60
+ };
61
+ const HEADS = [
62
+ "Collection",
63
+ "Document",
64
+ "Effect",
65
+ "Reason"
66
+ ];
67
+ const groupsOf = (confirmations) => {
68
+ const groups = /* @__PURE__ */ new Map();
69
+ for (const confirmation of confirmations) {
70
+ const entries = groups.get(confirmation.group) ?? [];
71
+ entries.push(confirmation);
72
+ groups.set(confirmation.group, entries);
73
+ }
74
+ return [...groups];
75
+ };
76
+ const DecisionButtons = ({ approve, busy, onDecide, reject, small = false }) => /*#__PURE__*/ jsxs(Fragment, { children: [/*#__PURE__*/ jsx(Button, {
77
+ buttonStyle: "primary",
78
+ disabled: busy,
79
+ margin: false,
80
+ onClick: () => {
81
+ onDecide("approved");
82
+ },
83
+ size: small ? "small" : "medium",
84
+ type: "button",
85
+ children: approve
86
+ }), /*#__PURE__*/ jsx(Button, {
87
+ buttonStyle: "secondary",
88
+ disabled: busy,
89
+ margin: false,
90
+ onClick: () => {
91
+ onDecide("rejected");
92
+ },
93
+ size: small ? "small" : "medium",
94
+ type: "button",
95
+ children: reject
96
+ })] });
97
+ const approveAllLabel = (confirmations) => {
98
+ const permanent = confirmations.filter((confirmation) => confirmation.summary.permanent).length;
99
+ return permanent === 0 ? `Approve all (${String(confirmations.length)})` : `Approve all (${String(confirmations.length)}, ${String(permanent)} permanent)`;
100
+ };
101
+ const Row = ({ busy, confirmation, onDecide }) => {
102
+ const { handle, highlighted, summary } = confirmation;
103
+ const cell = highlighted ? {
104
+ ...styles.cell,
105
+ ...styles.highlighted
106
+ } : styles.cell;
107
+ const name = summary.title ?? summary.id ?? "Unreadable";
108
+ return /*#__PURE__*/ jsxs("tr", {
109
+ "aria-current": highlighted ? "true" : void 0,
110
+ id: confirmationRowId(handle),
111
+ children: [
112
+ /*#__PURE__*/ jsx("td", {
113
+ style: cell,
114
+ children: summary.label
115
+ }),
116
+ /*#__PURE__*/ jsxs("td", {
117
+ style: cell,
118
+ children: [
119
+ summary.href ? /*#__PURE__*/ jsx("a", {
120
+ href: summary.href,
121
+ rel: "noreferrer",
122
+ target: "_blank",
123
+ children: name
124
+ }) : name,
125
+ summary.title && summary.id ? /*#__PURE__*/ jsxs("span", {
126
+ style: styles.muted,
127
+ children: [
128
+ " (",
129
+ summary.id,
130
+ ")"
131
+ ]
132
+ }) : null,
133
+ summary.status ? /*#__PURE__*/ jsx("span", {
134
+ style: styles.status,
135
+ children: summary.status
136
+ }) : null
137
+ ]
138
+ }),
139
+ /*#__PURE__*/ jsx("td", {
140
+ style: cell,
141
+ children: summary.permanent ? /*#__PURE__*/ jsx("span", {
142
+ style: styles.permanent,
143
+ children: "Permanent"
144
+ }) : "Moves to trash"
145
+ }),
146
+ /*#__PURE__*/ jsx("td", {
147
+ style: cell,
148
+ children: summary.reason ? /*#__PURE__*/ jsxs("span", { children: [/*#__PURE__*/ jsx("span", {
149
+ style: styles.muted,
150
+ children: "The client wrote: "
151
+ }), summary.reason] }) : /*#__PURE__*/ jsx("span", {
152
+ style: styles.muted,
153
+ children: "No reason given"
154
+ })
155
+ }),
156
+ /*#__PURE__*/ jsx("td", {
157
+ style: cell,
158
+ children: /*#__PURE__*/ jsx("div", {
159
+ style: styles.actions,
160
+ children: /*#__PURE__*/ jsx(DecisionButtons, {
161
+ approve: "Approve",
162
+ busy,
163
+ onDecide: (decision) => {
164
+ onDecide([handle], decision);
165
+ },
166
+ reject: "Reject",
167
+ small: true
168
+ })
169
+ })
170
+ })
171
+ ]
172
+ });
173
+ };
174
+ /**
175
+ * The calls of an API key waiting for its user's decision, grouped by tool.
176
+ * Renders nothing when none is waiting.
177
+ */ const ConfirmationsPanel = ({ busy, confirmations, onDecide }) => {
178
+ if (confirmations.length === 0) return null;
179
+ const handles = confirmations.map((confirmation) => confirmation.handle);
180
+ return /*#__PURE__*/ jsxs("section", {
181
+ className: "field-type",
182
+ style: styles.panel,
183
+ children: [
184
+ /*#__PURE__*/ jsx("h3", {
185
+ style: styles.heading,
186
+ children: "Waiting for your approval"
187
+ }),
188
+ /*#__PURE__*/ jsx("p", {
189
+ style: styles.lead,
190
+ children: "A client using this key asked for these calls. Approving runs nothing: the client runs approved calls with runConfirmed. Each call expires 15 minutes after it was requested."
191
+ }),
192
+ groupsOf(confirmations).map(([group, entries]) => /*#__PURE__*/ jsxs("div", {
193
+ style: styles.group,
194
+ children: [/*#__PURE__*/ jsx("h4", {
195
+ style: styles.groupHeading,
196
+ children: group
197
+ }), /*#__PURE__*/ jsxs("table", {
198
+ style: styles.table,
199
+ children: [/*#__PURE__*/ jsx("thead", { children: /*#__PURE__*/ jsxs("tr", { children: [HEADS.map((head) => /*#__PURE__*/ jsx("th", {
200
+ scope: "col",
201
+ style: styles.head,
202
+ children: head
203
+ }, head)), /*#__PURE__*/ jsx("th", {
204
+ scope: "col",
205
+ style: styles.head,
206
+ children: /*#__PURE__*/ jsx("span", {
207
+ className: "sr-only",
208
+ children: "Decision"
209
+ })
210
+ })] }) }), /*#__PURE__*/ jsx("tbody", { children: entries.map((confirmation) => /*#__PURE__*/ jsx(Row, {
211
+ busy,
212
+ confirmation,
213
+ onDecide
214
+ }, confirmation.handle)) })]
215
+ })]
216
+ }, group)),
217
+ /*#__PURE__*/ jsx("div", {
218
+ style: styles.footer,
219
+ children: /*#__PURE__*/ jsx(DecisionButtons, {
220
+ approve: approveAllLabel(confirmations),
221
+ busy,
222
+ onDecide: (decision) => {
223
+ onDecide(handles, decision);
224
+ },
225
+ reject: "Reject all"
226
+ })
227
+ })
228
+ ]
229
+ });
230
+ };
231
+ //#endregion
232
+ export { ConfirmationsPanel, confirmationRowId };
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ //#region src/admin/confirmations.d.ts
3
+ interface McpxConfirmationsProps {
4
+ /**
5
+ * Endpoint path below the API route, from the plugin options.
6
+ */
7
+ endpointPath: string;
8
+ }
9
+ /**
10
+ * The calls waiting for approval on the API key edit view. Reads and decides
11
+ * through the plugin's endpoints, never through the key's form, so a decision
12
+ * does not mark the key modified. Renders nothing while none is waiting.
13
+ */
14
+ export declare const McpxConfirmations: React.FC<McpxConfirmationsProps>;
15
+ //#endregion
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { CONFIRMATIONS_PATH } from "../api-keys/confirmation-view.mjs";
3
+ import { ConfirmationsPanel, confirmationRowId } from "./confirmations-panel.mjs";
4
+ import { endpointUrl } from "./endpoint-url.mjs";
5
+ import { jsx } from "react/jsx-runtime";
6
+ import { toast, useConfig, useDocumentInfo } from "@payloadcms/ui";
7
+ import { useCallback, useEffect, useState } from "react";
8
+ //#region src/admin/confirmations.tsx
9
+ /**
10
+ * The calls waiting for approval on the API key edit view. Reads and decides
11
+ * through the plugin's endpoints, never through the key's form, so a decision
12
+ * does not mark the key modified. Renders nothing while none is waiting.
13
+ */ const McpxConfirmations = ({ endpointPath }) => {
14
+ const { id } = useDocumentInfo();
15
+ const { config } = useConfig();
16
+ const [confirmations, setConfirmations] = useState([]);
17
+ const [busy, setBusy] = useState(false);
18
+ const [version, setVersion] = useState(0);
19
+ const url = `${endpointUrl(config.serverURL, config.routes.api, endpointPath)}${CONFIRMATIONS_PATH}`;
20
+ useEffect(() => {
21
+ if (id === void 0) return;
22
+ let active = true;
23
+ const params = new URLSearchParams({ key: String(id) });
24
+ const highlight = new URLSearchParams(window.location.search).get("confirmation");
25
+ if (highlight !== null) params.set("confirmation", highlight);
26
+ fetch(`${url}?${params.toString()}`, { credentials: "include" }).then(async (response) => response.ok ? await response.json() : { confirmations: [] }).then((body) => {
27
+ if (active) setConfirmations(body.confirmations);
28
+ }).catch(() => void 0);
29
+ return () => {
30
+ active = false;
31
+ };
32
+ }, [
33
+ id,
34
+ url,
35
+ version
36
+ ]);
37
+ const highlighted = confirmations.find((entry) => entry.highlighted)?.handle;
38
+ useEffect(() => {
39
+ if (highlighted !== void 0) document.getElementById(confirmationRowId(highlighted))?.scrollIntoView({ block: "center" });
40
+ }, [highlighted]);
41
+ const decide = useCallback((handles, decision) => {
42
+ setBusy(true);
43
+ fetch(url, {
44
+ method: "POST",
45
+ credentials: "include",
46
+ headers: { "content-type": "application/json" },
47
+ body: JSON.stringify({
48
+ key: id,
49
+ handles,
50
+ decision
51
+ })
52
+ }).then(async (response) => {
53
+ if (!response.ok) {
54
+ const body = await response.json().catch(() => ({}));
55
+ toast.error(body.error ?? "The decision could not be saved.");
56
+ }
57
+ }).catch(() => {
58
+ toast.error("The decision could not be saved.");
59
+ }).finally(() => {
60
+ setBusy(false);
61
+ setVersion((current) => current + 1);
62
+ });
63
+ }, [id, url]);
64
+ return /*#__PURE__*/ jsx(ConfirmationsPanel, {
65
+ busy,
66
+ confirmations,
67
+ onDecide: decide
68
+ });
69
+ };
70
+ //#endregion
71
+ export { McpxConfirmations };
@@ -0,0 +1,7 @@
1
+ //#region src/admin/endpoint-url.ts
2
+ /**
3
+ * The plugin endpoint's URL below the API route, without a doubled slash
4
+ * after the origin.
5
+ */ const endpointUrl = (origin, apiRoute, endpointPath) => `${origin.replace(/\/+$/, "")}${apiRoute}${endpointPath}`;
6
+ //#endregion
7
+ export { endpointUrl };
@@ -1,3 +1,4 @@
1
1
  import { McpxCapabilityMatrix } from "./capability-matrix.mjs";
2
+ import { McpxConfirmations } from "./confirmations.mjs";
2
3
  import { McpxSetupGuide } from "./setup-guide.mjs";
3
- export { McpxCapabilityMatrix, McpxSetupGuide };
4
+ export { McpxCapabilityMatrix, McpxConfirmations, McpxSetupGuide };
@@ -1,3 +1,4 @@
1
1
  import { McpxCapabilityMatrix } from "./capability-matrix.mjs";
2
+ import { McpxConfirmations } from "./confirmations.mjs";
2
3
  import { McpxSetupGuide } from "./setup-guide.mjs";
3
- export { McpxCapabilityMatrix, McpxSetupGuide };
4
+ export { McpxCapabilityMatrix, McpxConfirmations, McpxSetupGuide };
@@ -1,13 +1,15 @@
1
1
  import React from "react";
2
2
  //#region src/admin/setup-guide.d.ts
3
3
  interface McpxSetupGuideProps {
4
- /** Endpoint path below the API route, from the plugin options. */
4
+ /**
5
+ * Endpoint path below the API route, from the plugin options.
6
+ */
5
7
  endpointPath: string;
6
8
  }
7
9
  /**
8
- * Per-key connection instructions on the API key edit view. Renders nothing
9
- * until the document is saved, because before that there is no key to hand to
10
- * a client.
10
+ * Per-key connection instructions on the API key edit view, opened from a
11
+ * button next to the document controls. Renders nothing until the document is
12
+ * saved, because before that there is no key to hand to a client.
11
13
  */
12
14
  export declare const McpxSetupGuide: React.FC<McpxSetupGuideProps>;
13
15
  //#endregion
@@ -1,7 +1,8 @@
1
1
  "use client";
2
+ import { endpointUrl } from "./endpoint-url.mjs";
2
3
  import { buildSetupGuide } from "../api-keys/setup-guide.mjs";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
- import { CopyToClipboard, useConfig, useDocumentInfo, useFormFields } from "@payloadcms/ui";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Banner, Button, CopyToClipboard, Drawer, formatDrawerSlug, useConfig, useDocumentInfo, useEditDepth, useFormFields, useModal } from "@payloadcms/ui";
5
6
  import { useEffect, useState } from "react";
6
7
  //#region src/admin/setup-guide.tsx
7
8
  const asString = (value) => typeof value === "string" ? value : void 0;
@@ -13,7 +14,6 @@ const useOrigin = (serverUrl) => {
13
14
  return origin;
14
15
  };
15
16
  const styles = {
16
- lead: { marginBottom: "calc(var(--base) * 0.75)" },
17
17
  section: { marginBottom: "calc(var(--base) * 0.75)" },
18
18
  sectionHeader: {
19
19
  display: "flex",
@@ -36,45 +36,63 @@ const styles = {
36
36
  wordBreak: "break-all"
37
37
  }
38
38
  };
39
- /**
40
- * Per-key connection instructions on the API key edit view. Renders nothing
41
- * until the document is saved, because before that there is no key to hand to
42
- * a client.
43
- */ const McpxSetupGuide = ({ endpointPath }) => {
44
- const { id } = useDocumentInfo();
39
+ const SetupGuideContent = ({ endpointPath }) => {
45
40
  const { config } = useConfig();
46
41
  const apiKey = useFormFields(([fields]) => fields["apiKey"]?.value);
47
42
  const label = useFormFields(([fields]) => fields["label"]?.value);
48
43
  const origin = useOrigin(config.serverURL);
49
- if (id === void 0) return null;
50
44
  const sections = buildSetupGuide({
51
- endpointUrl: `${origin.replace(/\/+$/, "")}${config.routes.api}${endpointPath}`,
45
+ endpointUrl: endpointUrl(origin, config.routes.api, endpointPath),
52
46
  apiKey: asString(apiKey),
53
47
  label: asString(label)
54
48
  });
55
- return /*#__PURE__*/ jsxs("div", {
56
- className: "field-type",
57
- children: [/*#__PURE__*/ jsx("p", {
58
- style: styles.lead,
59
- children: "Every snippet below contains this key in full. Treat it like a password."
60
- }), sections.map((section) => /*#__PURE__*/ jsxs("section", {
61
- style: styles.section,
62
- children: [
63
- /*#__PURE__*/ jsxs("div", {
64
- style: styles.sectionHeader,
65
- children: [/*#__PURE__*/ jsx("strong", { children: section.title }), /*#__PURE__*/ jsx(CopyToClipboard, { value: section.snippet })]
66
- }),
67
- section.description ? /*#__PURE__*/ jsx("p", {
68
- style: styles.description,
69
- children: section.description
70
- }) : null,
71
- /*#__PURE__*/ jsx("pre", {
72
- style: styles.snippet,
73
- children: section.snippet
74
- })
75
- ]
76
- }, section.id))]
49
+ return /*#__PURE__*/ jsxs(Fragment, { children: [/*#__PURE__*/ jsx(Banner, {
50
+ type: "warning",
51
+ children: "Every snippet below contains this key in full. Treat it like a password."
52
+ }), sections.map((section) => /*#__PURE__*/ jsxs("section", {
53
+ style: styles.section,
54
+ children: [
55
+ /*#__PURE__*/ jsxs("div", {
56
+ style: styles.sectionHeader,
57
+ children: [/*#__PURE__*/ jsx("strong", { children: section.title }), /*#__PURE__*/ jsx(CopyToClipboard, { value: section.snippet })]
58
+ }),
59
+ section.description ? /*#__PURE__*/ jsx("p", {
60
+ style: styles.description,
61
+ children: section.description
62
+ }) : null,
63
+ /*#__PURE__*/ jsx("pre", {
64
+ style: styles.snippet,
65
+ children: section.snippet
66
+ })
67
+ ]
68
+ }, section.id))] });
69
+ };
70
+ /**
71
+ * Per-key connection instructions on the API key edit view, opened from a
72
+ * button next to the document controls. Renders nothing until the document is
73
+ * saved, because before that there is no key to hand to a client.
74
+ */ const McpxSetupGuide = ({ endpointPath }) => {
75
+ const { id } = useDocumentInfo();
76
+ const { openModal } = useModal();
77
+ const drawerSlug = formatDrawerSlug({
78
+ slug: "mcpx-connect",
79
+ depth: useEditDepth()
77
80
  });
81
+ if (id === void 0) return null;
82
+ return /*#__PURE__*/ jsxs(Fragment, { children: [/*#__PURE__*/ jsx(Button, {
83
+ buttonStyle: "secondary",
84
+ margin: false,
85
+ onClick: () => {
86
+ openModal(drawerSlug);
87
+ },
88
+ size: "medium",
89
+ type: "button",
90
+ children: "Connect a client"
91
+ }), /*#__PURE__*/ jsx(Drawer, {
92
+ slug: drawerSlug,
93
+ title: "Connect a client",
94
+ children: /*#__PURE__*/ jsx(SetupGuideContent, { endpointPath })
95
+ })] });
78
96
  };
79
97
  //#endregion
80
98
  export { McpxSetupGuide };
@@ -1,19 +1,21 @@
1
+ import { McpxEntityCapabilities } from "../types.mjs";
1
2
  import "../options.mjs";
3
+ import "../capabilities.mjs";
2
4
  //#region src/api-keys/capability-matrix.d.ts
3
5
  /**
4
6
  * One entity's row. The booleans say what the plugin config exposes, not what
5
- * the key was granted. A `false` renders as a dash, so absence reads as a
6
- * refusal by config rather than a gap.
7
+ * the key was granted. A `false` leaves its segment out of the row's controls.
7
8
  */
8
- export interface CapabilityRow {
9
+ export interface CapabilityRow extends McpxEntityCapabilities {
9
10
  fieldName: string;
10
- /** The slug, which is what MCP clients send and what refusals name. */
11
- label: string;
12
- read: boolean;
13
- write: boolean;
14
- publish: boolean;
15
- /** Said only where a row departs from what its column header promises. */
16
- hint?: string;
11
+ /**
12
+ * The slug, which is what MCP clients send and what refusals name.
13
+ */
14
+ slug: string;
15
+ /**
16
+ * Writes go live immediately, as the entity keeps no drafts.
17
+ */
18
+ live?: boolean;
17
19
  }
18
20
  interface CapabilityTool {
19
21
  name: string;