@abinnovision/payloadcms-mcpx 2.0.0-beta.9 → 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.
- package/CHANGELOG.md +43 -0
- package/README.md +56 -36
- package/dist/admin/capability-matrix.d.mts +5 -3
- package/dist/admin/capability-matrix.mjs +374 -272
- package/dist/admin/capability-toggles.mjs +79 -42
- package/dist/admin/confirmations-panel.mjs +232 -0
- package/dist/admin/confirmations.d.mts +15 -0
- package/dist/admin/confirmations.mjs +71 -0
- package/dist/admin/endpoint-url.mjs +7 -0
- package/dist/admin/index.d.mts +2 -1
- package/dist/admin/index.mjs +2 -1
- package/dist/admin/setup-guide.d.mts +3 -3
- package/dist/admin/setup-guide.mjs +51 -33
- package/dist/api-keys/capability-matrix.d.mts +7 -9
- package/dist/api-keys/capability-matrix.mjs +15 -37
- package/dist/api-keys/collection.mjs +16 -18
- package/dist/api-keys/confirmation-view.mjs +7 -0
- package/dist/api-keys/fields.mjs +50 -57
- package/dist/auth/resolve.mjs +69 -40
- package/dist/capabilities.d.mts +2 -0
- package/dist/capabilities.mjs +64 -16
- package/dist/define-tool.d.mts +4 -4
- package/dist/define-tool.mjs +8 -1
- package/dist/endpoint/confirmations.mjs +124 -0
- package/dist/endpoint/handler.mjs +25 -27
- package/dist/endpoint/http.mjs +27 -0
- package/dist/endpoint/server.mjs +48 -29
- package/dist/endpoint/upload.mjs +104 -0
- package/dist/grants/confirmation.mjs +75 -0
- package/dist/grants/grant.mjs +137 -0
- package/dist/guards.mjs +8 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/options.mjs +68 -61
- package/dist/plugin.d.mts +3 -2
- package/dist/plugin.mjs +19 -4
- package/dist/request.mjs +5 -1
- package/dist/result.mjs +11 -1
- package/dist/schema/admin-hidden.mjs +4 -4
- package/dist/schema/describe.mjs +12 -18
- package/dist/schema/index.mjs +5 -6
- package/dist/schema/lexical-pointer.mjs +8 -8
- package/dist/schema/lexical.mjs +15 -13
- package/dist/schema/outline.mjs +4 -5
- package/dist/schema/path.mjs +4 -1
- package/dist/schema/pointer.mjs +11 -17
- package/dist/schema/shape.mjs +38 -50
- package/dist/schema/walk.mjs +57 -18
- package/dist/tools/builtin.mjs +7 -1
- package/dist/tools/create-document.mjs +38 -26
- package/dist/tools/delete-document.mjs +154 -0
- package/dist/tools/describe-schema.mjs +7 -5
- package/dist/tools/document.mjs +131 -45
- package/dist/tools/duplicate-document.mjs +50 -0
- package/dist/tools/find-documents.mjs +15 -20
- package/dist/tools/find-versions.mjs +11 -15
- package/dist/tools/get-document.mjs +31 -15
- package/dist/tools/links.mjs +53 -0
- package/dist/tools/list-capabilities.mjs +17 -7
- package/dist/tools/patch-document.mjs +57 -53
- package/dist/tools/publish-document.mjs +17 -29
- package/dist/tools/query-paths.mjs +1 -1
- package/dist/tools/run-confirmed.mjs +114 -0
- package/dist/tools/shared.mjs +53 -46
- package/dist/tools/validate-document.mjs +6 -8
- package/dist/tools/versions.mjs +11 -6
- package/dist/types.d.mts +158 -43
- package/dist/upload/file.mjs +177 -0
- package/dist/version.mjs +4 -2
- package/dist/write/draft-guard.mjs +35 -28
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-intent.mjs +23 -11
- package/dist/write/row-ids.mjs +3 -1
- package/dist/write/write-data.mjs +18 -31
- package/package.json +14 -14
- package/dist/builtin-tool-names.mjs +0 -16
|
@@ -1,73 +1,110 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 ===
|
|
15
|
+
return granted === matrix.tools.length ? "on" : "mixed";
|
|
8
16
|
};
|
|
9
17
|
/**
|
|
10
|
-
*
|
|
11
|
-
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*/ const
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
52
|
+
for (const operation of rowOperations(row)) {
|
|
38
53
|
const change = changes[operation.id];
|
|
39
|
-
if (change !== void 0
|
|
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
|
|
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
|
|
53
|
-
*
|
|
54
|
-
* to
|
|
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
|
|
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
|
|
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.
|
|
65
|
-
const changes = intent.kind === "
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 {
|
|
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 };
|
package/dist/admin/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/admin/index.mjs
CHANGED
|
@@ -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 };
|
|
@@ -7,9 +7,9 @@ interface McpxSetupGuideProps {
|
|
|
7
7
|
endpointPath: string;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Per-key connection instructions on the API key edit view
|
|
11
|
-
*
|
|
12
|
-
* 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.
|
|
13
13
|
*/
|
|
14
14
|
export declare const McpxSetupGuide: React.FC<McpxSetupGuideProps>;
|
|
15
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:
|
|
45
|
+
endpointUrl: endpointUrl(origin, config.routes.api, endpointPath),
|
|
52
46
|
apiKey: asString(apiKey),
|
|
53
47
|
label: asString(label)
|
|
54
48
|
});
|
|
55
|
-
return /*#__PURE__*/ jsxs(
|
|
56
|
-
|
|
57
|
-
children:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/*#__PURE__*/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
section.description
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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,23 +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`
|
|
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
11
|
/**
|
|
11
12
|
* The slug, which is what MCP clients send and what refusals name.
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
-
read: boolean;
|
|
15
|
-
write: boolean;
|
|
16
|
-
publish: boolean;
|
|
14
|
+
slug: string;
|
|
17
15
|
/**
|
|
18
|
-
*
|
|
16
|
+
* Writes go live immediately, as the entity keeps no drafts.
|
|
19
17
|
*/
|
|
20
|
-
|
|
18
|
+
live?: boolean;
|
|
21
19
|
}
|
|
22
20
|
interface CapabilityTool {
|
|
23
21
|
name: string;
|