payload-mcp-toolkit 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +100 -237
  2. package/dist/api-keys.js +16 -0
  3. package/dist/api-keys.js.map +1 -1
  4. package/dist/auth-strategy.js +49 -49
  5. package/dist/auth-strategy.js.map +1 -1
  6. package/dist/components/AgentConnectPill.d.ts +3 -0
  7. package/dist/components/AgentConnectPill.js +121 -0
  8. package/dist/components/AgentConnectPill.js.map +1 -0
  9. package/dist/components/CollectionScopesMatrix.js.map +1 -1
  10. package/dist/components/ConsentPermissions.d.ts +10 -0
  11. package/dist/components/ConsentPermissions.js +165 -0
  12. package/dist/components/ConsentPermissions.js.map +1 -0
  13. package/dist/components/GlobalScopesMatrix.js.map +1 -1
  14. package/dist/components/OAuthView.d.ts +4 -0
  15. package/dist/components/OAuthView.js +269 -0
  16. package/dist/components/OAuthView.js.map +1 -0
  17. package/dist/components/ScopesTable.d.ts +28 -0
  18. package/dist/components/ScopesTable.js +165 -130
  19. package/dist/components/ScopesTable.js.map +1 -1
  20. package/dist/components/agentInstructions.d.ts +2 -0
  21. package/dist/components/agentInstructions.js +34 -0
  22. package/dist/components/agentInstructions.js.map +1 -0
  23. package/dist/components/index.d.ts +2 -0
  24. package/dist/components/index.js +2 -0
  25. package/dist/components/index.js.map +1 -1
  26. package/dist/components/oauth.css +111 -0
  27. package/dist/conflict-detection.js +13 -13
  28. package/dist/conflict-detection.js.map +1 -1
  29. package/dist/draft-workflow.js +26 -26
  30. package/dist/draft-workflow.js.map +1 -1
  31. package/dist/endpoint.d.ts +5 -0
  32. package/dist/endpoint.js +21 -13
  33. package/dist/endpoint.js.map +1 -1
  34. package/dist/hash.js +15 -15
  35. package/dist/hash.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +67 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/introspection.js +47 -47
  40. package/dist/introspection.js.map +1 -1
  41. package/dist/oauth-permissions.d.ts +52 -0
  42. package/dist/oauth-permissions.js +100 -0
  43. package/dist/oauth-permissions.js.map +1 -0
  44. package/dist/oauth-store.d.ts +19 -0
  45. package/dist/oauth-store.js +122 -0
  46. package/dist/oauth-store.js.map +1 -0
  47. package/dist/oauth.d.ts +19 -0
  48. package/dist/oauth.js +523 -0
  49. package/dist/oauth.js.map +1 -0
  50. package/dist/prompts.js +5 -5
  51. package/dist/prompts.js.map +1 -1
  52. package/dist/registry.js +7 -1
  53. package/dist/registry.js.map +1 -1
  54. package/dist/resources.js +10 -10
  55. package/dist/resources.js.map +1 -1
  56. package/dist/scope/audit-log.js +9 -9
  57. package/dist/scope/audit-log.js.map +1 -1
  58. package/dist/scope/policy.js +7 -7
  59. package/dist/scope/policy.js.map +1 -1
  60. package/dist/tools/_helpers.d.ts +10 -0
  61. package/dist/tools/_helpers.js +53 -41
  62. package/dist/tools/_helpers.js.map +1 -1
  63. package/dist/tools/_layout-helpers.js +33 -33
  64. package/dist/tools/_layout-helpers.js.map +1 -1
  65. package/dist/tools/create-document.js +20 -19
  66. package/dist/tools/create-document.js.map +1 -1
  67. package/dist/tools/delete-document.js +10 -9
  68. package/dist/tools/delete-document.js.map +1 -1
  69. package/dist/tools/find-document.js +18 -15
  70. package/dist/tools/find-document.js.map +1 -1
  71. package/dist/tools/find-global.js +13 -13
  72. package/dist/tools/find-global.js.map +1 -1
  73. package/dist/tools/global-versions.js +8 -7
  74. package/dist/tools/global-versions.js.map +1 -1
  75. package/dist/tools/patch-global-layout.js +9 -9
  76. package/dist/tools/patch-global-layout.js.map +1 -1
  77. package/dist/tools/patch-layout.js +15 -14
  78. package/dist/tools/patch-layout.js.map +1 -1
  79. package/dist/tools/publish-draft.js +2 -1
  80. package/dist/tools/publish-draft.js.map +1 -1
  81. package/dist/tools/publish-global-draft.js +4 -4
  82. package/dist/tools/publish-global-draft.js.map +1 -1
  83. package/dist/tools/resolve-reference.js.map +1 -1
  84. package/dist/tools/safe-delete.js +14 -14
  85. package/dist/tools/safe-delete.js.map +1 -1
  86. package/dist/tools/schedule-publish.js +21 -20
  87. package/dist/tools/schedule-publish.js.map +1 -1
  88. package/dist/tools/search-content.js +12 -12
  89. package/dist/tools/search-content.js.map +1 -1
  90. package/dist/tools/update-document.js +6 -5
  91. package/dist/tools/update-document.js.map +1 -1
  92. package/dist/tools/update-global.js +9 -9
  93. package/dist/tools/update-global.js.map +1 -1
  94. package/dist/tools/upload-media.js +2 -1
  95. package/dist/tools/upload-media.js.map +1 -1
  96. package/dist/tools/versions.js +10 -8
  97. package/dist/tools/versions.js.map +1 -1
  98. package/dist/types.d.ts +3 -0
  99. package/dist/types.js.map +1 -1
  100. package/docs/oauth.md +96 -0
  101. package/package.json +7 -2
@@ -0,0 +1,269 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import React, { useEffect, useState } from 'react';
4
+ import { Banner, Button, Gutter } from '@payloadcms/ui';
5
+ import { agentInstructions } from './agentInstructions';
6
+ import { ConsentPermissions } from './ConsentPermissions';
7
+ import './oauth.css';
8
+ function CopyButton({ text, label }) {
9
+ const [status, setStatus] = useState('');
10
+ async function copy() {
11
+ setStatus('');
12
+ try {
13
+ await navigator.clipboard.writeText(text);
14
+ setStatus('Copied.');
15
+ } catch {
16
+ setStatus('Copy failed. Select the text and copy it.');
17
+ }
18
+ }
19
+ return /*#__PURE__*/ _jsxs("div", {
20
+ className: "mcp-oauth__copy",
21
+ children: [
22
+ /*#__PURE__*/ _jsx(Button, {
23
+ type: "button",
24
+ buttonStyle: "secondary",
25
+ size: "small",
26
+ margin: false,
27
+ onClick: copy,
28
+ children: label
29
+ }),
30
+ /*#__PURE__*/ _jsx("span", {
31
+ role: "status",
32
+ children: status
33
+ })
34
+ ]
35
+ });
36
+ }
37
+ export function OAuthView({ mode }) {
38
+ const [data, setData] = useState(null);
39
+ const [error, setError] = useState('');
40
+ const [nothingPicked, setNothingPicked] = useState(false);
41
+ const endpoint = `/api/mcp/oauth/${mode}`;
42
+ useEffect(()=>{
43
+ const controller = new AbortController();
44
+ async function load() {
45
+ try {
46
+ const response = await fetch(`${endpoint}${window.location.search}`, {
47
+ headers: {
48
+ Accept: 'application/json'
49
+ },
50
+ credentials: 'same-origin',
51
+ cache: 'no-store',
52
+ signal: controller.signal
53
+ });
54
+ if (response.redirected) {
55
+ const destination = new URL(response.url);
56
+ if (destination.origin === window.location.origin) window.location.assign(destination.href);
57
+ return;
58
+ }
59
+ if (!response.ok) {
60
+ setError(response.status === 403 ? 'Your account cannot authorize this connection.' : 'This request could not be loaded. Start the connection again from your AI app.');
61
+ return;
62
+ }
63
+ setData(await response.json());
64
+ } catch {
65
+ if (!controller.signal.aborted) setError('The connection could not be loaded. Check your connection and reload this page.');
66
+ }
67
+ }
68
+ void load();
69
+ return ()=>controller.abort();
70
+ }, [
71
+ endpoint
72
+ ]);
73
+ const approval = data && 'account' in data ? data : null;
74
+ const connections = data && 'grants' in data ? data : null;
75
+ const instructions = connections ? agentInstructions(connections.resource, connections.access) : '';
76
+ return /*#__PURE__*/ _jsx(Gutter, {
77
+ children: /*#__PURE__*/ _jsxs("main", {
78
+ className: "mcp-oauth",
79
+ children: [
80
+ /*#__PURE__*/ _jsx("h1", {
81
+ children: mode === 'authorize' ? 'Connect your account' : 'AI connections'
82
+ }),
83
+ error ? /*#__PURE__*/ _jsx(Banner, {
84
+ type: "error",
85
+ children: /*#__PURE__*/ _jsx("span", {
86
+ role: "alert",
87
+ children: error
88
+ })
89
+ }) : !data ? /*#__PURE__*/ _jsx("p", {
90
+ role: "status",
91
+ children: "Loading connection…"
92
+ }) : null,
93
+ approval && /*#__PURE__*/ _jsxs(_Fragment, {
94
+ children: [
95
+ /*#__PURE__*/ _jsxs("p", {
96
+ children: [
97
+ /*#__PURE__*/ _jsx("strong", {
98
+ children: approval.clientName
99
+ }),
100
+ " is requesting access to your website account."
101
+ ]
102
+ }),
103
+ /*#__PURE__*/ _jsxs("dl", {
104
+ className: "mcp-oauth__details",
105
+ children: [
106
+ /*#__PURE__*/ _jsx("dt", {
107
+ children: "Signed in as"
108
+ }),
109
+ /*#__PURE__*/ _jsx("dd", {
110
+ children: approval.account
111
+ }),
112
+ /*#__PURE__*/ _jsx("dt", {
113
+ children: "Permissions"
114
+ }),
115
+ /*#__PURE__*/ _jsx("dd", {
116
+ children: "What you choose below. Your website permissions still apply. Deleting content and changing global settings are not allowed."
117
+ }),
118
+ /*#__PURE__*/ _jsx("dt", {
119
+ children: "Duration"
120
+ }),
121
+ /*#__PURE__*/ _jsx("dd", {
122
+ children: "Up to 30 days. You can disconnect at any time."
123
+ })
124
+ ]
125
+ }),
126
+ /*#__PURE__*/ _jsxs("form", {
127
+ action: endpoint,
128
+ method: "post",
129
+ onKeyDown: (e)=>{
130
+ if (e.key === 'Enter' && e.target.matches('input[type=checkbox]')) e.preventDefault();
131
+ },
132
+ children: [
133
+ /*#__PURE__*/ _jsx("input", {
134
+ type: "hidden",
135
+ name: "consent",
136
+ value: approval.consent
137
+ }),
138
+ /*#__PURE__*/ _jsx(ConsentPermissions, {
139
+ catalog: approval.catalog,
140
+ max: approval.scope.split(' ').includes('mcp:write') ? 'editor' : 'read-only',
141
+ onEmptyChange: setNothingPicked
142
+ }),
143
+ /*#__PURE__*/ _jsxs("div", {
144
+ className: "mcp-oauth__actions",
145
+ children: [
146
+ /*#__PURE__*/ _jsx(Button, {
147
+ type: "submit",
148
+ margin: false,
149
+ disabled: nothingPicked,
150
+ extraButtonProps: {
151
+ name: 'decision',
152
+ value: 'allow'
153
+ },
154
+ children: "Allow access"
155
+ }),
156
+ /*#__PURE__*/ _jsx(Button, {
157
+ type: "submit",
158
+ buttonStyle: "secondary",
159
+ margin: false,
160
+ extraButtonProps: {
161
+ name: 'decision',
162
+ value: 'deny'
163
+ },
164
+ children: "Cancel"
165
+ })
166
+ ]
167
+ })
168
+ ]
169
+ })
170
+ ]
171
+ }),
172
+ connections && /*#__PURE__*/ _jsxs(_Fragment, {
173
+ children: [
174
+ connections.eligible ? /*#__PURE__*/ _jsxs(_Fragment, {
175
+ children: [
176
+ /*#__PURE__*/ _jsx("p", {
177
+ children: "Add this URL as a custom connector in your AI app. Then sign in and approve access."
178
+ }),
179
+ /*#__PURE__*/ _jsxs("ul", {
180
+ children: [
181
+ /*#__PURE__*/ _jsxs("li", {
182
+ children: [
183
+ /*#__PURE__*/ _jsx("strong", {
184
+ children: "Claude:"
185
+ }),
186
+ " Settings → Connectors → Add custom connector."
187
+ ]
188
+ }),
189
+ /*#__PURE__*/ _jsxs("li", {
190
+ children: [
191
+ /*#__PURE__*/ _jsx("strong", {
192
+ children: "ChatGPT (web):"
193
+ }),
194
+ " turn on Developer mode in Settings, then create an app with this URL and OAuth sign-in."
195
+ ]
196
+ })
197
+ ]
198
+ }),
199
+ /*#__PURE__*/ _jsx("code", {
200
+ className: "mcp-oauth__url",
201
+ children: connections.resource
202
+ }),
203
+ /*#__PURE__*/ _jsx(CopyButton, {
204
+ text: connections.resource,
205
+ label: "Copy URL"
206
+ }),
207
+ /*#__PURE__*/ _jsx("h2", {
208
+ children: "Agent instructions"
209
+ }),
210
+ /*#__PURE__*/ _jsx("p", {
211
+ children: "Paste these into your AI app, for example as project instructions. They tell the assistant how to connect and how to work with your content."
212
+ }),
213
+ /*#__PURE__*/ _jsx("pre", {
214
+ className: "mcp-oauth__instructions",
215
+ children: instructions
216
+ }),
217
+ /*#__PURE__*/ _jsx(CopyButton, {
218
+ text: instructions,
219
+ label: "Copy instructions"
220
+ })
221
+ ]
222
+ }) : /*#__PURE__*/ _jsx("p", {
223
+ children: "Your account cannot connect AI apps. Ask a site administrator for access."
224
+ }),
225
+ /*#__PURE__*/ _jsx("h2", {
226
+ children: "Connected apps"
227
+ }),
228
+ connections.grants.length ? connections.grants.map((grant)=>/*#__PURE__*/ _jsxs("div", {
229
+ className: "mcp-oauth__connection",
230
+ children: [
231
+ /*#__PURE__*/ _jsxs("div", {
232
+ children: [
233
+ /*#__PURE__*/ _jsx("strong", {
234
+ children: grant.clientName
235
+ }),
236
+ /*#__PURE__*/ _jsx("p", {
237
+ children: grant.summary
238
+ })
239
+ ]
240
+ }),
241
+ /*#__PURE__*/ _jsxs("form", {
242
+ action: endpoint,
243
+ method: "post",
244
+ children: [
245
+ /*#__PURE__*/ _jsx("input", {
246
+ type: "hidden",
247
+ name: "consent",
248
+ value: grant.consent
249
+ }),
250
+ /*#__PURE__*/ _jsx(Button, {
251
+ type: "submit",
252
+ buttonStyle: "secondary",
253
+ margin: false,
254
+ children: "Disconnect"
255
+ })
256
+ ]
257
+ })
258
+ ]
259
+ }, grant.consent)) : /*#__PURE__*/ _jsx("p", {
260
+ children: "No active connections."
261
+ })
262
+ ]
263
+ })
264
+ ]
265
+ })
266
+ });
267
+ }
268
+
269
+ //# sourceMappingURL=OAuthView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/OAuthView.tsx"],"sourcesContent":["'use client'\n\nimport React, { useEffect, useState } from 'react'\nimport { Banner, Button, Gutter } from '@payloadcms/ui'\nimport type { Catalog } from '../oauth-permissions'\nimport { agentInstructions } from './agentInstructions'\nimport { ConsentPermissions } from './ConsentPermissions'\nimport './oauth.css'\n\ntype Consent = { consent: string; clientName: string; account: string; scope: string; catalog: Catalog }\ntype Grant = { consent: string; clientName: string; scope: string; summary: string }\ntype Connections = { resource: string; access: 'read-only' | 'editor'; eligible: boolean; grants: Grant[] }\n\nfunction CopyButton({ text, label }: { text: string; label: string }) {\n const [status, setStatus] = useState('')\n async function copy() {\n setStatus('')\n try {\n await navigator.clipboard.writeText(text)\n setStatus('Copied.')\n } catch {\n setStatus('Copy failed. Select the text and copy it.')\n }\n }\n return (\n <div className=\"mcp-oauth__copy\">\n <Button type=\"button\" buttonStyle=\"secondary\" size=\"small\" margin={false} onClick={copy}>{label}</Button>\n <span role=\"status\">{status}</span>\n </div>\n )\n}\n\nexport function OAuthView({ mode }: { mode: 'authorize' | 'connections' }) {\n const [data, setData] = useState<Consent | Connections | null>(null)\n const [error, setError] = useState('')\n const [nothingPicked, setNothingPicked] = useState(false)\n const endpoint = `/api/mcp/oauth/${mode}`\n\n useEffect(() => {\n const controller = new AbortController()\n async function load() {\n try {\n const response = await fetch(`${endpoint}${window.location.search}`, {\n headers: { Accept: 'application/json' }, credentials: 'same-origin', cache: 'no-store', signal: controller.signal,\n })\n if (response.redirected) {\n const destination = new URL(response.url)\n if (destination.origin === window.location.origin) window.location.assign(destination.href)\n return\n }\n if (!response.ok) {\n setError(response.status === 403 ? 'Your account cannot authorize this connection.' : 'This request could not be loaded. Start the connection again from your AI app.')\n return\n }\n setData(await response.json())\n } catch {\n if (!controller.signal.aborted) setError('The connection could not be loaded. Check your connection and reload this page.')\n }\n }\n void load()\n return () => controller.abort()\n }, [endpoint])\n\n const approval = data && 'account' in data ? data : null\n const connections = data && 'grants' in data ? data : null\n const instructions = connections ? agentInstructions(connections.resource, connections.access) : ''\n\n return (\n <Gutter>\n <main className=\"mcp-oauth\">\n <h1>{mode === 'authorize' ? 'Connect your account' : 'AI connections'}</h1>\n {error ? <Banner type=\"error\"><span role=\"alert\">{error}</span></Banner> : !data ? <p role=\"status\">Loading connection…</p> : null}\n {approval && (\n <>\n <p><strong>{approval.clientName}</strong> is requesting access to your website account.</p>\n <dl className=\"mcp-oauth__details\">\n <dt>Signed in as</dt><dd>{approval.account}</dd>\n <dt>Permissions</dt><dd>What you choose below. Your website permissions still apply. Deleting content and changing global settings are not allowed.</dd>\n <dt>Duration</dt><dd>Up to 30 days. You can disconnect at any time.</dd>\n </dl>\n {/* Enter on a checkbox would submit the form through its first button, Allow access. */}\n <form action={endpoint} method=\"post\" onKeyDown={e => {\n if (e.key === 'Enter' && (e.target as HTMLElement).matches('input[type=checkbox]')) e.preventDefault()\n }}>\n <input type=\"hidden\" name=\"consent\" value={approval.consent} />\n <ConsentPermissions\n catalog={approval.catalog}\n max={approval.scope.split(' ').includes('mcp:write') ? 'editor' : 'read-only'}\n onEmptyChange={setNothingPicked}\n />\n <div className=\"mcp-oauth__actions\">\n <Button type=\"submit\" margin={false} disabled={nothingPicked} extraButtonProps={{ name: 'decision', value: 'allow' }}>Allow access</Button>\n <Button type=\"submit\" buttonStyle=\"secondary\" margin={false} extraButtonProps={{ name: 'decision', value: 'deny' }}>Cancel</Button>\n </div>\n </form>\n </>\n )}\n {connections && (\n <>\n {connections.eligible ? (\n <>\n <p>Add this URL as a custom connector in your AI app. Then sign in and approve access.</p>\n <ul>\n <li><strong>Claude:</strong> Settings → Connectors → Add custom connector.</li>\n <li><strong>ChatGPT (web):</strong> turn on Developer mode in Settings, then create an app with this URL and OAuth sign-in.</li>\n </ul>\n <code className=\"mcp-oauth__url\">{connections.resource}</code>\n <CopyButton text={connections.resource} label=\"Copy URL\" />\n <h2>Agent instructions</h2>\n <p>Paste these into your AI app, for example as project instructions. They tell the assistant how to connect and how to work with your content.</p>\n <pre className=\"mcp-oauth__instructions\">{instructions}</pre>\n <CopyButton text={instructions} label=\"Copy instructions\" />\n </>\n ) : <p>Your account cannot connect AI apps. Ask a site administrator for access.</p>}\n <h2>Connected apps</h2>\n {connections.grants.length ? connections.grants.map(grant => (\n <div className=\"mcp-oauth__connection\" key={grant.consent}>\n <div><strong>{grant.clientName}</strong><p>{grant.summary}</p></div>\n <form action={endpoint} method=\"post\">\n <input type=\"hidden\" name=\"consent\" value={grant.consent} />\n <Button type=\"submit\" buttonStyle=\"secondary\" margin={false}>Disconnect</Button>\n </form>\n </div>\n )) : <p>No active connections.</p>}\n </>\n )}\n </main>\n </Gutter>\n )\n}\n"],"names":["React","useEffect","useState","Banner","Button","Gutter","agentInstructions","ConsentPermissions","CopyButton","text","label","status","setStatus","copy","navigator","clipboard","writeText","div","className","type","buttonStyle","size","margin","onClick","span","role","OAuthView","mode","data","setData","error","setError","nothingPicked","setNothingPicked","endpoint","controller","AbortController","load","response","fetch","window","location","search","headers","Accept","credentials","cache","signal","redirected","destination","URL","url","origin","assign","href","ok","json","aborted","abort","approval","connections","instructions","resource","access","main","h1","p","strong","clientName","dl","dt","dd","account","form","action","method","onKeyDown","e","key","target","matches","preventDefault","input","name","value","consent","catalog","max","scope","split","includes","onEmptyChange","disabled","extraButtonProps","eligible","ul","li","code","h2","pre","grants","length","map","grant","summary"],"mappings":"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"}
@@ -1,4 +1,29 @@
1
1
  import * as React from 'react';
2
+ /**
3
+ * Reusable matrix renderer for "items × actions" scope tables.
4
+ *
5
+ * Used by both CollectionScopesMatrix (collections × 4 actions) and
6
+ * GlobalScopesMatrix (globals × 2 actions). The stored shape is
7
+ * `Array<{ slug: string; actions: string[] }>` — the parent field name
8
+ * (`collectionScopes` vs `globalScopes`) already encodes which axis the
9
+ * slugs belong to, so the row payload no longer mirrors that distinction.
10
+ */
11
+ interface RowValue {
12
+ slug?: unknown;
13
+ actions?: unknown;
14
+ }
15
+ type MatrixValue = RowValue[];
16
+ export interface ScopesMatrixProps {
17
+ /** Rows in the stored `{ slug, actions }[]` shape. */
18
+ value: MatrixValue | null | undefined;
19
+ onChange: (rows: MatrixValue) => void;
20
+ /** Table id; unique on the page. */
21
+ id: string;
22
+ items: string[];
23
+ actions: string[];
24
+ actionLabels: Record<string, string>;
25
+ itemHeader: string;
26
+ }
2
27
  export interface ScopesTableProps {
3
28
  path: string;
4
29
  /** Slugs to render as rows. */
@@ -16,4 +41,7 @@ export interface ScopesTableProps {
16
41
  /** Copy shown when `items` is empty. */
17
42
  emptyMessage: string;
18
43
  }
44
+ /** The items × actions checkbox table without form binding. The OAuth consent screen uses it too. */
45
+ export declare function ScopesMatrix(props: ScopesMatrixProps): React.ReactElement;
19
46
  export declare function ScopesTable(props: ScopesTableProps): React.ReactElement;
47
+ export {};
@@ -43,16 +43,30 @@ const headerCellStyle = {
43
43
  borderBottom: '1px solid var(--theme-elevation-150)',
44
44
  fontWeight: 600
45
45
  };
46
+ // The matrix shows row names and column headers visually; this gives each checkbox a spoken name too.
47
+ const hiddenStyle = {
48
+ position: 'absolute',
49
+ width: 1,
50
+ height: 1,
51
+ margin: -1,
52
+ padding: 0,
53
+ overflow: 'hidden',
54
+ clip: 'rect(0 0 0 0)',
55
+ whiteSpace: 'nowrap',
56
+ border: 0
57
+ };
58
+ const hiddenLabel = (htmlFor, text)=>/*#__PURE__*/ _jsx("label", {
59
+ htmlFor: htmlFor,
60
+ style: hiddenStyle,
61
+ children: text
62
+ });
46
63
  const labelCellStyle = {
47
64
  textAlign: 'left',
48
65
  padding: '0.5rem 0.75rem',
49
66
  verticalAlign: 'middle'
50
67
  };
51
- export function ScopesTable(props) {
52
- const { value, setValue } = useField({
53
- path: props.path,
54
- hasRows: false
55
- });
68
+ /** The items × actions checkbox table without form binding. The OAuth consent screen uses it too. */ export function ScopesMatrix(props) {
69
+ const { value, onChange: setValue } = props;
56
70
  const allowedActionsSet = React.useMemo(()=>new Set(props.actions), [
57
71
  props.actions
58
72
  ]);
@@ -110,6 +124,145 @@ export function ScopesTable(props) {
110
124
  const toggleAll = (checked)=>update((m)=>{
111
125
  for (const slug of props.items)m.set(slug, checked ? new Set(props.actions) : new Set());
112
126
  });
127
+ return /*#__PURE__*/ _jsxs("table", {
128
+ id: props.id,
129
+ style: {
130
+ width: '100%',
131
+ borderCollapse: 'collapse',
132
+ fontSize: '0.9rem',
133
+ background: 'var(--theme-input-bg)',
134
+ border: '1px solid var(--theme-elevation-150)',
135
+ borderRadius: '4px',
136
+ overflow: 'hidden'
137
+ },
138
+ children: [
139
+ /*#__PURE__*/ _jsx("thead", {
140
+ children: /*#__PURE__*/ _jsxs("tr", {
141
+ children: [
142
+ /*#__PURE__*/ _jsx("th", {
143
+ style: {
144
+ ...headerCellStyle,
145
+ textAlign: 'left'
146
+ },
147
+ children: /*#__PURE__*/ _jsxs("div", {
148
+ style: {
149
+ display: 'flex',
150
+ flexDirection: 'column',
151
+ gap: '0.35rem'
152
+ },
153
+ children: [
154
+ /*#__PURE__*/ _jsx("span", {
155
+ children: props.itemHeader
156
+ }),
157
+ /*#__PURE__*/ _jsx(CheckboxInput, {
158
+ checked: isAllFull,
159
+ partialChecked: isAllPartial && !isAllFull,
160
+ onToggle: (e)=>toggleAll(e.currentTarget.checked),
161
+ label: "All"
162
+ })
163
+ ]
164
+ })
165
+ }),
166
+ props.actions.map((action)=>/*#__PURE__*/ _jsx("th", {
167
+ style: headerCellStyle,
168
+ children: /*#__PURE__*/ _jsxs("div", {
169
+ style: {
170
+ display: 'flex',
171
+ flexDirection: 'column',
172
+ alignItems: 'center',
173
+ gap: '0.35rem'
174
+ },
175
+ children: [
176
+ /*#__PURE__*/ _jsx("span", {
177
+ children: props.actionLabels[action] ?? action
178
+ }),
179
+ /*#__PURE__*/ _jsx(CheckboxInput, {
180
+ checked: isColumnFull(action),
181
+ id: `${props.id}-col-${action}`,
182
+ Label: hiddenLabel(`${props.id}-col-${action}`, `${props.actionLabels[action] ?? action}: all`),
183
+ partialChecked: isColumnPartial(action) && !isColumnFull(action),
184
+ onToggle: (e)=>toggleColumn(action, e.currentTarget.checked)
185
+ })
186
+ ]
187
+ })
188
+ }, action))
189
+ ]
190
+ })
191
+ }),
192
+ /*#__PURE__*/ _jsx("tbody", {
193
+ children: props.items.map((slug, idx)=>{
194
+ const niceName = toWords(slug);
195
+ return /*#__PURE__*/ _jsxs("tr", {
196
+ style: {
197
+ background: idx % 2 === 0 ? 'var(--theme-input-bg)' : 'var(--theme-elevation-25)'
198
+ },
199
+ children: [
200
+ /*#__PURE__*/ _jsx("td", {
201
+ style: {
202
+ ...labelCellStyle,
203
+ borderTop: idx === 0 ? 'none' : '1px solid var(--theme-elevation-100)'
204
+ },
205
+ children: /*#__PURE__*/ _jsxs("div", {
206
+ style: {
207
+ display: 'flex',
208
+ alignItems: 'center',
209
+ gap: '0.6rem'
210
+ },
211
+ children: [
212
+ /*#__PURE__*/ _jsx(CheckboxInput, {
213
+ checked: isRowFull(slug),
214
+ id: `${props.id}-row-${idx}`,
215
+ Label: hiddenLabel(`${props.id}-row-${idx}`, `${niceName}: all actions`),
216
+ partialChecked: isRowPartial(slug) && !isRowFull(slug),
217
+ onToggle: (e)=>toggleRow(slug, e.currentTarget.checked)
218
+ }),
219
+ /*#__PURE__*/ _jsxs("div", {
220
+ style: {
221
+ display: 'flex',
222
+ flexDirection: 'column'
223
+ },
224
+ children: [
225
+ /*#__PURE__*/ _jsx("span", {
226
+ children: niceName
227
+ }),
228
+ /*#__PURE__*/ _jsx("code", {
229
+ style: {
230
+ fontSize: '0.75rem',
231
+ color: 'var(--theme-elevation-500)',
232
+ background: 'transparent',
233
+ padding: 0
234
+ },
235
+ children: slug
236
+ })
237
+ ]
238
+ })
239
+ ]
240
+ })
241
+ }),
242
+ props.actions.map((action)=>/*#__PURE__*/ _jsx("td", {
243
+ style: {
244
+ ...cellStyle,
245
+ borderTop: idx === 0 ? 'none' : '1px solid var(--theme-elevation-100)'
246
+ },
247
+ children: /*#__PURE__*/ _jsx(CheckboxInput, {
248
+ checked: isCellChecked(slug, action),
249
+ id: `${props.id}-row-${idx}-${action}`,
250
+ Label: hiddenLabel(`${props.id}-row-${idx}-${action}`, `${niceName}: ${props.actionLabels[action] ?? action}`),
251
+ onToggle: (e)=>toggleCell(slug, action, e.currentTarget.checked)
252
+ })
253
+ }, action))
254
+ ]
255
+ }, slug);
256
+ })
257
+ })
258
+ ]
259
+ });
260
+ }
261
+ export function ScopesTable(props) {
262
+ const { value, setValue } = useField({
263
+ path: props.path,
264
+ hasRows: false
265
+ });
113
266
  if (props.items.length === 0) {
114
267
  return /*#__PURE__*/ _jsxs("div", {
115
268
  className: "field-type",
@@ -151,132 +304,14 @@ export function ScopesTable(props) {
151
304
  },
152
305
  children: props.description
153
306
  }),
154
- /*#__PURE__*/ _jsxs("table", {
307
+ /*#__PURE__*/ _jsx(ScopesMatrix, {
155
308
  id: `${props.path}-matrix`,
156
- style: {
157
- width: '100%',
158
- borderCollapse: 'collapse',
159
- fontSize: '0.9rem',
160
- background: 'var(--theme-input-bg)',
161
- border: '1px solid var(--theme-elevation-150)',
162
- borderRadius: '4px',
163
- overflow: 'hidden'
164
- },
165
- children: [
166
- /*#__PURE__*/ _jsx("thead", {
167
- children: /*#__PURE__*/ _jsxs("tr", {
168
- children: [
169
- /*#__PURE__*/ _jsx("th", {
170
- style: {
171
- ...headerCellStyle,
172
- textAlign: 'left'
173
- },
174
- children: /*#__PURE__*/ _jsxs("div", {
175
- style: {
176
- display: 'flex',
177
- flexDirection: 'column',
178
- gap: '0.35rem'
179
- },
180
- children: [
181
- /*#__PURE__*/ _jsx("span", {
182
- children: props.itemHeader
183
- }),
184
- /*#__PURE__*/ _jsx(CheckboxInput, {
185
- checked: isAllFull,
186
- partialChecked: isAllPartial && !isAllFull,
187
- onToggle: (e)=>toggleAll(e.currentTarget.checked),
188
- label: "All"
189
- })
190
- ]
191
- })
192
- }),
193
- props.actions.map((action)=>/*#__PURE__*/ _jsx("th", {
194
- style: headerCellStyle,
195
- children: /*#__PURE__*/ _jsxs("div", {
196
- style: {
197
- display: 'flex',
198
- flexDirection: 'column',
199
- alignItems: 'center',
200
- gap: '0.35rem'
201
- },
202
- children: [
203
- /*#__PURE__*/ _jsx("span", {
204
- children: props.actionLabels[action] ?? action
205
- }),
206
- /*#__PURE__*/ _jsx(CheckboxInput, {
207
- checked: isColumnFull(action),
208
- partialChecked: isColumnPartial(action) && !isColumnFull(action),
209
- onToggle: (e)=>toggleColumn(action, e.currentTarget.checked)
210
- })
211
- ]
212
- })
213
- }, action))
214
- ]
215
- })
216
- }),
217
- /*#__PURE__*/ _jsx("tbody", {
218
- children: props.items.map((slug, idx)=>{
219
- const niceName = toWords(slug);
220
- return /*#__PURE__*/ _jsxs("tr", {
221
- style: {
222
- background: idx % 2 === 0 ? 'var(--theme-input-bg)' : 'var(--theme-elevation-25)'
223
- },
224
- children: [
225
- /*#__PURE__*/ _jsx("td", {
226
- style: {
227
- ...labelCellStyle,
228
- borderTop: idx === 0 ? 'none' : '1px solid var(--theme-elevation-100)'
229
- },
230
- children: /*#__PURE__*/ _jsxs("div", {
231
- style: {
232
- display: 'flex',
233
- alignItems: 'center',
234
- gap: '0.6rem'
235
- },
236
- children: [
237
- /*#__PURE__*/ _jsx(CheckboxInput, {
238
- checked: isRowFull(slug),
239
- partialChecked: isRowPartial(slug) && !isRowFull(slug),
240
- onToggle: (e)=>toggleRow(slug, e.currentTarget.checked)
241
- }),
242
- /*#__PURE__*/ _jsxs("div", {
243
- style: {
244
- display: 'flex',
245
- flexDirection: 'column'
246
- },
247
- children: [
248
- /*#__PURE__*/ _jsx("span", {
249
- children: niceName
250
- }),
251
- /*#__PURE__*/ _jsx("code", {
252
- style: {
253
- fontSize: '0.75rem',
254
- color: 'var(--theme-elevation-500)',
255
- background: 'transparent',
256
- padding: 0
257
- },
258
- children: slug
259
- })
260
- ]
261
- })
262
- ]
263
- })
264
- }),
265
- props.actions.map((action)=>/*#__PURE__*/ _jsx("td", {
266
- style: {
267
- ...cellStyle,
268
- borderTop: idx === 0 ? 'none' : '1px solid var(--theme-elevation-100)'
269
- },
270
- children: /*#__PURE__*/ _jsx(CheckboxInput, {
271
- checked: isCellChecked(slug, action),
272
- onToggle: (e)=>toggleCell(slug, action, e.currentTarget.checked)
273
- })
274
- }, action))
275
- ]
276
- }, slug);
277
- })
278
- })
279
- ]
309
+ value: value,
310
+ onChange: setValue,
311
+ items: props.items,
312
+ actions: props.actions,
313
+ actionLabels: props.actionLabels,
314
+ itemHeader: props.itemHeader
280
315
  })
281
316
  ]
282
317
  });