@forumone/throughline-publishing 0.2.3 → 0.3.1

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 (80) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +154 -1
  3. package/dist/admin/PublishButton.d.ts +24 -0
  4. package/dist/admin/PublishButton.d.ts.map +1 -0
  5. package/dist/admin/PublishButton.js +128 -0
  6. package/dist/admin/PublishButton.js.map +1 -0
  7. package/dist/admin/UnpublishButton.d.ts +15 -0
  8. package/dist/admin/UnpublishButton.d.ts.map +1 -0
  9. package/dist/admin/UnpublishButton.js +90 -0
  10. package/dist/admin/UnpublishButton.js.map +1 -0
  11. package/dist/admin/publishing-client.d.ts +55 -0
  12. package/dist/admin/publishing-client.d.ts.map +1 -0
  13. package/dist/admin/publishing-client.js +64 -0
  14. package/dist/admin/publishing-client.js.map +1 -0
  15. package/dist/checks/alt-text.d.ts +8 -2
  16. package/dist/checks/alt-text.d.ts.map +1 -1
  17. package/dist/checks/alt-text.js +13 -3
  18. package/dist/checks/alt-text.js.map +1 -1
  19. package/dist/endpoints/admin.d.ts +19 -0
  20. package/dist/endpoints/admin.d.ts.map +1 -0
  21. package/dist/endpoints/admin.js +93 -0
  22. package/dist/endpoints/admin.js.map +1 -0
  23. package/dist/events.d.ts +16 -0
  24. package/dist/events.d.ts.map +1 -0
  25. package/dist/events.js +30 -0
  26. package/dist/events.js.map +1 -0
  27. package/dist/exports/client.d.ts +14 -0
  28. package/dist/exports/client.d.ts.map +1 -0
  29. package/dist/exports/client.js +11 -0
  30. package/dist/exports/client.js.map +1 -0
  31. package/dist/hooks/block-status-writes.d.ts +4 -0
  32. package/dist/hooks/block-status-writes.d.ts.map +1 -1
  33. package/dist/hooks/block-status-writes.js +6 -1
  34. package/dist/hooks/block-status-writes.js.map +1 -1
  35. package/dist/index.d.ts +7 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +6 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/options.d.ts +19 -0
  40. package/dist/options.d.ts.map +1 -1
  41. package/dist/options.js.map +1 -1
  42. package/dist/pipeline/index.d.ts.map +1 -1
  43. package/dist/pipeline/index.js +9 -1
  44. package/dist/pipeline/index.js.map +1 -1
  45. package/dist/pipeline/steps/accessibility.d.ts +4 -0
  46. package/dist/pipeline/steps/accessibility.d.ts.map +1 -1
  47. package/dist/pipeline/steps/accessibility.js +6 -1
  48. package/dist/pipeline/steps/accessibility.js.map +1 -1
  49. package/dist/pipeline/steps/execute.d.ts +9 -0
  50. package/dist/pipeline/steps/execute.d.ts.map +1 -1
  51. package/dist/pipeline/steps/execute.js +17 -2
  52. package/dist/pipeline/steps/execute.js.map +1 -1
  53. package/dist/pipeline/types.d.ts +15 -1
  54. package/dist/pipeline/types.d.ts.map +1 -1
  55. package/dist/plugin.d.ts.map +1 -1
  56. package/dist/plugin.js +62 -5
  57. package/dist/plugin.js.map +1 -1
  58. package/dist/service.d.ts +116 -0
  59. package/dist/service.d.ts.map +1 -0
  60. package/dist/service.js +245 -0
  61. package/dist/service.js.map +1 -0
  62. package/dist/tools/get-publish-status.d.ts +4 -1
  63. package/dist/tools/get-publish-status.d.ts.map +1 -1
  64. package/dist/tools/get-publish-status.js +16 -30
  65. package/dist/tools/get-publish-status.js.map +1 -1
  66. package/dist/tools/publish.d.ts +4 -1
  67. package/dist/tools/publish.d.ts.map +1 -1
  68. package/dist/tools/publish.js +8 -57
  69. package/dist/tools/publish.js.map +1 -1
  70. package/dist/tools/rollback.d.ts.map +1 -1
  71. package/dist/tools/rollback.js +5 -1
  72. package/dist/tools/rollback.js.map +1 -1
  73. package/dist/tools/schedule-publish.d.ts.map +1 -1
  74. package/dist/tools/schedule-publish.js +9 -2
  75. package/dist/tools/schedule-publish.js.map +1 -1
  76. package/dist/tools/unpublish.d.ts +4 -1
  77. package/dist/tools/unpublish.d.ts.map +1 -1
  78. package/dist/tools/unpublish.js +8 -47
  79. package/dist/tools/unpublish.js.map +1 -1
  80. package/package.json +19 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @forumone/throughline-publishing
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - fc5c236: Fix two defects reported against 0.3.0.
8
+
9
+ **The `alt-text` check false-positived on Payload upload derivatives.** `walkForImages` descended into a populated upload's `sizes` map, and every generated size carries `filename` and `mimeType` but never `alt` — that lives on the parent document. Each configured `imageSize` therefore produced one false failure, and any page carrying a sized image could not be published. The walk now skips `sizes` on an object that is itself an image; alt text is checked once, on the parent. A parent with missing or empty alt still fails, at the parent's path, and a host with no `imageSizes` is unaffected.
10
+
11
+ **A failed Inngest emission failed a publish that had already succeeded.** The event is sent after the document is written, so a transport failure (an invalid `INNGEST_EVENT_KEY`, say) returned 500 for a document that was published — and lost the audit record for it. Publish, unpublish, rollback and `schedule_publish` now report success and carry the emission failure as a `warnings` array on the result. The admin renders it as a warning toast on an otherwise successful publish.
12
+
13
+ Also adds `disableAccessibilityChecks`, naming built-in checks to skip. `accessibilityChecks` only appends, so previously a built-in that misfired on a host's content shape blocked every publish until the plugin shipped a fix.
14
+
15
+ ## 0.3.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 422b970: Ship the admin Publish/Unpublish controls and a server-side publishing API.
20
+
21
+ `publishingPlugin` blocked direct writes to `_status` — which is what Payload's native Publish button submits — but shipped no replacement, so no document in a publishable collection could be published from the admin. The pipeline was reachable only over the MCP endpoint, which requires an API key.
22
+ - **Admin controls.** The plugin now installs its own `PublishButton` and `UnpublishButton` on every configured collection, exported from `@forumone/throughline-publishing/client`. They run the pipeline as the logged-in editor over the session cookie — no API key, correct audit attribution — and render the failing step, its issues and its suggestion instead of a generic error. Hosts must run `payload generate:importmap`. Opt out with `adminComponents: false`; an explicit host-set slot is never overwritten.
23
+ - **Server-side API.** `publishDocument`, `unpublishDocument`, `getPublishStatus` and `getPublishingService` are now public, for host code that needs to publish outside the admin.
24
+ - **Audit attribution.** Publishes are attributed to the user who made them rather than to the API key that transported the call. Admin publishes record `mcpTool: 'admin:publish'`.
25
+ - **Access control.** Admin and host-API publishes run the underlying write with `overrideAccess: false` as that user, so bypassing the status hook does not bypass collection permissions.
26
+ - **Diagnosis.** The status-write hook now throws `APIError(…, 400)` rather than `Error`, so the message reaches the admin instead of becoming a 500 and a generic "Something went wrong" toast.
27
+
28
+ The MCP tools now route through the same service as the admin path, so the two channels cannot drift. Their input and output shapes are unchanged.
29
+
3
30
  ## 0.2.3
4
31
 
5
32
  ### Patch Changes
package/README.md CHANGED
@@ -2,4 +2,157 @@
2
2
 
3
3
  Policy-gated publishing server for Throughline. The trust boundary that decides what's allowed to ship.
4
4
 
5
- _Authoring guide and full API reference land in a later commit._
5
+ ## What this package provides
6
+
7
+ - **A publish pipeline** — `exist` → `composition` → `accessibility` → `required-fields` → `embargo` → `approval` → `execute`. The first step to object stops the publish and reports which one, why, and what to do about it.
8
+ - **A trust boundary** — a `beforeChange` hook on every configured collection that rejects direct writes to `_status`. The pipeline is the only sanctioned way to publish.
9
+ - **Admin controls** — Publish and Unpublish buttons that run the pipeline as the logged-in editor. Installed automatically; no host-side code.
10
+ - **A server-side API** — `publishDocument` / `unpublishDocument` / `getPublishStatus` for host code that needs to publish outside the admin.
11
+ - **Five MCP tools** served at `/api/publishing/mcp`: `publish`, `unpublish`, `schedule_publish`, `get_publish_status`, `rollback`.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pnpm add @forumone/throughline-publishing
17
+ ```
18
+
19
+ Peers: `payload@^3.0.0`, `inngest@^4.0.0`. Required runtime peer: `@forumone/throughline-core` (audit log). The admin controls additionally use `@payloadcms/ui` and `react`, both already present in any Payload 3 admin.
20
+
21
+ ## Usage
22
+
23
+ ```ts
24
+ import { buildConfig } from 'payload'
25
+ import { auditPlugin, createInngestClient } from '@forumone/throughline-core'
26
+ import { publishingPlugin } from '@forumone/throughline-publishing'
27
+
28
+ const inngest = createInngestClient({ id: 'my-site' })
29
+
30
+ export default buildConfig({
31
+ // collections, db, secret...
32
+ plugins: [
33
+ auditPlugin({ inngest }),
34
+ publishingPlugin({
35
+ inngest,
36
+ collections: [{ slug: 'pages' }],
37
+ }),
38
+ ],
39
+ })
40
+ ```
41
+
42
+ Publishable collections must have drafts enabled (`versions: { drafts: true }`).
43
+
44
+ ## Publishing from the admin
45
+
46
+ The plugin replaces Payload's native Publish and Unpublish buttons on every configured collection. It has to: the native buttons submit `_status: 'published'` straight to the update endpoint, which is exactly the write the trust boundary exists to reject.
47
+
48
+ The replacements save pending edits as a draft, then call the plugin's own endpoint, which runs the full pipeline. **Run `payload generate:importmap` after adding the plugin** so Payload can resolve the components — the dev server does this for you; CI builds need it explicitly.
49
+
50
+ What you get:
51
+
52
+ - **No API key in the editorial publish path.** The endpoint authenticates off the Payload session cookie.
53
+ - **The person is the actor.** The audit event records the logged-in editor, with `mcpTool` set to `admin:publish` so admin publishes are distinguishable from MCP ones.
54
+ - **Access control still applies.** The write runs with `overrideAccess: false` as that user. Bypassing the status hook is not bypassing permissions.
55
+ - **Real feedback.** A blocked publish renders the failing step, its issues, and its suggestion — not a generic error.
56
+
57
+ The Publish button is hidden on the create view: the pipeline's first step is `exist`, so there is nothing to evaluate until the draft is saved. Use Payload's Save Draft button, then publish from the edit view.
58
+
59
+ To supply your own controls instead, either set `admin.components.edit.PublishButton` on the collection yourself (an explicit host setting always wins), or turn the feature off entirely:
60
+
61
+ ```ts
62
+ publishingPlugin({ inngest, collections: [{ slug: 'pages' }], adminComponents: false })
63
+ ```
64
+
65
+ With `adminComponents: false` the admin has **no** working publish path until you supply one — the native buttons will still be rejected by the hook.
66
+
67
+ ### Endpoints
68
+
69
+ | Route | Auth | Body |
70
+ |---|---|---|
71
+ | `POST /api/publishing/publish` | Payload session | `{ collection, id }` |
72
+ | `POST /api/publishing/unpublish` | Payload session | `{ collection, id }` |
73
+ | `POST /api/publishing/mcp` | Bearer API key | JSON-RPC |
74
+
75
+ A publish blocked by the pipeline returns **200** with `{ published: false, failedAt, reason, code, issues, suggestion }`. The pipeline ran correctly and the answer was no; that is not a transport error. Non-2xx is reserved for auth (401/403), bad input (400), and genuine failures (500).
76
+
77
+ ## Publishing from host code
78
+
79
+ `runPublishPipeline` needs plugin options and an audit writer that only exist inside the plugin. Rather than export the raw pipeline, the plugin attaches a service to the Payload instance at `onInit` and exposes these helpers:
80
+
81
+ ```ts
82
+ import { publishDocument, unpublishDocument, getPublishStatus } from '@forumone/throughline-publishing'
83
+
84
+ // In a custom endpoint, job, or Server Action:
85
+ const result = await publishDocument({
86
+ payload: req.payload,
87
+ collection: 'pages',
88
+ id: '42',
89
+ user: req.user, // attributed in the audit log; their permissions apply
90
+ })
91
+
92
+ if (!result.published) {
93
+ console.log(result.failedAt, result.reason, result.suggestion)
94
+ }
95
+ ```
96
+
97
+ All three run the same pipeline the admin and MCP paths use, so host code cannot drift from the plugin's policy. `getPublishStatus` runs every check except the write and mutates nothing.
98
+
99
+ ## Bypassing the pipeline
100
+
101
+ `bypassPublishingServer: true` in the Payload request context is the only way to write `_status` without the pipeline. It is intended for seed scripts and migrations:
102
+
103
+ ```ts
104
+ await payload.update({
105
+ collection: 'pages',
106
+ id,
107
+ data: { _status: 'published' },
108
+ context: { bypassPublishingServer: true },
109
+ })
110
+ ```
111
+
112
+ It skips composition, accessibility, required-field, embargo, and approval checks. Nothing in the admin path uses it.
113
+
114
+ > **Note:** the hook guards `update` only. `payload.create({ data: { _status: 'published' } })` is not intercepted, so a create can publish without the pipeline. Save as a draft and publish in a second step if that matters to you.
115
+
116
+ ## Warnings
117
+
118
+ A publish or unpublish writes the document first, then emits its Inngest event. The event is a consequence of the write, not a step in it — so if the emission fails, the action still reports success and carries a warning:
119
+
120
+ ```jsonc
121
+ { "published": true, "publishedAt": "…", "warnings": ["The content/page.published event could not be sent, …"] }
122
+ ```
123
+
124
+ The admin shows these as a warning toast on an otherwise successful publish. Reporting failure for a write that landed would tell an editor their change isn't live when it is, and the obvious response to that is to publish again over live content.
125
+
126
+ `rollback` and `schedule_publish` behave the same way.
127
+
128
+ ## Custom accessibility checks
129
+
130
+ The built-in checks (`alt-text`, `heading-hierarchy`, `link-labels`) are exported from `@forumone/throughline-publishing/checks`. Add your own via `accessibilityChecks`:
131
+
132
+ ```ts
133
+ publishingPlugin({
134
+ inngest,
135
+ collections: [{ slug: 'pages' }],
136
+ accessibilityChecks: [
137
+ {
138
+ name: 'no-empty-tables',
139
+ run: (doc) => (hasEmptyTable(doc) ? [{ message: 'Table has no rows', severity: 'error' }] : []),
140
+ },
141
+ ],
142
+ })
143
+ ```
144
+
145
+ `accessibilityChecks` appends. To switch a built-in off — because it misfires on your content shape, or you want to replace it — name it in `disableAccessibilityChecks`:
146
+
147
+ ```ts
148
+ publishingPlugin({
149
+ inngest,
150
+ collections: [{ slug: 'pages' }],
151
+ disableAccessibilityChecks: ['alt-text'],
152
+ accessibilityChecks: [myOwnAltTextCheck],
153
+ })
154
+ ```
155
+
156
+ ### A note on uploads
157
+
158
+ The `alt-text` check walks the document for image-shaped objects but does **not** descend into a populated upload's `sizes` map. Payload's generated derivatives carry `filename` and `mimeType` but never `alt` — that lives on the parent document — so treating them as images would report one false failure per configured `imageSize`. Alt text is checked once, on the parent.
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ export interface ThroughlinePublishButtonProps {
3
+ /** Route prefix the plugin is mounted under. Injected via `clientProps`. */
4
+ routePrefix?: string;
5
+ /** Publish-timestamp field for this collection. Injected via `clientProps`. */
6
+ publishedAtField?: string;
7
+ }
8
+ /**
9
+ * Replaces Payload's Publish button on collections the publishing plugin
10
+ * governs.
11
+ *
12
+ * The native button submits `_status: 'published'` straight to the update
13
+ * endpoint, which the plugin's trust boundary rejects by design. This one
14
+ * saves pending edits as a draft, then asks the publishing server to run the
15
+ * pipeline — as the logged-in editor, over the session cookie, with no API
16
+ * key. A block renders the failing step, its issues and its suggestion
17
+ * instead of a generic error.
18
+ *
19
+ * On the create view this renders nothing: the pipeline's first step is
20
+ * `exist`, so there is nothing to evaluate until the draft is saved.
21
+ * Payload's own Save Draft button covers that step.
22
+ */
23
+ export declare function PublishButton(props?: ThroughlinePublishButtonProps): React.ReactNode;
24
+ //# sourceMappingURL=PublishButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PublishButton.d.ts","sourceRoot":"","sources":["../../src/admin/PublishButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAgC,MAAM,OAAO,CAAA;AAYpD,MAAM,WAAW,6BAA6B;IAC5C,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,KAAK,GAAE,6BAAkC,GAAG,KAAK,CAAC,SAAS,CAuIxF"}
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useCallback, useState } from 'react';
4
+ import { FormSubmit, toast, useConfig, useDocumentInfo, useForm, useFormModified, useTranslation, } from '@payloadcms/ui';
5
+ import { callPublishingEndpoint, describeBlock } from './publishing-client.js';
6
+ /**
7
+ * Replaces Payload's Publish button on collections the publishing plugin
8
+ * governs.
9
+ *
10
+ * The native button submits `_status: 'published'` straight to the update
11
+ * endpoint, which the plugin's trust boundary rejects by design. This one
12
+ * saves pending edits as a draft, then asks the publishing server to run the
13
+ * pipeline — as the logged-in editor, over the session cookie, with no API
14
+ * key. A block renders the failing step, its issues and its suggestion
15
+ * instead of a generic error.
16
+ *
17
+ * On the create view this renders nothing: the pipeline's first step is
18
+ * `exist`, so there is nothing to evaluate until the draft is saved.
19
+ * Payload's own Save Draft button covers that step.
20
+ */
21
+ export function PublishButton(props = {}) {
22
+ const routePrefix = props.routePrefix ?? '/publishing';
23
+ const { collectionSlug, data, hasPublishedDoc, hasPublishPermission, id, setHasPublishedDoc, setMostRecentVersionIsAutosaved, setUnpublishedVersionCount, unpublishedVersionCount, uploadStatus, } = useDocumentInfo();
24
+ const { config } = useConfig();
25
+ const { reset, submit } = useForm();
26
+ const modified = useFormModified();
27
+ const { t } = useTranslation();
28
+ const [publishing, setPublishing] = useState(false);
29
+ const { api } = config.routes;
30
+ const serverURL = config.serverURL ?? '';
31
+ const publish = useCallback(async () => {
32
+ if (id === undefined || id === null || !collectionSlug || publishing)
33
+ return;
34
+ setPublishing(true);
35
+ try {
36
+ // Persist pending edits first. Draft writes never touch `_status`, so
37
+ // the trust boundary lets them through; the pipeline then evaluates
38
+ // what was actually saved rather than what is on screen.
39
+ if (modified) {
40
+ const saved = await submit({
41
+ action: `${serverURL}${api}/${collectionSlug}/${id}?draft=true&depth=0`,
42
+ method: 'PATCH',
43
+ overrides: { _status: 'draft' },
44
+ skipValidation: true,
45
+ });
46
+ // `submit` returns void and toasts its own error on most failures,
47
+ // but falls through with a non-ok response when the body carries no
48
+ // message. Check both so a failed save never reaches the pipeline.
49
+ if (!saved || !saved.res.ok)
50
+ return;
51
+ }
52
+ const result = await callPublishingEndpoint({
53
+ serverURL,
54
+ apiRoute: api,
55
+ routePrefix,
56
+ action: 'publish',
57
+ collection: collectionSlug,
58
+ id,
59
+ });
60
+ if (!result.ok) {
61
+ toast.error(result.message);
62
+ return;
63
+ }
64
+ if (!result.body.published) {
65
+ const { title, description } = describeBlock(result.body);
66
+ toast.error(title, {
67
+ ...(description ? { description } : {}),
68
+ duration: 10_000,
69
+ });
70
+ return;
71
+ }
72
+ await reset({
73
+ ...(data ?? {}),
74
+ _status: 'published',
75
+ ...(props.publishedAtField && result.body.publishedAt
76
+ ? { [props.publishedAtField]: result.body.publishedAt }
77
+ : {}),
78
+ });
79
+ setHasPublishedDoc(true);
80
+ setUnpublishedVersionCount(0);
81
+ setMostRecentVersionIsAutosaved(false);
82
+ // The document is live either way. A warning means something
83
+ // downstream of the publish didn't happen, which is worth saying
84
+ // without implying the publish failed.
85
+ const warnings = result.body.warnings ?? [];
86
+ if (warnings.length > 0) {
87
+ toast.warning(t('version:published'), {
88
+ description: warnings.join('\n'),
89
+ duration: 10_000,
90
+ });
91
+ }
92
+ else {
93
+ toast.success(t('version:published'));
94
+ }
95
+ }
96
+ finally {
97
+ setPublishing(false);
98
+ }
99
+ }, [
100
+ api,
101
+ collectionSlug,
102
+ data,
103
+ id,
104
+ modified,
105
+ props.publishedAtField,
106
+ publishing,
107
+ reset,
108
+ routePrefix,
109
+ serverURL,
110
+ setHasPublishedDoc,
111
+ setMostRecentVersionIsAutosaved,
112
+ setUnpublishedVersionCount,
113
+ submit,
114
+ t,
115
+ ]);
116
+ if (!hasPublishPermission)
117
+ return null;
118
+ // Create view — no document for the pipeline to evaluate yet.
119
+ if (id === undefined || id === null || !collectionSlug)
120
+ return null;
121
+ const canPublish = (modified || unpublishedVersionCount > 0 || !hasPublishedDoc) &&
122
+ uploadStatus !== 'uploading' &&
123
+ !publishing;
124
+ return (_jsx(FormSubmit, { buttonId: "action-save", disabled: !canPublish, onClick: () => {
125
+ void publish();
126
+ }, size: "medium", type: "button", children: publishing ? t('version:publishing') : t('version:publishChanges') }));
127
+ }
128
+ //# sourceMappingURL=PublishButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PublishButton.js","sourceRoot":"","sources":["../../src/admin/PublishButton.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ export interface ThroughlineUnpublishButtonProps {
3
+ /** Route prefix the plugin is mounted under. Injected via `clientProps`. */
4
+ routePrefix?: string;
5
+ }
6
+ /**
7
+ * Replaces Payload's Unpublish control on collections the publishing plugin
8
+ * governs. Same reasoning as the Publish button: the native control writes
9
+ * `_status: 'draft'` directly, which the trust boundary rejects. This one
10
+ * routes through the publishing server so the unpublish is audited against
11
+ * the logged-in editor and `content/page.unpublished` fires for
12
+ * revalidation.
13
+ */
14
+ export declare function UnpublishButton(props?: ThroughlineUnpublishButtonProps): React.ReactNode;
15
+ //# sourceMappingURL=UnpublishButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UnpublishButton.d.ts","sourceRoot":"","sources":["../../src/admin/UnpublishButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAgC,MAAM,OAAO,CAAA;AAapD,MAAM,WAAW,+BAA+B;IAC9C,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,KAAK,GAAE,+BAAoC,GAC1C,KAAK,CAAC,SAAS,CA4GjB"}
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { useCallback, useState } from 'react';
4
+ import { ConfirmationModal, PopupList, toast, useConfig, useDocumentInfo, useForm, useModal, useTranslation, } from '@payloadcms/ui';
5
+ import { callPublishingEndpoint } from './publishing-client.js';
6
+ /**
7
+ * Replaces Payload's Unpublish control on collections the publishing plugin
8
+ * governs. Same reasoning as the Publish button: the native control writes
9
+ * `_status: 'draft'` directly, which the trust boundary rejects. This one
10
+ * routes through the publishing server so the unpublish is audited against
11
+ * the logged-in editor and `content/page.unpublished` fires for
12
+ * revalidation.
13
+ */
14
+ export function UnpublishButton(props = {}) {
15
+ const routePrefix = props.routePrefix ?? '/publishing';
16
+ const { collectionSlug, data, hasPublishedDoc, hasPublishPermission, id, incrementVersionCount, isTrashed, setHasPublishedDoc, setMostRecentVersionIsAutosaved, setUnpublishedVersionCount, } = useDocumentInfo();
17
+ const { config } = useConfig();
18
+ const { reset } = useForm();
19
+ const { t } = useTranslation();
20
+ const { toggleModal } = useModal();
21
+ const [unpublishing, setUnpublishing] = useState(false);
22
+ const modalSlug = `throughline-confirm-unpublish-${id}`;
23
+ const { api } = config.routes;
24
+ const serverURL = config.serverURL ?? '';
25
+ const unpublish = useCallback(async () => {
26
+ if (id === undefined || id === null || !collectionSlug || unpublishing)
27
+ return;
28
+ setUnpublishing(true);
29
+ try {
30
+ const result = await callPublishingEndpoint({
31
+ serverURL,
32
+ apiRoute: api,
33
+ routePrefix,
34
+ action: 'unpublish',
35
+ collection: collectionSlug,
36
+ id,
37
+ });
38
+ if (!result.ok) {
39
+ toast.error(result.message);
40
+ return;
41
+ }
42
+ if (!result.body.unpublished) {
43
+ toast.error(result.body.reason ?? t('error:unPublishingDocument'));
44
+ return;
45
+ }
46
+ await reset({ ...(data ?? {}), _status: 'draft' });
47
+ incrementVersionCount();
48
+ setUnpublishedVersionCount(1);
49
+ setMostRecentVersionIsAutosaved(false);
50
+ setHasPublishedDoc(false);
51
+ const warnings = result.body.warnings ?? [];
52
+ if (warnings.length > 0) {
53
+ toast.warning(t('version:unpublishedSuccessfully'), {
54
+ description: warnings.join('\n'),
55
+ duration: 10_000,
56
+ });
57
+ }
58
+ else {
59
+ toast.success(t('version:unpublishedSuccessfully'));
60
+ }
61
+ }
62
+ finally {
63
+ setUnpublishing(false);
64
+ }
65
+ }, [
66
+ api,
67
+ collectionSlug,
68
+ data,
69
+ id,
70
+ incrementVersionCount,
71
+ reset,
72
+ routePrefix,
73
+ serverURL,
74
+ setHasPublishedDoc,
75
+ setMostRecentVersionIsAutosaved,
76
+ setUnpublishedVersionCount,
77
+ t,
78
+ unpublishing,
79
+ ]);
80
+ if (!hasPublishPermission || !hasPublishedDoc || isTrashed)
81
+ return null;
82
+ if (id === undefined || id === null || !collectionSlug)
83
+ return null;
84
+ return (_jsxs(React.Fragment, { children: [_jsx(PopupList.Button, { id: "action-unpublish", onClick: () => {
85
+ toggleModal(modalSlug);
86
+ }, children: t('version:unpublish') }), _jsx(ConfirmationModal, { body: t('version:aboutToUnpublish'), confirmingLabel: t('version:unpublishing'), heading: t('version:confirmUnpublish'), modalSlug: modalSlug, onConfirm: () => {
87
+ void unpublish();
88
+ } })] }));
89
+ }
90
+ //# sourceMappingURL=UnpublishButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UnpublishButton.js","sourceRoot":"","sources":["../../src/admin/UnpublishButton.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Client-side helpers shared by the admin Publish and Unpublish controls.
3
+ * No JSX here so the message formatting can be unit tested directly.
4
+ */
5
+ export interface PublishingIssue {
6
+ field?: string;
7
+ message: string;
8
+ severity?: 'error' | 'warning';
9
+ rule?: string;
10
+ }
11
+ export interface PublishingResponse {
12
+ published?: boolean;
13
+ unpublished?: boolean;
14
+ publishedAt?: string;
15
+ failedAt?: string;
16
+ reason?: string;
17
+ code?: string;
18
+ issues?: PublishingIssue[];
19
+ suggestion?: string;
20
+ /** Non-fatal problems on an action that otherwise succeeded. */
21
+ warnings?: string[];
22
+ error?: string;
23
+ }
24
+ export interface CallPublishingEndpointArgs {
25
+ serverURL: string;
26
+ apiRoute: string;
27
+ routePrefix: string;
28
+ action: 'publish' | 'unpublish';
29
+ collection: string;
30
+ id: number | string;
31
+ }
32
+ export type PublishingCallResult = {
33
+ ok: true;
34
+ body: PublishingResponse;
35
+ } | {
36
+ ok: false;
37
+ message: string;
38
+ };
39
+ /**
40
+ * POSTs to the plugin's admin endpoint using the browser's session cookie.
41
+ * Returns a transport-level failure as `ok: false`; a pipeline block comes
42
+ * back as `ok: true` with a `published: false` body.
43
+ */
44
+ export declare function callPublishingEndpoint(args: CallPublishingEndpointArgs): Promise<PublishingCallResult>;
45
+ /**
46
+ * Turns a pipeline block into something an editor can act on: which step
47
+ * said no, what it objected to, and what to do about it. This is the
48
+ * information the pipeline already returns — it just never had a way to
49
+ * reach the screen.
50
+ */
51
+ export declare function describeBlock(body: PublishingResponse): {
52
+ title: string;
53
+ description: string;
54
+ };
55
+ //# sourceMappingURL=publishing-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publishing-client.d.ts","sourceRoot":"","sources":["../../src/admin/publishing-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,eAAe,EAAE,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,SAAS,GAAG,WAAW,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CACpB;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAA;CAAE,GACtC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAElC;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,oBAAoB,CAAC,CA8B/B;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,kBAAkB,GAAG;IACvD,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;CACpB,CAqBA"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Client-side helpers shared by the admin Publish and Unpublish controls.
3
+ * No JSX here so the message formatting can be unit tested directly.
4
+ */
5
+ /**
6
+ * POSTs to the plugin's admin endpoint using the browser's session cookie.
7
+ * Returns a transport-level failure as `ok: false`; a pipeline block comes
8
+ * back as `ok: true` with a `published: false` body.
9
+ */
10
+ export async function callPublishingEndpoint(args) {
11
+ const url = `${args.serverURL}${args.apiRoute}${args.routePrefix}/${args.action}`;
12
+ let response;
13
+ try {
14
+ response = await fetch(url, {
15
+ method: 'POST',
16
+ credentials: 'include',
17
+ headers: { 'content-type': 'application/json' },
18
+ body: JSON.stringify({ collection: args.collection, id: args.id }),
19
+ });
20
+ }
21
+ catch {
22
+ return { ok: false, message: 'Could not reach the publishing server.' };
23
+ }
24
+ let body;
25
+ try {
26
+ body = (await response.json());
27
+ }
28
+ catch {
29
+ return { ok: false, message: `Publishing server returned ${response.status}.` };
30
+ }
31
+ if (!response.ok) {
32
+ return {
33
+ ok: false,
34
+ message: body.error ?? `Publishing server returned ${response.status}.`,
35
+ };
36
+ }
37
+ return { ok: true, body };
38
+ }
39
+ const MAX_LISTED_ISSUES = 5;
40
+ /**
41
+ * Turns a pipeline block into something an editor can act on: which step
42
+ * said no, what it objected to, and what to do about it. This is the
43
+ * information the pipeline already returns — it just never had a way to
44
+ * reach the screen.
45
+ */
46
+ export function describeBlock(body) {
47
+ const title = body.reason ?? `Publish blocked at the ${body.failedAt ?? 'policy'} check.`;
48
+ const lines = [];
49
+ if (body.failedAt && body.reason) {
50
+ lines.push(`Blocked at: ${body.failedAt}`);
51
+ }
52
+ const issues = body.issues ?? [];
53
+ for (const issue of issues.slice(0, MAX_LISTED_ISSUES)) {
54
+ lines.push(`• ${issue.field ? `${issue.field}: ` : ''}${issue.message}`);
55
+ }
56
+ if (issues.length > MAX_LISTED_ISSUES) {
57
+ lines.push(`• …and ${issues.length - MAX_LISTED_ISSUES} more`);
58
+ }
59
+ if (body.suggestion) {
60
+ lines.push(`Suggestion: ${body.suggestion}`);
61
+ }
62
+ return { title, description: lines.join('\n') };
63
+ }
64
+ //# sourceMappingURL=publishing-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publishing-client.js","sourceRoot":"","sources":["../../src/admin/publishing-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoCH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,IAAgC;IAEhC,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,MAAM,EAAE,CAAA;IAEjF,IAAI,QAAkB,CAAA;IACtB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC1B,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,SAAS;YACtB,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;SACnE,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,wCAAwC,EAAE,CAAA;IACzE,CAAC;IAED,IAAI,IAAwB,CAAA;IAC5B,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAuB,CAAA;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,8BAA8B,QAAQ,CAAC,MAAM,GAAG,EAAE,CAAA;IACjF,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,IAAI,CAAC,KAAK,IAAI,8BAA8B,QAAQ,CAAC,MAAM,GAAG;SACxE,CAAA;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AAC3B,CAAC;AAED,MAAM,iBAAiB,GAAG,CAAC,CAAA;AAE3B;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAwB;IAIpD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI,0BAA0B,IAAI,CAAC,QAAQ,IAAI,QAAQ,SAAS,CAAA;IAEzF,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAA;IAC5C,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAA;IAChC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,iBAAiB,CAAC,EAAE,CAAC;QACvD,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IAC1E,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,iBAAiB,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,UAAU,MAAM,CAAC,MAAM,GAAG,iBAAiB,OAAO,CAAC,CAAA;IAChE,CAAC;IAED,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,UAAU,EAAE,CAAC,CAAA;IAC9C,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;AACjD,CAAC"}
@@ -4,8 +4,14 @@ import type { AccessibilityCheck } from '../options.js';
4
4
  * a `url` or `filename` property whose `mimeType`, if present, starts with
5
5
  * `image/`). Flags any whose `alt` is missing or empty.
6
6
  *
7
- * Heuristic, not exhaustive clients with non-standard image shapes
8
- * register their own check via `accessibilityChecks`.
7
+ * Skips an upload's `sizes` map: Payload's generated derivatives carry
8
+ * `filename` and `mimeType` but never `alt`, which lives on the parent
9
+ * document. They are renditions of an image already checked, not images in
10
+ * their own right — walking into them reports one false failure per
11
+ * configured `imageSize`.
12
+ *
13
+ * Heuristic, not exhaustive. A host can add checks via `accessibilityChecks`
14
+ * and switch a built-in off via `disableAccessibilityChecks`.
9
15
  */
10
16
  export declare const altTextCheck: AccessibilityCheck;
11
17
  //# sourceMappingURL=alt-text.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"alt-text.d.ts","sourceRoot":"","sources":["../../src/checks/alt-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAsB,MAAM,eAAe,CAAA;AAE3E;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,EAAE,kBAgB1B,CAAA"}
1
+ {"version":3,"file":"alt-text.d.ts","sourceRoot":"","sources":["../../src/checks/alt-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAsB,MAAM,eAAe,CAAA;AAE3E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,EAAE,kBAgB1B,CAAA"}