@abinnovision/payloadcms-mcpx 2.0.0-beta.16 → 2.0.0-beta.19

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.
package/README.md CHANGED
@@ -116,19 +116,20 @@ every check.
116
116
 
117
117
  ## Tools
118
118
 
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` |
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
+ | `duplicateDocument` | Copies a document into a new draft, in every locale. | `write` |
129
+ | `validateDocument` | Lists what blocks publishing, without saving. | `write` |
130
+ | `publishDocument` | Publishes the current draft. | `publish` |
131
+ | `deleteDocument` | Asks to delete a document, which the key's user approves. | `delete` |
132
+ | `runConfirmed` | Runs the approved calls. | `delete` |
132
133
 
133
134
  Collections and globals use the same tools. A tool that addresses one document takes either
134
135
  `collection` and `id`, or `global` alone. [`docs/concepts.md`](./docs/concepts.md#tools) lists
@@ -144,7 +145,7 @@ each one is readable, writable and publishable. A key cannot go past it.
144
145
  Each key carries one checkbox per exposed entity and operation (`read`, `write`, `publish`,
145
146
  `delete`, `deleteUnattended`), and
146
147
  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
+ control (None, Read, Write, Publish) and a Delete checkbox with an approval shield. Each access level
148
149
  includes the ones before it, and a level the config does not expose has no segment. `publish`
149
150
  exists only where the config exposes `write` and `publish` on an entity with drafts, and counts
150
151
  only when `write` is ticked as well. `write` counts only when `read` is ticked, so a key never
@@ -153,7 +154,7 @@ immediately.
153
154
  `delete` exists only where the config sets `delete: true` on a collection, and counts only when
154
155
  `read` is ticked as well. Setting access to None clears it. A delete runs only after the key's user
155
156
  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
+ key's approval shield is off: there the document moves to trash at once. See
157
158
  [deleting](./docs/concepts.md#deleting).
158
159
 
159
160
  `tools/list` follows the key. A tool the key cannot use is not listed, and each `collection` and
@@ -26,7 +26,8 @@ interface McpxCapabilityMatrixProps {
26
26
  }
27
27
  /**
28
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.
29
+ * an Access control and a Delete checkbox with an approval shield, and an
30
+ * "All" row that sets every row.
30
31
  * The nested checkbox fields back every control, so the stored document is
31
32
  * unaffected.
32
33
  */
@@ -1,16 +1,16 @@
1
1
  "use client";
2
2
  import { CAPABILITIES_FIELD } from "../capabilities.mjs";
3
- import { CAPABILITY_OPERATIONS, STORED_OPERATIONS, capabilityPaths, toolPath } from "../api-keys/capability-matrix.mjs";
3
+ import { CAPABILITY_OPERATIONS, capabilityPaths, toolPath } from "../api-keys/capability-matrix.mjs";
4
+ import { translateStatic } from "../i18n.mjs";
4
5
  import { accessLevelOf, accessLevelsOf, allAccessLevel, allDeleteMode, buildToggleActions, deleteModeOf, toolsState } from "./capability-toggles.mjs";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
- import { Button, CheckboxInput, FieldDescription, FieldLabel, Pill, useForm, useFormFields } from "@payloadcms/ui";
7
+ import { Button, CheckboxInput, FieldDescription, FieldLabel, Pill, useConfig, useForm, useFormFields, useTranslation } from "@payloadcms/ui";
7
8
  import { useCallback, useMemo } from "react";
8
9
  //#region src/admin/capability-matrix.tsx
9
10
  const BASE_CLASS = "mcpx-capabilities";
10
11
  const LIVE_WRITE = "Writes go live immediately.";
11
12
  const SHEET = `
12
13
  .${BASE_CLASS} tbody tr:hover { background: var(--theme-elevation-50); }
13
- .${BASE_CLASS} section:last-of-type { margin-bottom: 0; }
14
14
  .${BASE_CLASS}__segments {
15
15
  display: inline-flex;
16
16
  border: 1px solid var(--theme-elevation-150);
@@ -22,12 +22,31 @@ const SHEET = `
22
22
  .${BASE_CLASS}__segments .btn:first-child { border-start-start-radius: inherit; border-end-start-radius: inherit; }
23
23
  .${BASE_CLASS}__segments .btn:last-child { border-start-end-radius: inherit; border-end-end-radius: inherit; }
24
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); }
25
+ .${BASE_CLASS}__shield {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ padding: calc(var(--base) * 0.2);
30
+ border: 0;
31
+ border-radius: var(--style-radius-s);
32
+ background: transparent;
33
+ color: var(--theme-elevation-800);
34
+ line-height: 0;
35
+ cursor: pointer;
36
+ }
37
+ .${BASE_CLASS}__shield:hover:not(:disabled) { background: var(--theme-elevation-100); }
38
+ .${BASE_CLASS}__shield:disabled { cursor: not-allowed; opacity: 0.6; }
39
+ .${BASE_CLASS}__shield.${BASE_CLASS}__danger { color: var(--theme-error-500); }
26
40
  `;
27
41
  const CELL_PADDING = "calc(var(--base) * 0.6)";
28
42
  const EDGE_PADDING = "calc(var(--base) * 0.8)";
29
43
  const styles = {
30
- section: { marginBottom: "var(--base)" },
44
+ sections: {
45
+ display: "flex",
46
+ flexWrap: "wrap",
47
+ gap: "var(--base) calc(var(--base) * 2)",
48
+ alignItems: "flex-start"
49
+ },
31
50
  table: {
32
51
  tableLayout: "fixed",
33
52
  width: "auto",
@@ -96,7 +115,12 @@ const styles = {
96
115
  verticalAlign: "middle",
97
116
  textAlign: "start"
98
117
  },
99
- lastCell: { paddingInlineEnd: EDGE_PADDING }
118
+ lastCell: { paddingInlineEnd: EDGE_PADDING },
119
+ deleteControls: {
120
+ display: "inline-flex",
121
+ alignItems: "center",
122
+ gap: "calc(var(--base) * 0.4)"
123
+ }
100
124
  };
101
125
  const Box = ({ label, onChange, readOnly, state }) => /*#__PURE__*/ jsx(CheckboxInput, {
102
126
  checked: state === "on",
@@ -105,7 +129,6 @@ const Box = ({ label, onChange, readOnly, state }) => /*#__PURE__*/ jsx(Checkbox
105
129
  partialChecked: state === "mixed",
106
130
  readOnly
107
131
  });
108
- const descriptionOf = (operation) => STORED_OPERATIONS.find((candidate) => candidate.id === operation)?.description;
109
132
  const ACCESS_OPTIONS = [{
110
133
  id: "none",
111
134
  label: "None",
@@ -115,23 +138,6 @@ const ACCESS_OPTIONS = [{
115
138
  label: operation.label,
116
139
  title: operation.description
117
140
  }))];
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
141
  const ARROW_STEPS = {
136
142
  ArrowDown: 1,
137
143
  ArrowLeft: -1,
@@ -162,7 +168,6 @@ const Segments = ({ included = [], label, onSelect, options, readOnly, selected
162
168
  const checked = option.id === selected;
163
169
  return /*#__PURE__*/ jsx(Button, {
164
170
  buttonStyle: checked ? "primary" : included.includes(option.id) ? "pill" : "transparent",
165
- className: checked && option.id === "trash" ? `${BASE_CLASS}__danger` : "",
166
171
  disabled: readOnly,
167
172
  extraButtonProps: {
168
173
  "aria-checked": checked,
@@ -185,10 +190,10 @@ const includedBelow = (options, level) => {
185
190
  return selected < 1 ? [] : options.slice(1, selected).map((option) => option.id);
186
191
  };
187
192
  const accessOptions = (rows) => ACCESS_OPTIONS.filter((option) => rows.some((row) => accessLevelsOf(row).includes(option.id)));
188
- const LABEL_WIDTH = "calc(var(--base) * 9)";
193
+ const LABEL_WIDTH = "calc(var(--base) * 11)";
189
194
  const OPERATION_WIDTH = "calc(var(--base) * 3.5)";
190
195
  const ACCESS_WIDTH = "calc(var(--base) * 13)";
191
- const DELETE_WIDTH = "calc(var(--base) * 10)";
196
+ const DELETE_WIDTH = "calc(var(--base) * 4)";
192
197
  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
198
  const lastCellStyle = {
194
199
  ...styles.cell,
@@ -213,225 +218,275 @@ const LiveBadge = () => /*#__PURE__*/ jsx("span", {
213
218
  })]
214
219
  })
215
220
  });
221
+ const APPROVED_ALWAYS = "Always approved: the plugin config does not allow unattended deletes";
222
+ const ApprovalToggle = ({ label, locked = false, onToggle, operation, pressed, readOnly }) => {
223
+ const title = locked ? APPROVED_ALWAYS : pressed ? `A person approves each ${operation}` : `Each ${operation} runs without approval`;
224
+ return /*#__PURE__*/ jsxs("button", {
225
+ "aria-pressed": pressed,
226
+ className: [`${BASE_CLASS}__shield`, !pressed && `${BASE_CLASS}__danger`].filter(Boolean).join(" "),
227
+ disabled: readOnly || locked,
228
+ onClick: onToggle,
229
+ title,
230
+ type: "button",
231
+ children: [/*#__PURE__*/ jsxs("svg", {
232
+ "aria-hidden": "true",
233
+ fill: "none",
234
+ height: "16",
235
+ stroke: "currentColor",
236
+ strokeLinecap: "round",
237
+ strokeLinejoin: "round",
238
+ strokeWidth: "2",
239
+ viewBox: "0 0 24 24",
240
+ width: "16",
241
+ children: [/*#__PURE__*/ jsx("path", { d: "M12 3l8 3v6c0 4.5-3.2 8-8 9-4.8-1-8-4.5-8-9V6z" }), pressed ? /*#__PURE__*/ jsx("path", { d: "M9 12l2 2 4-4" }) : null]
242
+ }), /*#__PURE__*/ jsx("span", {
243
+ style: styles.visuallyHidden,
244
+ children: `${label}: ${title}`
245
+ })]
246
+ });
247
+ };
216
248
  const NamespaceTable = ({ matrix, namespace, path, readOnly, toggle, values }) => {
217
249
  const rows = matrix[namespace.id];
218
250
  const withDelete = rows.some((row) => row.delete);
219
251
  const allOptions = accessOptions(rows);
220
252
  const allLevel = allAccessLevel(matrix, path, namespace.id, values);
221
253
  const entries = namespace.title.toLowerCase();
222
- return /*#__PURE__*/ jsx("section", {
223
- style: styles.section,
224
- children: /*#__PURE__*/ jsxs("table", {
225
- style: styles.table,
226
- children: [
227
- /*#__PURE__*/ jsx(Columns, { widths: withDelete ? [ACCESS_WIDTH, DELETE_WIDTH] : [ACCESS_WIDTH] }),
228
- /*#__PURE__*/ jsxs("thead", {
229
- style: styles.head,
230
- children: [/*#__PURE__*/ jsxs("tr", { children: [
231
- /*#__PURE__*/ jsx("th", {
232
- scope: "col",
233
- style: {
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,
262
- ...styles.rule
263
- },
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) => {
275
- toggle({
276
- kind: "access",
277
- namespace: namespace.id,
278
- level
279
- });
280
- },
281
- options: allOptions,
282
- readOnly,
283
- selected: allLevel
284
- })
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
- ] })]
306
- }),
307
- /*#__PURE__*/ jsx("tbody", { children: rows.map((row) => {
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);
352
- }) })
353
- ]
354
- })
355
- });
356
- };
357
- const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
358
- const toolState = toolsState(matrix, path, values);
359
- return /*#__PURE__*/ jsx("section", {
360
- style: styles.section,
361
- children: /*#__PURE__*/ jsxs("table", {
362
- style: styles.table,
363
- children: [
364
- /*#__PURE__*/ jsx(Columns, { widths: [OPERATION_WIDTH] }),
365
- /*#__PURE__*/ jsxs("thead", {
366
- style: styles.head,
367
- children: [/*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
254
+ const { i18n } = useTranslation();
255
+ const { getEntityConfig } = useConfig();
256
+ const nameOf = (row) => translateStatic(namespace.id === "collections" ? getEntityConfig({ collectionSlug: row.slug }).labels.plural : getEntityConfig({ globalSlug: row.slug }).label, i18n) ?? row.slug;
257
+ const allDelete = allDeleteMode(matrix, path, namespace.id, values);
258
+ return /*#__PURE__*/ jsx("section", { children: /*#__PURE__*/ jsxs("table", {
259
+ style: styles.table,
260
+ children: [
261
+ /*#__PURE__*/ jsx(Columns, { widths: withDelete ? [ACCESS_WIDTH, DELETE_WIDTH] : [ACCESS_WIDTH] }),
262
+ /*#__PURE__*/ jsxs("thead", {
263
+ style: styles.head,
264
+ children: [/*#__PURE__*/ jsxs("tr", { children: [
265
+ /*#__PURE__*/ jsx("th", {
368
266
  scope: "col",
369
267
  style: {
370
268
  ...styles.headTitle,
371
269
  ...styles.topStart
372
270
  },
373
- children: "Tools"
374
- }), /*#__PURE__*/ jsx("th", {
271
+ children: namespace.title
272
+ }),
273
+ /*#__PURE__*/ jsx("th", {
274
+ scope: "col",
275
+ style: {
276
+ ...styles.headOperation,
277
+ textAlign: "start",
278
+ ...withDelete ? {} : styles.topEnd
279
+ },
280
+ children: "Access"
281
+ }),
282
+ withDelete ? /*#__PURE__*/ jsx("th", {
375
283
  scope: "col",
376
284
  style: {
377
285
  ...styles.headOperation,
286
+ textAlign: "start",
378
287
  ...styles.topEnd
379
288
  },
380
- children: "Enabled"
381
- })] }), /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
289
+ children: "Delete"
290
+ }) : null
291
+ ] }), /*#__PURE__*/ jsxs("tr", { children: [
292
+ /*#__PURE__*/ jsx("th", {
382
293
  scope: "row",
383
294
  style: {
384
295
  ...styles.headLabel,
385
296
  ...styles.rule
386
297
  },
387
298
  children: "All"
388
- }), /*#__PURE__*/ jsx("td", {
299
+ }),
300
+ /*#__PURE__*/ jsx("td", {
301
+ style: {
302
+ ...controlStyle(!withDelete),
303
+ ...styles.rule
304
+ },
305
+ children: /*#__PURE__*/ jsx(Segments, {
306
+ included: includedBelow(allOptions, allLevel),
307
+ label: `Access for every ${entries} entry`,
308
+ onSelect: (level) => {
309
+ toggle({
310
+ kind: "access",
311
+ namespace: namespace.id,
312
+ level
313
+ });
314
+ },
315
+ options: allOptions,
316
+ readOnly,
317
+ selected: allLevel
318
+ })
319
+ }),
320
+ withDelete ? /*#__PURE__*/ jsx("td", {
389
321
  style: {
390
- ...lastCellStyle,
322
+ ...controlStyle(true),
391
323
  ...styles.rule
392
324
  },
393
325
  children: /*#__PURE__*/ jsx(Box, {
394
- label: "Enable every tool",
326
+ label: `Delete for every ${entries} entry`,
395
327
  onChange: () => {
396
328
  toggle({
397
- kind: "tools",
398
- value: toolState !== "on"
329
+ kind: "deleteMode",
330
+ namespace: namespace.id,
331
+ mode: allDelete === "off" || allDelete === void 0 ? "approve" : "off"
332
+ });
333
+ },
334
+ readOnly,
335
+ state: allDelete === void 0 ? "mixed" : allDelete === "off" ? "off" : "on"
336
+ })
337
+ }) : null
338
+ ] })]
339
+ }),
340
+ /*#__PURE__*/ jsx("tbody", { children: rows.map((row) => {
341
+ const options = accessOptions([row]);
342
+ const level = accessLevelOf(path, namespace.id, row, values);
343
+ const mode = deleteModeOf(path, namespace.id, row, values);
344
+ const name = nameOf(row);
345
+ return /*#__PURE__*/ jsxs("tr", { children: [
346
+ /*#__PURE__*/ jsxs("th", {
347
+ scope: "row",
348
+ style: styles.rowHeader,
349
+ children: [
350
+ name,
351
+ row.live ? /*#__PURE__*/ jsx(LiveBadge, {}) : null,
352
+ name === row.slug ? null : /*#__PURE__*/ jsx("span", {
353
+ style: styles.hint,
354
+ children: row.slug
355
+ })
356
+ ]
357
+ }),
358
+ /*#__PURE__*/ jsx("td", {
359
+ style: controlStyle(!withDelete),
360
+ children: /*#__PURE__*/ jsx(Segments, {
361
+ included: includedBelow(options, level),
362
+ label: `Access for ${name}`,
363
+ onSelect: (next) => {
364
+ toggle({
365
+ kind: "access",
366
+ namespace: namespace.id,
367
+ fieldName: row.fieldName,
368
+ level: next
399
369
  });
400
370
  },
371
+ options,
401
372
  readOnly,
402
- state: toolState
373
+ selected: level
403
374
  })
404
- })] })]
405
- }),
406
- /*#__PURE__*/ jsx("tbody", { children: matrix.tools.map((tool) => /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsxs("th", {
375
+ }),
376
+ withDelete ? /*#__PURE__*/ jsx("td", {
377
+ style: controlStyle(true),
378
+ children: row.delete ? /*#__PURE__*/ jsxs("span", {
379
+ style: styles.deleteControls,
380
+ children: [/*#__PURE__*/ jsx(Box, {
381
+ label: `Delete ${name}`,
382
+ onChange: () => {
383
+ toggle({
384
+ kind: "deleteMode",
385
+ namespace: namespace.id,
386
+ fieldName: row.fieldName,
387
+ mode: mode === "off" ? "approve" : "off"
388
+ });
389
+ },
390
+ readOnly,
391
+ state: mode === "off" ? "off" : "on"
392
+ }), mode === "off" ? null : /*#__PURE__*/ jsx(ApprovalToggle, {
393
+ label: `Approval for deleting ${name}`,
394
+ locked: !row.deleteUnattended,
395
+ onToggle: () => {
396
+ toggle({
397
+ kind: "deleteMode",
398
+ namespace: namespace.id,
399
+ fieldName: row.fieldName,
400
+ mode: mode === "trash" ? "approve" : "trash"
401
+ });
402
+ },
403
+ operation: "delete",
404
+ pressed: mode !== "trash",
405
+ readOnly
406
+ })]
407
+ }) : null
408
+ }) : null
409
+ ] }, row.fieldName);
410
+ }) })
411
+ ]
412
+ }) });
413
+ };
414
+ const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
415
+ const toolState = toolsState(matrix, path, values);
416
+ return /*#__PURE__*/ jsx("section", { children: /*#__PURE__*/ jsxs("table", {
417
+ style: styles.table,
418
+ children: [
419
+ /*#__PURE__*/ jsx(Columns, { widths: [OPERATION_WIDTH] }),
420
+ /*#__PURE__*/ jsxs("thead", {
421
+ style: styles.head,
422
+ children: [/*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
423
+ scope: "col",
424
+ style: {
425
+ ...styles.headTitle,
426
+ ...styles.topStart
427
+ },
428
+ children: "Tools"
429
+ }), /*#__PURE__*/ jsx("th", {
430
+ scope: "col",
431
+ style: {
432
+ ...styles.headOperation,
433
+ ...styles.topEnd
434
+ },
435
+ children: "Enabled"
436
+ })] }), /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
407
437
  scope: "row",
408
- style: styles.rowHeader,
409
- children: [tool.name, /*#__PURE__*/ jsx("span", {
410
- style: styles.hint,
411
- children: tool.description
412
- })]
438
+ style: {
439
+ ...styles.headLabel,
440
+ ...styles.rule
441
+ },
442
+ children: "All"
413
443
  }), /*#__PURE__*/ jsx("td", {
414
- style: lastCellStyle,
444
+ style: {
445
+ ...lastCellStyle,
446
+ ...styles.rule
447
+ },
415
448
  children: /*#__PURE__*/ jsx(Box, {
416
- label: `Enable ${tool.name}`,
449
+ label: "Enable every tool",
417
450
  onChange: () => {
418
451
  toggle({
419
- kind: "tool",
420
- name: tool.name,
421
- value: values[toolPath(path, tool.name)] !== true
452
+ kind: "tools",
453
+ value: toolState !== "on"
422
454
  });
423
455
  },
424
456
  readOnly,
425
- state: values[toolPath(path, tool.name)] === true ? "on" : "off"
457
+ state: toolState
426
458
  })
427
- })] }, tool.name)) })
428
- ]
429
- })
430
- });
459
+ })] })]
460
+ }),
461
+ /*#__PURE__*/ jsx("tbody", { children: matrix.tools.map((tool) => /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsxs("th", {
462
+ scope: "row",
463
+ style: styles.rowHeader,
464
+ children: [tool.name, /*#__PURE__*/ jsx("span", {
465
+ style: styles.hint,
466
+ children: tool.description
467
+ })]
468
+ }), /*#__PURE__*/ jsx("td", {
469
+ style: lastCellStyle,
470
+ children: /*#__PURE__*/ jsx(Box, {
471
+ label: `Enable ${tool.name}`,
472
+ onChange: () => {
473
+ toggle({
474
+ kind: "tool",
475
+ name: tool.name,
476
+ value: values[toolPath(path, tool.name)] !== true
477
+ });
478
+ },
479
+ readOnly,
480
+ state: values[toolPath(path, tool.name)] === true ? "on" : "off"
481
+ })
482
+ })] }, tool.name)) })
483
+ ]
484
+ }) });
431
485
  };
432
486
  /**
433
487
  * 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.
488
+ * an Access control and a Delete checkbox with an approval shield, and an
489
+ * "All" row that sets every row.
435
490
  * The nested checkbox fields back every control, so the stored document is
436
491
  * unaffected.
437
492
  */ const McpxCapabilityMatrix = ({ field, matrix, path = CAPABILITIES_FIELD, readOnly = false, withinTab = false }) => {
@@ -468,36 +523,35 @@ const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
468
523
  ].filter(Boolean).join(" "),
469
524
  children: [/*#__PURE__*/ jsx("style", { children: SHEET }), /*#__PURE__*/ jsxs("div", {
470
525
  className: "group-field__wrap",
471
- children: [
472
- /*#__PURE__*/ jsx("div", {
473
- className: "group-field__header",
474
- children: /*#__PURE__*/ jsxs("header", { children: [/*#__PURE__*/ jsx("h3", {
475
- className: "group-field__title",
476
- children: /*#__PURE__*/ jsx(FieldLabel, {
477
- as: "span",
478
- label: "Capabilities"
479
- })
480
- }), typeof field?.admin?.description === "string" ? /*#__PURE__*/ jsx(FieldDescription, {
481
- description: field.admin.description,
482
- path
483
- }) : null] })
484
- }),
485
- namespaces.map((namespace) => /*#__PURE__*/ jsx(NamespaceTable, {
526
+ children: [/*#__PURE__*/ jsx("div", {
527
+ className: "group-field__header",
528
+ children: /*#__PURE__*/ jsxs("header", { children: [withinTab ? null : /*#__PURE__*/ jsx("h3", {
529
+ className: "group-field__title",
530
+ children: /*#__PURE__*/ jsx(FieldLabel, {
531
+ as: "span",
532
+ label: "Capabilities"
533
+ })
534
+ }), typeof field?.admin?.description === "string" ? /*#__PURE__*/ jsx(FieldDescription, {
535
+ description: field.admin.description,
536
+ path
537
+ }) : null] })
538
+ }), /*#__PURE__*/ jsxs("div", {
539
+ style: styles.sections,
540
+ children: [namespaces.map((namespace) => /*#__PURE__*/ jsx(NamespaceTable, {
486
541
  matrix,
487
542
  namespace,
488
543
  path,
489
544
  readOnly,
490
545
  toggle,
491
546
  values
492
- }, namespace.id)),
493
- matrix.tools.length > 0 ? /*#__PURE__*/ jsx(ToolsTable, {
547
+ }, namespace.id)), matrix.tools.length > 0 ? /*#__PURE__*/ jsx(ToolsTable, {
494
548
  matrix,
495
549
  path,
496
550
  readOnly,
497
551
  toggle,
498
552
  values
499
- }) : null
500
- ]
553
+ }) : null]
554
+ })]
501
555
  })]
502
556
  });
503
557
  };
@@ -9,7 +9,7 @@ export interface CapabilityRow {
9
9
  /**
10
10
  * The slug, which is what MCP clients send and what refusals name.
11
11
  */
12
- label: string;
12
+ slug: string;
13
13
  read: boolean;
14
14
  write: boolean;
15
15
  publish: boolean;
@@ -2,8 +2,8 @@ import { CAPABILITIES_FIELD, canPublish, canWrite, isLiveWrite } from "../capabi
2
2
  //#region src/api-keys/capability-matrix.ts
3
3
  /**
4
4
  * The group's `admin.description`, rendered by the matrix in Payload's
5
- * description slot. It explains how to read the segmented controls.
6
- */ const CAPABILITIES_DESCRIPTION = "What this key may do. Each level includes the ones before it, and a missing segment means the plugin config does not expose it.";
5
+ * description slot. It explains how to read the controls.
6
+ */ const CAPABILITIES_DESCRIPTION = "What this key may do. Each level includes the ones before it, and a missing segment means the plugin config does not expose it. A shield means this key's user approves each call.";
7
7
  /**
8
8
  * The operations a collection or global can expose. The generated checkboxes
9
9
  * and the matrix segments share this wording. `requires` names the
@@ -34,7 +34,7 @@ import { CAPABILITIES_FIELD, canPublish, canWrite, isLiveWrite } from "../capabi
34
34
  }
35
35
  ];
36
36
  /**
37
- * Stored as a checkbox of its own but drawn as the Trash segment of the Delete
37
+ * Stored as a checkbox of its own but drawn as the approval shield of the Delete
38
38
  * control.
39
39
  */ const DELETE_UNATTENDED = {
40
40
  id: "deleteUnattended",
@@ -48,7 +48,7 @@ import { CAPABILITIES_FIELD, canPublish, canWrite, isLiveWrite } from "../capabi
48
48
  */ const STORED_OPERATIONS = [...CAPABILITY_OPERATIONS, DELETE_UNATTENDED];
49
49
  const toRow = (entity) => ({
50
50
  fieldName: entity.fieldName,
51
- label: entity.slug,
51
+ slug: entity.slug,
52
52
  read: entity.read,
53
53
  write: canWrite(entity),
54
54
  publish: canPublish(entity),
@@ -1,4 +1,4 @@
1
- import { createCapabilityFields, createConfirmationFields, createKeyFields, withSetupGuideTab } from "./fields.mjs";
1
+ import { createCapabilityFields, createConfirmationFields, createKeyFields, withKeyTabs } from "./fields.mjs";
2
2
  import { generateApiKey, hashApiKey } from "./key.mjs";
3
3
  //#region src/api-keys/collection.ts
4
4
  /**
@@ -37,22 +37,18 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
37
37
  delete: ownKeysOnly
38
38
  },
39
39
  hooks: { beforeChange: [keyBeforeChange] },
40
- fields: [...createConfirmationFields(options), ...withSetupGuideTab([
41
- {
42
- name: "user",
43
- type: "relationship",
44
- relationTo: userCollection,
45
- required: true,
46
- access: {
47
- create: () => false,
48
- update: () => false
49
- },
50
- defaultValue: ({ req }) => isUser({ req }) ? req.user?.id : void 0,
51
- admin: { description: "The user this key acts as." }
40
+ fields: [...createConfirmationFields(options), ...withKeyTabs([{
41
+ name: "user",
42
+ type: "relationship",
43
+ relationTo: userCollection,
44
+ required: true,
45
+ access: {
46
+ create: () => false,
47
+ update: () => false
52
48
  },
53
- ...createKeyFields(),
54
- ...createCapabilityFields(options)
55
- ], options)]
49
+ defaultValue: ({ req }) => isUser({ req }) ? req.user?.id : void 0,
50
+ admin: { description: "The user this key acts as." }
51
+ }, ...createKeyFields()], createCapabilityFields(options), options)]
56
52
  };
57
53
  };
58
54
  //#endregion
@@ -96,34 +96,40 @@ const SETUP_GUIDE_FIELD = "setupGuide";
96
96
  }
97
97
  }] : [];
98
98
  /**
99
- * Wraps the key fields and the setup guide in unnamed tabs, so wide snippets
100
- * get the full form width. Named tabs would nest the data and move
101
- * `capabilities` off the document root, which capability resolution reads.
99
+ * Wraps the key fields, the capabilities and the setup guide in unnamed tabs,
100
+ * so wide tables and snippets get the full form width. Named tabs would nest
101
+ * the data and move `capabilities` off the document root, which capability
102
+ * resolution reads.
102
103
  *
103
104
  * The guide tab only shows on update: on create there is no key to hand out.
104
- */ const withSetupGuideTab = (keyFields, options) => {
105
- if (!options.setupGuide) return keyFields;
105
+ */ const withKeyTabs = (keyFields, capabilityFields, options) => {
106
+ const tabs = [{
107
+ label: "Key",
108
+ fields: keyFields
109
+ }];
110
+ if (capabilityFields.length > 0) tabs.push({
111
+ label: "Capabilities",
112
+ fields: capabilityFields
113
+ });
114
+ if (options.setupGuide) tabs.push({
115
+ label: "Connect a client",
116
+ admin: { condition: (_data, _siblingData, { operation }) => operation === "update" },
117
+ fields: [{
118
+ name: SETUP_GUIDE_FIELD,
119
+ type: "ui",
120
+ admin: {
121
+ disableListColumn: true,
122
+ components: { Field: {
123
+ path: "@abinnovision/payloadcms-mcpx/admin",
124
+ exportName: "McpxSetupGuide",
125
+ clientProps: { endpointPath: options.endpointPath }
126
+ } }
127
+ }
128
+ }]
129
+ });
106
130
  return [{
107
131
  type: "tabs",
108
- tabs: [{
109
- label: "Key",
110
- fields: keyFields
111
- }, {
112
- label: "Connect a client",
113
- admin: { condition: (_data, _siblingData, { operation }) => operation === "update" },
114
- fields: [{
115
- name: SETUP_GUIDE_FIELD,
116
- type: "ui",
117
- admin: {
118
- disableListColumn: true,
119
- components: { Field: {
120
- path: "@abinnovision/payloadcms-mcpx/admin",
121
- exportName: "McpxSetupGuide",
122
- clientProps: { endpointPath: options.endpointPath }
123
- } }
124
- }
125
- }]
126
- }]
132
+ tabs
127
133
  }];
128
134
  };
129
135
  /**
@@ -140,7 +146,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
140
146
  const entityGroups = (rows) => rows.map((row) => ({
141
147
  name: row.fieldName,
142
148
  type: "group",
143
- label: row.label,
149
+ label: row.slug,
144
150
  fields: STORED_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => checkbox(operation.id, operation.description))
145
151
  }));
146
152
  const collectionGroups = entityGroups(matrix.collections);
@@ -174,7 +180,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
174
180
  exportName: "McpxCapabilityMatrix",
175
181
  clientProps: {
176
182
  matrix,
177
- withinTab: options.setupGuide
183
+ withinTab: true
178
184
  }
179
185
  } }
180
186
  },
@@ -182,4 +188,4 @@ const SETUP_GUIDE_FIELD = "setupGuide";
182
188
  }];
183
189
  };
184
190
  //#endregion
185
- export { createCapabilityFields, createConfirmationFields, createKeyFields, withSetupGuideTab };
191
+ export { createCapabilityFields, createConfirmationFields, createKeyFields, withKeyTabs };
@@ -9,6 +9,7 @@
9
9
  "findVersions",
10
10
  "patchDocument",
11
11
  "createDocument",
12
+ "duplicateDocument",
12
13
  "validateDocument",
13
14
  "publishDocument",
14
15
  "deleteDocument",
@@ -33,7 +33,11 @@ const NO_CONFIRMATION = {
33
33
  }
34
34
  };
35
35
  const WRITE_STEP = {
36
- tools: ["patchDocument", "createDocument"],
36
+ tools: [
37
+ "patchDocument",
38
+ "createDocument",
39
+ "duplicateDocument"
40
+ ],
37
41
  does: "the write"
38
42
  };
39
43
  const WORKFLOW = [
@@ -1,6 +1,7 @@
1
1
  import { createDocument } from "./create-document.mjs";
2
2
  import { deleteDocument } from "./delete-document.mjs";
3
3
  import { describeSchema } from "./describe-schema.mjs";
4
+ import { duplicateDocument } from "./duplicate-document.mjs";
4
5
  import { findDocuments } from "./find-documents.mjs";
5
6
  import { findVersions } from "./find-versions.mjs";
6
7
  import { getDocument } from "./get-document.mjs";
@@ -23,6 +24,7 @@ import { validateDocument } from "./validate-document.mjs";
23
24
  findVersions,
24
25
  patchDocument,
25
26
  createDocument,
27
+ duplicateDocument,
26
28
  validateDocument,
27
29
  publishDocument,
28
30
  deleteDocument,
@@ -19,6 +19,36 @@ const DESCRIPTION = (scope) => `Creates a document in a collection. Use it only
19
19
 
20
20
  ${liveWriteSentence(scope, "create")}${uploadSentence(scope)}`;
21
21
  /**
22
+ * What createDocument and duplicateDocument return: the new document, re-read
23
+ * privileged in `locale`, with its links and publish blockers.
24
+ */ const createdResult = async (scope, args) => {
25
+ const { target, locale } = args;
26
+ const saved = await readDraft(scope, {
27
+ target: {
28
+ ...target,
29
+ id: args.id
30
+ },
31
+ locale,
32
+ privileged: true
33
+ });
34
+ const validation = await collectPublishBlockers(scope.req, {
35
+ doc: saved,
36
+ entity: target
37
+ });
38
+ return jsonResult({
39
+ id: saved["id"],
40
+ status: saved["_status"],
41
+ updatedAt: saved["updatedAt"],
42
+ ...await documentLinks(scope.req, {
43
+ target,
44
+ doc: saved,
45
+ locale
46
+ }),
47
+ ...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
48
+ ...validation.unavailable ? { publishBlockersUnavailable: true } : {}
49
+ });
50
+ };
51
+ /**
22
52
  * Collection-only, because a global always exists. A create in an upload
23
53
  * collection needs `file`: the call then returns an upload grant, and the PUT
24
54
  * to it runs the call again with the bytes.
@@ -89,31 +119,12 @@ ${liveWriteSentence(scope, "create")}${uploadSentence(scope)}`;
89
119
  ...locale === void 0 ? {} : { locale },
90
120
  ...bytes === void 0 ? {} : { file: bytes }
91
121
  });
92
- const saved = await readDraft(scope, {
93
- target: {
94
- ...target,
95
- id: created["id"]
96
- },
97
- locale,
98
- privileged: true
99
- });
100
- const validation = await collectPublishBlockers(scope.req, {
101
- doc: saved,
102
- entity: target
103
- });
104
- return jsonResult({
105
- id: saved["id"],
106
- status: saved["_status"],
107
- updatedAt: saved["updatedAt"],
108
- ...await documentLinks(scope.req, {
109
- target,
110
- doc: saved,
111
- locale
112
- }),
113
- ...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
114
- ...validation.unavailable ? { publishBlockersUnavailable: true } : {}
122
+ return await createdResult(scope, {
123
+ target,
124
+ id: created["id"],
125
+ locale
115
126
  });
116
127
  }
117
128
  });
118
129
  //#endregion
119
- export { createDocument };
130
+ export { createDocument, createdResult };
@@ -0,0 +1,49 @@
1
+ import { defineMcpxTool } from "../define-tool.mjs";
2
+ import { idSchema, liveWriteSentence, localeOf, slugEnum, slugsFor } from "./shared.mjs";
3
+ import { resolveEntity } from "./document.mjs";
4
+ import { createdResult } from "./create-document.mjs";
5
+ import { withDuplicateIntent } from "../write/publish-intent.mjs";
6
+ import { Forbidden } from "payload";
7
+ //#region src/tools/duplicate-document.ts
8
+ const duplicableSlugs = (scope) => slugsFor(scope, "create").collections.filter((slug) => scope.req.payload.config.collections.find((collection) => collection.slug === slug)?.disableDuplicate !== true && !scope.exposure.collections.some((entity) => entity.slug === slug && entity.isUpload));
9
+ const DESCRIPTION = (scope) => `Copies a document into a new one in the same collection. Use it instead of getDocument and createDocument to copy a document. It copies the latest version, the draft if there is one, in every locale. Unique text fields get " - Copy" appended. Returns the same fields as createDocument. To rename the copy, call patchDocument with the returned "updatedAt".
10
+
11
+ ${liveWriteSentence(scope, "create")}`;
12
+ /**
13
+ * Wraps Payload's duplicate, which checks read access on the source and create
14
+ * access on the copy, and runs the fields' `beforeDuplicate` hooks.
15
+ */ const duplicateDocument = defineMcpxTool({
16
+ name: "duplicateDocument",
17
+ description: DESCRIPTION,
18
+ annotations: {
19
+ readOnlyHint: false,
20
+ destructiveHint: false,
21
+ idempotentHint: false,
22
+ openWorldHint: false
23
+ },
24
+ isEnabled: (scope) => duplicableSlugs(scope).length > 0,
25
+ inputSchema: (scope) => ({
26
+ collection: slugEnum(duplicableSlugs(scope)),
27
+ id: idSchema
28
+ }),
29
+ handler: async ({ args, scope }) => {
30
+ const target = resolveEntity(scope, { collection: args.collection }, "create");
31
+ if (target.kind === "global" || !duplicableSlugs(scope).includes(target.slug)) throw new Forbidden(scope.req.t);
32
+ const created = await scope.req.payload.duplicate({
33
+ collection: target.slug,
34
+ id: args.id,
35
+ data: withDuplicateIntent({}),
36
+ depth: 0,
37
+ draft: true,
38
+ overrideAccess: false,
39
+ req: scope.req
40
+ });
41
+ return await createdResult(scope, {
42
+ target,
43
+ id: created["id"],
44
+ locale: localeOf(scope, void 0)
45
+ });
46
+ }
47
+ });
48
+ //#endregion
49
+ export { duplicateDocument };
package/dist/types.d.mts CHANGED
@@ -34,8 +34,9 @@ export interface McpxCollectionOptions {
34
34
  */
35
35
  read?: boolean;
36
36
  /**
37
- * Expose `patchDocument`, `validateDocument` and `createDocument`. An upload
38
- * collection is created in only where MCP accepts its files. Without drafts
37
+ * Expose `patchDocument`, `validateDocument`, `createDocument` and
38
+ * `duplicateDocument`. An upload collection is created in only where MCP
39
+ * accepts its files, and is never duplicated. Without drafts
39
40
  * a write changes live content. Needs `read`. Default `true` while `read` is.
40
41
  */
41
42
  write?: boolean;
@@ -1,4 +1,4 @@
1
- import { hasPublishIntent, hasTrashIntent, takePublishIntent, takeTrashIntent, withTrashIntent } from "./publish-intent.mjs";
1
+ import { hasPublishIntent, hasTrashIntent, takeDuplicateIntent, takePublishIntent, takeTrashIntent, withTrashIntent } from "./publish-intent.mjs";
2
2
  import { isMcpxRequest } from "../request.mjs";
3
3
  import { APIError } from "payload";
4
4
  import { hasDraftsEnabled } from "payload/shared";
@@ -30,6 +30,11 @@ const scrubWriteArgs = (args, intent) => {
30
30
  const forceDraftWrite = (hookArgs) => {
31
31
  const { args, operation, req } = hookArgs;
32
32
  if (!isMcpxRequest(req) || operation !== "create" && operation !== "update") return args;
33
+ if (operation === "create" && takeDuplicateIntent(args.data)) {
34
+ const next = scrubWriteArgs(args, void 0);
35
+ next["duplicateFromID"] = args.duplicateFromID;
36
+ return next;
37
+ }
33
38
  const intent = operation !== "update" ? void 0 : hasPublishIntent(args.data) ? "publish" : hasTrashIntent(args.data) ? "trash" : void 0;
34
39
  return scrubWriteArgs(args, intent);
35
40
  };
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  //#region src/write/publish-intent.ts
4
4
  const PUBLISH_INTENT = "__mcpxPublishIntent";
5
5
  const TRASH_INTENT = "__mcpxTrashIntent";
6
+ const DUPLICATE_INTENT = "__mcpxDuplicateIntent";
6
7
  const TOKEN = randomUUID();
7
8
  const take = (data, key) => {
8
9
  if (!isPlainObject(data) || data[key] !== TOKEN) return false;
@@ -25,5 +26,12 @@ const hasTrashIntent = (data) => isPlainObject(data) && data[TRASH_INTENT] === T
25
26
  /**
26
27
  * The counterpart of {@link takePublishIntent}.
27
28
  */ const takeTrashIntent = (data) => take(data, TRASH_INTENT);
29
+ const withDuplicateIntent = (data) => ({
30
+ ...data,
31
+ [DUPLICATE_INTENT]: TOKEN
32
+ });
33
+ /**
34
+ * Removes the marker, so it never reaches the operation.
35
+ */ const takeDuplicateIntent = (data) => take(data, DUPLICATE_INTENT);
28
36
  //#endregion
29
- export { hasPublishIntent, hasTrashIntent, takePublishIntent, takeTrashIntent, withPublishIntent, withTrashIntent };
37
+ export { hasPublishIntent, hasTrashIntent, takeDuplicateIntent, takePublishIntent, takeTrashIntent, withDuplicateIntent, withPublishIntent, withTrashIntent };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@abinnovision/payloadcms-mcpx",
4
- "version": "2.0.0-beta.16",
4
+ "version": "2.0.0-beta.19",
5
5
  "description": "Payload CMS plugin exposing a fixed, schema-aware MCP tool surface with draft-only writes and per-API-key capabilities.",
6
6
  "keywords": [
7
7
  "payload",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "prettier": "@abinnovision/prettier-config",
59
59
  "dependencies": {
60
- "@modelcontextprotocol/sdk": "^1.30.1",
60
+ "@modelcontextprotocol/sdk": "^1.31.0",
61
61
  "rfc6902": "^5.3.0",
62
62
  "zod": "^4.6.5"
63
63
  },
@@ -69,20 +69,20 @@
69
69
  "@payloadcms/db-sqlite": "3.90.2",
70
70
  "@payloadcms/richtext-lexical": "3.90.2",
71
71
  "@payloadcms/ui": "3.90.2",
72
- "@swc/core": "^1.16.2",
73
- "@types/node": "^26.5.1",
72
+ "@swc/core": "^1.16.12",
73
+ "@types/node": "^26.6.3",
74
74
  "@types/react": "^19.3.0",
75
- "@vitest/coverage-v8": "^5.0.0",
76
- "eslint": "^10.10.0",
75
+ "@vitest/coverage-v8": "^5.0.2",
76
+ "eslint": "^10.11.0",
77
77
  "payload": "3.90.2",
78
- "prettier": "^3.9.5",
78
+ "prettier": "^3.9.9",
79
79
  "publint": "^0.3.24",
80
80
  "react": "^19.3.0",
81
81
  "react-dom": "^19.3.0",
82
82
  "tsdown": "^0.23.0",
83
83
  "typescript": "^6.0.3",
84
- "unplugin-swc": "^1.6.0",
85
- "vitest": "^5.0.0"
84
+ "unplugin-swc": "^2.0.0",
85
+ "vitest": "^5.0.2"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "@payloadcms/ui": ">=3.88.0 <4",