@abinnovision/payloadcms-mcpx 1.1.0-beta.6 → 2.0.0-beta.12
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 +144 -470
- package/dist/admin/capability-matrix.d.mts +33 -0
- package/dist/admin/capability-matrix.mjs +354 -0
- package/dist/admin/capability-toggles.mjs +73 -0
- package/dist/{client → admin}/setup-guide.d.mts +4 -2
- package/dist/{client → admin}/setup-guide.mjs +3 -10
- package/dist/api-keys/capability-matrix.d.mts +12 -8
- package/dist/api-keys/capability-matrix.mjs +14 -86
- package/dist/api-keys/collection.mjs +2 -1
- package/dist/api-keys/fields.mjs +36 -24
- package/dist/auth/resolve.mjs +71 -23
- package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
- package/dist/capabilities.mjs +44 -46
- package/dist/define-tool.d.mts +23 -0
- package/dist/{types.mjs → define-tool.mjs} +1 -1
- package/dist/endpoint/errors.mjs +5 -6
- package/dist/endpoint/handler.mjs +73 -29
- package/dist/endpoint/server.mjs +85 -22
- package/dist/endpoint/upload.mjs +135 -0
- package/dist/entity.d.mts +9 -0
- package/dist/guards.mjs +7 -0
- package/dist/i18n.mjs +10 -8
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +3 -2
- package/dist/options.mjs +54 -47
- package/dist/plugin.d.mts +2 -2
- package/dist/plugin.mjs +13 -3
- package/dist/request.d.mts +2 -3
- package/dist/request.mjs +2 -3
- package/dist/result.d.mts +2 -3
- package/dist/result.mjs +3 -3
- package/dist/schema/admin-hidden.mjs +91 -0
- package/dist/schema/describe.mjs +17 -33
- package/dist/schema/errors.mjs +8 -0
- package/dist/schema/index.mjs +5 -2
- package/dist/schema/lexical-pointer.mjs +18 -20
- package/dist/schema/lexical.mjs +44 -93
- package/dist/schema/outline.mjs +5 -15
- package/dist/schema/path.mjs +28 -0
- package/dist/schema/pointer.mjs +18 -25
- package/dist/schema/shape.mjs +25 -71
- package/dist/schema/walk.mjs +43 -45
- package/dist/tools/builtin.mjs +4 -4
- package/dist/tools/create-document.mjs +50 -24
- package/dist/tools/describe-schema.mjs +38 -34
- package/dist/tools/document.mjs +124 -0
- package/dist/tools/find-documents.mjs +32 -19
- package/dist/tools/find-versions.mjs +22 -18
- package/dist/tools/get-document.mjs +60 -33
- package/dist/tools/list-capabilities.mjs +35 -19
- package/dist/tools/patch-document.mjs +122 -66
- package/dist/tools/publish-document.mjs +26 -32
- package/dist/tools/query-paths.mjs +57 -0
- package/dist/tools/read-request.d.mts +10 -0
- package/dist/tools/read-request.mjs +43 -0
- package/dist/tools/shared.mjs +74 -105
- package/dist/tools/validate-document.mjs +18 -30
- package/dist/tools/versions.mjs +37 -20
- package/dist/types.d.mts +135 -75
- package/dist/upload/file.mjs +166 -0
- package/dist/upload/grant.mjs +96 -0
- package/dist/version.mjs +3 -1
- package/dist/write/draft-guard.mjs +23 -41
- package/dist/write/patch.mjs +27 -183
- package/dist/write/publish-blockers.mjs +7 -14
- package/dist/write/publish-intent.mjs +6 -4
- package/dist/write/row-ids.mjs +46 -0
- package/dist/write/transaction.mjs +5 -8
- package/dist/write/write-data.mjs +64 -0
- package/package.json +11 -10
- package/dist/client/capability-matrix.d.mts +0 -26
- package/dist/client/capability-matrix.mjs +0 -349
- package/dist/endpoint/index.mjs +0 -4
- package/dist/tools/target.mjs +0 -45
- /package/dist/{client → admin}/index.d.mts +0 -0
- /package/dist/{client → admin}/index.mjs +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CapabilityMatrix } from "../api-keys/capability-matrix.mjs";
|
|
2
|
+
import React from "react";
|
|
3
|
+
//#region src/admin/capability-matrix.d.ts
|
|
4
|
+
interface McpxCapabilityMatrixProps {
|
|
5
|
+
/**
|
|
6
|
+
* Built from the plugin options at config time.
|
|
7
|
+
*/
|
|
8
|
+
matrix: CapabilityMatrix;
|
|
9
|
+
/**
|
|
10
|
+
* The group's own config, supplied by Payload. Carries the description.
|
|
11
|
+
*/
|
|
12
|
+
field?: {
|
|
13
|
+
admin?: {
|
|
14
|
+
description?: unknown;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The `capabilities` group's own path, supplied by Payload.
|
|
19
|
+
*/
|
|
20
|
+
path?: string;
|
|
21
|
+
readOnly?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the key form is split into tabs, from the plugin options.
|
|
24
|
+
*/
|
|
25
|
+
withinTab?: boolean;
|
|
26
|
+
}
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
export declare const McpxCapabilityMatrix: React.FC<McpxCapabilityMatrixProps>;
|
|
33
|
+
//#endregion
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use client";
|
|
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";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { CheckboxInput, FieldDescription, FieldLabel, useForm, useFormFields } from "@payloadcms/ui";
|
|
7
|
+
import { useCallback, useMemo } from "react";
|
|
8
|
+
//#region src/admin/capability-matrix.tsx
|
|
9
|
+
const NOT_EXPOSED = "The plugin config does not expose this.";
|
|
10
|
+
const BASE_CLASS = "mcpx-capabilities";
|
|
11
|
+
const SHEET = `
|
|
12
|
+
.${BASE_CLASS} tbody tr:hover { background: var(--theme-elevation-50); }
|
|
13
|
+
.${BASE_CLASS}__toggle:hover { text-decoration: underline; }
|
|
14
|
+
.${BASE_CLASS} section:last-of-type { margin-bottom: 0; }
|
|
15
|
+
`;
|
|
16
|
+
const CELL_PADDING = "calc(var(--base) * 0.6)";
|
|
17
|
+
const EDGE_PADDING = "calc(var(--base) * 0.8)";
|
|
18
|
+
const styles = {
|
|
19
|
+
section: { marginBottom: "var(--base)" },
|
|
20
|
+
table: {
|
|
21
|
+
tableLayout: "fixed",
|
|
22
|
+
width: "auto",
|
|
23
|
+
borderCollapse: "separate",
|
|
24
|
+
borderSpacing: 0,
|
|
25
|
+
textAlign: "left"
|
|
26
|
+
},
|
|
27
|
+
head: { background: "var(--theme-elevation-50)" },
|
|
28
|
+
headTitle: {
|
|
29
|
+
padding: `${CELL_PADDING} ${CELL_PADDING} ${CELL_PADDING} ${EDGE_PADDING}`,
|
|
30
|
+
verticalAlign: "middle",
|
|
31
|
+
fontSize: "0.8em",
|
|
32
|
+
letterSpacing: "0.08em",
|
|
33
|
+
textTransform: "uppercase",
|
|
34
|
+
color: "var(--theme-elevation-400)",
|
|
35
|
+
fontWeight: "normal"
|
|
36
|
+
},
|
|
37
|
+
headOperation: {
|
|
38
|
+
padding: CELL_PADDING,
|
|
39
|
+
verticalAlign: "middle",
|
|
40
|
+
color: "var(--theme-elevation-400)",
|
|
41
|
+
fontWeight: "normal",
|
|
42
|
+
textAlign: "center",
|
|
43
|
+
whiteSpace: "nowrap"
|
|
44
|
+
},
|
|
45
|
+
headLabel: {
|
|
46
|
+
padding: `${CELL_PADDING} ${CELL_PADDING} ${CELL_PADDING} ${EDGE_PADDING}`,
|
|
47
|
+
verticalAlign: "middle",
|
|
48
|
+
color: "var(--theme-elevation-500)",
|
|
49
|
+
fontWeight: "normal"
|
|
50
|
+
},
|
|
51
|
+
rule: { borderBottom: "1px solid var(--theme-border-color)" },
|
|
52
|
+
topStart: { borderStartStartRadius: "var(--style-radius-s)" },
|
|
53
|
+
topEnd: { borderStartEndRadius: "var(--style-radius-s)" },
|
|
54
|
+
rowHeader: {
|
|
55
|
+
padding: `${CELL_PADDING} ${CELL_PADDING} ${CELL_PADDING} ${EDGE_PADDING}`,
|
|
56
|
+
verticalAlign: "middle",
|
|
57
|
+
fontWeight: "normal",
|
|
58
|
+
wordBreak: "break-word"
|
|
59
|
+
},
|
|
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
|
+
hint: {
|
|
71
|
+
display: "block",
|
|
72
|
+
color: "var(--theme-elevation-400)"
|
|
73
|
+
},
|
|
74
|
+
cell: {
|
|
75
|
+
padding: CELL_PADDING,
|
|
76
|
+
verticalAlign: "middle",
|
|
77
|
+
textAlign: "center"
|
|
78
|
+
},
|
|
79
|
+
lastCell: { paddingInlineEnd: EDGE_PADDING },
|
|
80
|
+
dash: {
|
|
81
|
+
color: "var(--theme-elevation-400)",
|
|
82
|
+
cursor: "help"
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const Box = ({ label, onChange, readOnly, state }) => /*#__PURE__*/ jsx(CheckboxInput, {
|
|
86
|
+
checked: state === "on",
|
|
87
|
+
name: label,
|
|
88
|
+
onToggle: onChange,
|
|
89
|
+
partialChecked: state === "mixed",
|
|
90
|
+
readOnly
|
|
91
|
+
});
|
|
92
|
+
const LABEL_WIDTH = "calc(var(--base) * 9)";
|
|
93
|
+
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 ? {
|
|
96
|
+
...styles.cell,
|
|
97
|
+
...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: "—"
|
|
104
|
+
});
|
|
105
|
+
const NamespaceTable = ({ matrix, namespace, path, readOnly, toggle, values }) => {
|
|
106
|
+
const rows = matrix[namespace.id];
|
|
107
|
+
const columns = CAPABILITY_OPERATIONS.filter((operation) => rows.some((row) => row[operation.id]));
|
|
108
|
+
return /*#__PURE__*/ jsx("section", {
|
|
109
|
+
style: styles.section,
|
|
110
|
+
children: /*#__PURE__*/ jsxs("table", {
|
|
111
|
+
style: styles.table,
|
|
112
|
+
children: [
|
|
113
|
+
/*#__PURE__*/ jsx(Columns, { count: columns.length }),
|
|
114
|
+
/*#__PURE__*/ jsxs("thead", {
|
|
115
|
+
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", {
|
|
141
|
+
style: {
|
|
142
|
+
...cellStyle(column === columns.length - 1),
|
|
143
|
+
...styles.rule
|
|
144
|
+
},
|
|
145
|
+
children: /*#__PURE__*/ jsx(Box, {
|
|
146
|
+
label: `${operation.label} every ${namespace.title.toLowerCase()} entry`,
|
|
147
|
+
onChange: () => {
|
|
148
|
+
toggle({
|
|
149
|
+
kind: "column",
|
|
150
|
+
namespace: namespace.id,
|
|
151
|
+
operation: operation.id,
|
|
152
|
+
value: state !== "on"
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
readOnly,
|
|
156
|
+
state
|
|
157
|
+
})
|
|
158
|
+
}, operation.id);
|
|
159
|
+
})] })]
|
|
160
|
+
}),
|
|
161
|
+
/*#__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);
|
|
202
|
+
}) })
|
|
203
|
+
]
|
|
204
|
+
})
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
const ToolsTable = ({ matrix, path, readOnly, toggle, values }) => {
|
|
208
|
+
const toolState = toolsState(matrix, path, values);
|
|
209
|
+
return /*#__PURE__*/ jsx("section", {
|
|
210
|
+
style: styles.section,
|
|
211
|
+
children: /*#__PURE__*/ jsxs("table", {
|
|
212
|
+
style: styles.table,
|
|
213
|
+
children: [
|
|
214
|
+
/*#__PURE__*/ jsx(Columns, { count: 1 }),
|
|
215
|
+
/*#__PURE__*/ jsxs("thead", {
|
|
216
|
+
style: styles.head,
|
|
217
|
+
children: [/*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
|
|
218
|
+
scope: "col",
|
|
219
|
+
style: {
|
|
220
|
+
...styles.headTitle,
|
|
221
|
+
...styles.topStart
|
|
222
|
+
},
|
|
223
|
+
children: "Tools"
|
|
224
|
+
}), /*#__PURE__*/ jsx("th", {
|
|
225
|
+
scope: "col",
|
|
226
|
+
style: {
|
|
227
|
+
...styles.headOperation,
|
|
228
|
+
...styles.topEnd
|
|
229
|
+
},
|
|
230
|
+
children: "Enabled"
|
|
231
|
+
})] }), /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
|
|
232
|
+
scope: "row",
|
|
233
|
+
style: {
|
|
234
|
+
...styles.headLabel,
|
|
235
|
+
...styles.rule
|
|
236
|
+
},
|
|
237
|
+
children: "All"
|
|
238
|
+
}), /*#__PURE__*/ jsx("td", {
|
|
239
|
+
style: {
|
|
240
|
+
...cellStyle(true),
|
|
241
|
+
...styles.rule
|
|
242
|
+
},
|
|
243
|
+
children: /*#__PURE__*/ jsx(Box, {
|
|
244
|
+
label: "Enable every tool",
|
|
245
|
+
onChange: () => {
|
|
246
|
+
toggle({
|
|
247
|
+
kind: "tools",
|
|
248
|
+
value: toolState !== "on"
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
readOnly,
|
|
252
|
+
state: toolState
|
|
253
|
+
})
|
|
254
|
+
})] })]
|
|
255
|
+
}),
|
|
256
|
+
/*#__PURE__*/ jsx("tbody", { children: matrix.tools.map((tool) => /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsxs("th", {
|
|
257
|
+
scope: "row",
|
|
258
|
+
style: styles.rowHeader,
|
|
259
|
+
children: [tool.name, /*#__PURE__*/ jsx("span", {
|
|
260
|
+
style: styles.hint,
|
|
261
|
+
children: tool.description
|
|
262
|
+
})]
|
|
263
|
+
}), /*#__PURE__*/ jsx("td", {
|
|
264
|
+
style: cellStyle(true),
|
|
265
|
+
children: /*#__PURE__*/ jsx(Box, {
|
|
266
|
+
label: `Enable ${tool.name}`,
|
|
267
|
+
onChange: () => {
|
|
268
|
+
toggle({
|
|
269
|
+
kind: "tool",
|
|
270
|
+
name: tool.name,
|
|
271
|
+
value: values[toolPath(path, tool.name)] !== true
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
readOnly,
|
|
275
|
+
state: values[toolPath(path, tool.name)] === true ? "on" : "off"
|
|
276
|
+
})
|
|
277
|
+
})] }, tool.name)) })
|
|
278
|
+
]
|
|
279
|
+
})
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
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.
|
|
286
|
+
*/ const McpxCapabilityMatrix = ({ field, matrix, path = CAPABILITIES_FIELD, readOnly = false, withinTab = false }) => {
|
|
287
|
+
const { dispatchFields, setModified } = useForm();
|
|
288
|
+
const paths = useMemo(() => capabilityPaths(matrix, path), [matrix, path]);
|
|
289
|
+
const encoded = useFormFields(([fields]) => paths.map((cell) => fields[cell]?.value === true ? "1" : "0").join(""));
|
|
290
|
+
const values = useMemo(() => Object.fromEntries(paths.map((cell, index) => [cell, encoded[index] === "1"])), [paths, encoded]);
|
|
291
|
+
const toggle = useCallback((intent) => {
|
|
292
|
+
const actions = buildToggleActions(matrix, path, values, intent);
|
|
293
|
+
if (actions.length === 0) return;
|
|
294
|
+
for (const action of actions) dispatchFields(action);
|
|
295
|
+
setModified(true);
|
|
296
|
+
}, [
|
|
297
|
+
matrix,
|
|
298
|
+
path,
|
|
299
|
+
values,
|
|
300
|
+
dispatchFields,
|
|
301
|
+
setModified
|
|
302
|
+
]);
|
|
303
|
+
const namespaces = [{
|
|
304
|
+
id: "collections",
|
|
305
|
+
title: "Collections"
|
|
306
|
+
}, {
|
|
307
|
+
id: "globals",
|
|
308
|
+
title: "Globals"
|
|
309
|
+
}].filter((namespace) => matrix[namespace.id].length > 0);
|
|
310
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
311
|
+
className: [
|
|
312
|
+
"field-type",
|
|
313
|
+
"group-field",
|
|
314
|
+
"group-field--top-level",
|
|
315
|
+
withinTab && "group-field--within-tab",
|
|
316
|
+
BASE_CLASS
|
|
317
|
+
].filter(Boolean).join(" "),
|
|
318
|
+
children: [/*#__PURE__*/ jsx("style", { children: SHEET }), /*#__PURE__*/ jsxs("div", {
|
|
319
|
+
className: "group-field__wrap",
|
|
320
|
+
children: [
|
|
321
|
+
/*#__PURE__*/ jsx("div", {
|
|
322
|
+
className: "group-field__header",
|
|
323
|
+
children: /*#__PURE__*/ jsxs("header", { children: [/*#__PURE__*/ jsx("h3", {
|
|
324
|
+
className: "group-field__title",
|
|
325
|
+
children: /*#__PURE__*/ jsx(FieldLabel, {
|
|
326
|
+
as: "span",
|
|
327
|
+
label: "Capabilities"
|
|
328
|
+
})
|
|
329
|
+
}), typeof field?.admin?.description === "string" ? /*#__PURE__*/ jsx(FieldDescription, {
|
|
330
|
+
description: field.admin.description,
|
|
331
|
+
path
|
|
332
|
+
}) : null] })
|
|
333
|
+
}),
|
|
334
|
+
namespaces.map((namespace) => /*#__PURE__*/ jsx(NamespaceTable, {
|
|
335
|
+
matrix,
|
|
336
|
+
namespace,
|
|
337
|
+
path,
|
|
338
|
+
readOnly,
|
|
339
|
+
toggle,
|
|
340
|
+
values
|
|
341
|
+
}, namespace.id)),
|
|
342
|
+
matrix.tools.length > 0 ? /*#__PURE__*/ jsx(ToolsTable, {
|
|
343
|
+
matrix,
|
|
344
|
+
path,
|
|
345
|
+
readOnly,
|
|
346
|
+
toggle,
|
|
347
|
+
values
|
|
348
|
+
}) : null
|
|
349
|
+
]
|
|
350
|
+
})]
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
//#endregion
|
|
354
|
+
export { McpxCapabilityMatrix };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CAPABILITY_OPERATIONS, cellPath, toolPath } from "../api-keys/capability-matrix.mjs";
|
|
2
|
+
//#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;
|
|
6
|
+
if (granted === 0) return "off";
|
|
7
|
+
return granted === paths.length ? "on" : "mixed";
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
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);
|
|
13
|
+
/**
|
|
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);
|
|
18
|
+
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;
|
|
28
|
+
};
|
|
29
|
+
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
|
+
};
|
|
35
|
+
const touched = /* @__PURE__ */ new Set();
|
|
36
|
+
const next = { ...current };
|
|
37
|
+
for (const operation of CAPABILITY_OPERATIONS) {
|
|
38
|
+
const change = changes[operation.id];
|
|
39
|
+
if (change !== void 0 && row[operation.id]) {
|
|
40
|
+
next[operation.id] = change;
|
|
41
|
+
touched.add(operation.id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const reconciled = reconcile(next, touched);
|
|
45
|
+
return CAPABILITY_OPERATIONS.filter((operation) => row[operation.id] && reconciled[operation.id] !== current[operation.id]).map((operation) => ({
|
|
46
|
+
type: "UPDATE",
|
|
47
|
+
path: cellPath(basePath, namespace, row.fieldName, operation.id),
|
|
48
|
+
value: reconciled[operation.id]
|
|
49
|
+
}));
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
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.
|
|
55
|
+
*
|
|
56
|
+
* Only cells whose value changes get an action, so a bulk toggle that changes
|
|
57
|
+
* nothing does not mark the form modified.
|
|
58
|
+
*/ 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) => ({
|
|
60
|
+
type: "UPDATE",
|
|
61
|
+
path: toolPath(basePath, tool.name),
|
|
62
|
+
value: intent.value
|
|
63
|
+
}));
|
|
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 };
|
|
70
|
+
return rows.flatMap((row) => rowActions(basePath, intent.namespace, row, values, changes));
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
export { buildToggleActions, columnState, rowState, toolsState };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
//#region src/
|
|
2
|
+
//#region src/admin/setup-guide.d.ts
|
|
3
3
|
interface McpxSetupGuideProps {
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Endpoint path below the API route, from the plugin options.
|
|
6
|
+
*/
|
|
5
7
|
endpointPath: string;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
@@ -3,23 +3,16 @@ import { buildSetupGuide } from "../api-keys/setup-guide.mjs";
|
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { CopyToClipboard, useConfig, useDocumentInfo, useFormFields } from "@payloadcms/ui";
|
|
5
5
|
import { useEffect, useState } from "react";
|
|
6
|
-
//#region src/
|
|
6
|
+
//#region src/admin/setup-guide.tsx
|
|
7
7
|
const asString = (value) => typeof value === "string" ? value : void 0;
|
|
8
|
-
|
|
9
|
-
* Reads `serverURL` when the config sets one and falls back to the browser's
|
|
10
|
-
* origin. The fallback has to wait for mount: this component is server-rendered
|
|
11
|
-
* first, where `window` does not exist.
|
|
12
|
-
*/ const useOrigin = (serverUrl) => {
|
|
8
|
+
const useOrigin = (serverUrl) => {
|
|
13
9
|
const [origin, setOrigin] = useState(serverUrl);
|
|
14
10
|
useEffect(() => {
|
|
15
11
|
if (serverUrl === "") setOrigin(window.location.origin);
|
|
16
12
|
}, [serverUrl]);
|
|
17
13
|
return origin;
|
|
18
14
|
};
|
|
19
|
-
|
|
20
|
-
* Payload's own theme variables, so the panel follows the admin's light and
|
|
21
|
-
* dark themes without shipping a stylesheet consumers would have to transpile.
|
|
22
|
-
*/ const styles = {
|
|
15
|
+
const styles = {
|
|
23
16
|
lead: { marginBottom: "calc(var(--base) * 0.75)" },
|
|
24
17
|
section: { marginBottom: "calc(var(--base) * 0.75)" },
|
|
25
18
|
sectionHeader: {
|
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
import "../options.mjs";
|
|
2
2
|
//#region src/api-keys/capability-matrix.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* One entity's row. The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 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
7
|
*/
|
|
8
8
|
export interface CapabilityRow {
|
|
9
9
|
fieldName: string;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The slug, which is what MCP clients send and what refusals name.
|
|
12
|
+
*/
|
|
11
13
|
label: string;
|
|
12
14
|
read: boolean;
|
|
13
15
|
write: boolean;
|
|
14
16
|
publish: boolean;
|
|
15
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Said only where a row departs from what its column header promises.
|
|
19
|
+
*/
|
|
16
20
|
hint?: string;
|
|
17
21
|
}
|
|
18
|
-
|
|
22
|
+
interface CapabilityTool {
|
|
19
23
|
name: string;
|
|
20
24
|
description: string;
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
27
|
* Everything the admin component needs to draw the matrix, derived from the
|
|
24
|
-
* plugin options at config time.
|
|
25
|
-
*
|
|
28
|
+
* plugin options at config time. Passed as a client prop, so every leaf must be
|
|
29
|
+
* JSON-serializable.
|
|
26
30
|
*/
|
|
27
31
|
export interface CapabilityMatrix {
|
|
28
32
|
collections: CapabilityRow[];
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { CAPABILITIES_FIELD,
|
|
1
|
+
import { CAPABILITIES_FIELD, canPublish, canWrite, isLiveWrite } from "../capabilities.mjs";
|
|
2
2
|
//#region src/api-keys/capability-matrix.ts
|
|
3
3
|
/**
|
|
4
|
-
* The group's `admin.description`,
|
|
5
|
-
* description slot
|
|
6
|
-
*
|
|
4
|
+
* The group's `admin.description`, rendered by the matrix in Payload's
|
|
5
|
+
* description slot. It explains both markings in the table: an unticked box
|
|
6
|
+
* and a dash.
|
|
7
7
|
*/ const CAPABILITIES_DESCRIPTION = "What this key may do. An unticked box is a refusal, and a dash means the plugin config does not expose that operation at all.";
|
|
8
8
|
/**
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
* wording a key's owner sees cannot drift from what the field config carries.
|
|
9
|
+
* The operations a collection or global can expose. The generated checkboxes
|
|
10
|
+
* and the matrix column headers share this wording.
|
|
12
11
|
*/ const CAPABILITY_OPERATIONS = [
|
|
13
12
|
{
|
|
14
13
|
id: "read",
|
|
@@ -26,19 +25,20 @@ import { CAPABILITIES_FIELD, canCreate, canPublish, canWrite } from "../capabili
|
|
|
26
25
|
description: "Promote the current draft to what the public sees."
|
|
27
26
|
}
|
|
28
27
|
];
|
|
29
|
-
const
|
|
28
|
+
const LIVE_HINT = "Writes go live immediately.";
|
|
29
|
+
const hintFor = (entity) => isLiveWrite(entity) ? { hint: LIVE_HINT } : {};
|
|
30
30
|
const toRow = (entity) => ({
|
|
31
31
|
fieldName: entity.fieldName,
|
|
32
32
|
label: entity.slug,
|
|
33
33
|
read: entity.read,
|
|
34
34
|
write: canWrite(entity),
|
|
35
35
|
publish: canPublish(entity),
|
|
36
|
-
...
|
|
36
|
+
...hintFor(entity)
|
|
37
37
|
});
|
|
38
38
|
/**
|
|
39
39
|
* The single description of what the config exposes. `createCapabilityFields`
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* and the admin component both draw from it, so a cell cannot appear without a
|
|
41
|
+
* field behind it.
|
|
42
42
|
*/ const createCapabilityMatrix = (options) => ({
|
|
43
43
|
collections: options.collections.map(toRow),
|
|
44
44
|
globals: options.globals.map(toRow),
|
|
@@ -49,80 +49,8 @@ const toRow = (entity) => ({
|
|
|
49
49
|
});
|
|
50
50
|
const cellPath = (basePath, namespace, fieldName, operation) => `${basePath}.${namespace}.${fieldName}.${operation}`;
|
|
51
51
|
const toolPath = (basePath, name) => `${basePath}.tools.${name}`;
|
|
52
|
-
/** Every path the matrix can address, in a stable order. */ const capabilityPaths = (matrix, basePath = CAPABILITIES_FIELD) => [...["collections", "globals"].flatMap((namespace) => matrix[namespace].flatMap((row) => CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)))), ...matrix.tools.map((tool) => toolPath(basePath, tool.name))];
|
|
53
|
-
const stateOf = (paths, values) => {
|
|
54
|
-
if (paths.length === 0) return "off";
|
|
55
|
-
const granted = paths.filter((path) => values[path] === true).length;
|
|
56
|
-
if (granted === 0) return "off";
|
|
57
|
-
return granted === paths.length ? "on" : "mixed";
|
|
58
|
-
};
|
|
59
52
|
/**
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
*/ const columnState = (matrix, basePath, namespace, operation, values) => stateOf(matrix[namespace].filter((row) => row[operation]).map((row) => cellPath(basePath, namespace, row.fieldName, operation)), values);
|
|
63
|
-
/**
|
|
64
|
-
* Whether an entity's row toggle reads as on, off or indeterminate, over the
|
|
65
|
-
* operations the config exposes for that row.
|
|
66
|
-
*/ const rowState = (basePath, namespace, row, values) => stateOf(CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)), values);
|
|
67
|
-
const toolsState = (matrix, basePath, values) => stateOf(matrix.tools.map((tool) => toolPath(basePath, tool.name)), values);
|
|
68
|
-
/**
|
|
69
|
-
* Publishing is an extension of writing, and `publishFlag` in `capabilities.ts`
|
|
70
|
-
* discards a publish without a write. Ticking publish therefore ticks write,
|
|
71
|
-
* and clearing write clears publish, so the form cannot save a combination the
|
|
72
|
-
* server ignores.
|
|
73
|
-
*/ const reconcile = (next, touched) => {
|
|
74
|
-
if (touched.has("publish") && next.publish) return {
|
|
75
|
-
...next,
|
|
76
|
-
write: true
|
|
77
|
-
};
|
|
78
|
-
if (touched.has("write") && !next.write) return {
|
|
79
|
-
...next,
|
|
80
|
-
publish: false
|
|
81
|
-
};
|
|
82
|
-
return next;
|
|
83
|
-
};
|
|
84
|
-
const rowActions = (basePath, namespace, row, values, changes) => {
|
|
85
|
-
const current = {
|
|
86
|
-
read: values[cellPath(basePath, namespace, row.fieldName, "read")] === true,
|
|
87
|
-
write: values[cellPath(basePath, namespace, row.fieldName, "write")] === true,
|
|
88
|
-
publish: values[cellPath(basePath, namespace, row.fieldName, "publish")] === true
|
|
89
|
-
};
|
|
90
|
-
const touched = /* @__PURE__ */ new Set();
|
|
91
|
-
const next = { ...current };
|
|
92
|
-
for (const operation of CAPABILITY_OPERATIONS) {
|
|
93
|
-
const change = changes[operation.id];
|
|
94
|
-
if (change !== void 0 && row[operation.id]) {
|
|
95
|
-
next[operation.id] = change;
|
|
96
|
-
touched.add(operation.id);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const reconciled = reconcile(next, touched);
|
|
100
|
-
return CAPABILITY_OPERATIONS.filter((operation) => row[operation.id] && reconciled[operation.id] !== current[operation.id]).map((operation) => ({
|
|
101
|
-
type: "UPDATE",
|
|
102
|
-
path: cellPath(basePath, namespace, row.fieldName, operation.id),
|
|
103
|
-
value: reconciled[operation.id]
|
|
104
|
-
}));
|
|
105
|
-
};
|
|
106
|
-
/**
|
|
107
|
-
* The form-state updates one click implies, as literal actions. Every toggle in
|
|
108
|
-
* the matrix — cell, row, column or tool — comes through here, so the publish
|
|
109
|
-
* rule holds for bulk grants as much as for a single box.
|
|
110
|
-
*
|
|
111
|
-
* Only cells whose value actually changes get an action, which keeps a bulk
|
|
112
|
-
* toggle from marking the form modified when it changes nothing.
|
|
113
|
-
*/ const buildToggleActions = (matrix, basePath, values, intent) => {
|
|
114
|
-
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) => ({
|
|
115
|
-
type: "UPDATE",
|
|
116
|
-
path: toolPath(basePath, tool.name),
|
|
117
|
-
value: intent.value
|
|
118
|
-
}));
|
|
119
|
-
const rows = matrix[intent.namespace].filter((row) => intent.kind === "column" || row.fieldName === intent.fieldName);
|
|
120
|
-
const changes = intent.kind === "row" ? {
|
|
121
|
-
read: intent.value,
|
|
122
|
-
write: intent.value,
|
|
123
|
-
publish: intent.value
|
|
124
|
-
} : { [intent.operation]: intent.value };
|
|
125
|
-
return rows.flatMap((row) => rowActions(basePath, intent.namespace, row, values, changes));
|
|
126
|
-
};
|
|
53
|
+
* Every path the matrix can address, in a stable order.
|
|
54
|
+
*/ const capabilityPaths = (matrix, basePath = CAPABILITIES_FIELD) => [...["collections", "globals"].flatMap((namespace) => matrix[namespace].flatMap((row) => CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)))), ...matrix.tools.map((tool) => toolPath(basePath, tool.name))];
|
|
127
55
|
//#endregion
|
|
128
|
-
export { CAPABILITIES_DESCRIPTION, CAPABILITY_OPERATIONS,
|
|
56
|
+
export { CAPABILITIES_DESCRIPTION, CAPABILITY_OPERATIONS, capabilityPaths, cellPath, createCapabilityMatrix, toolPath };
|