@forumone/throughline-publishing 0.2.3 → 0.3.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 +15 -0
- package/README.md +127 -1
- package/dist/admin/PublishButton.d.ts +24 -0
- package/dist/admin/PublishButton.d.ts.map +1 -0
- package/dist/admin/PublishButton.js +116 -0
- package/dist/admin/PublishButton.js.map +1 -0
- package/dist/admin/UnpublishButton.d.ts +15 -0
- package/dist/admin/UnpublishButton.d.ts.map +1 -0
- package/dist/admin/UnpublishButton.js +81 -0
- package/dist/admin/UnpublishButton.js.map +1 -0
- package/dist/admin/publishing-client.d.ts +53 -0
- package/dist/admin/publishing-client.d.ts.map +1 -0
- package/dist/admin/publishing-client.js +64 -0
- package/dist/admin/publishing-client.js.map +1 -0
- package/dist/endpoints/admin.d.ts +19 -0
- package/dist/endpoints/admin.d.ts.map +1 -0
- package/dist/endpoints/admin.js +93 -0
- package/dist/endpoints/admin.js.map +1 -0
- package/dist/exports/client.d.ts +14 -0
- package/dist/exports/client.d.ts.map +1 -0
- package/dist/exports/client.js +11 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/hooks/block-status-writes.d.ts +4 -0
- package/dist/hooks/block-status-writes.d.ts.map +1 -1
- package/dist/hooks/block-status-writes.js +6 -1
- package/dist/hooks/block-status-writes.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +10 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js.map +1 -1
- package/dist/pipeline/steps/execute.d.ts +5 -0
- package/dist/pipeline/steps/execute.d.ts.map +1 -1
- package/dist/pipeline/steps/execute.js +8 -0
- package/dist/pipeline/steps/execute.js.map +1 -1
- package/dist/pipeline/types.d.ts +8 -1
- package/dist/pipeline/types.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +62 -5
- package/dist/plugin.js.map +1 -1
- package/dist/service.d.ts +109 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +225 -0
- package/dist/service.js.map +1 -0
- package/dist/tools/get-publish-status.d.ts +4 -1
- package/dist/tools/get-publish-status.d.ts.map +1 -1
- package/dist/tools/get-publish-status.js +16 -30
- package/dist/tools/get-publish-status.js.map +1 -1
- package/dist/tools/publish.d.ts +4 -1
- package/dist/tools/publish.d.ts.map +1 -1
- package/dist/tools/publish.js +8 -57
- package/dist/tools/publish.js.map +1 -1
- package/dist/tools/unpublish.d.ts +4 -1
- package/dist/tools/unpublish.d.ts.map +1 -1
- package/dist/tools/unpublish.js +8 -47
- package/dist/tools/unpublish.js.map +1 -1
- package/package.json +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @forumone/throughline-publishing
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 422b970: Ship the admin Publish/Unpublish controls and a server-side publishing API.
|
|
8
|
+
|
|
9
|
+
`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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **Server-side API.** `publishDocument`, `unpublishDocument`, `getPublishStatus` and `getPublishingService` are now public, for host code that needs to publish outside the admin.
|
|
12
|
+
- **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'`.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
3
18
|
## 0.2.3
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -2,4 +2,130 @@
|
|
|
2
2
|
|
|
3
3
|
Policy-gated publishing server for Throughline. The trust boundary that decides what's allowed to ship.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
+
## Custom accessibility checks
|
|
117
|
+
|
|
118
|
+
The built-in checks (alt text, heading hierarchy, link labels) are exported from `@forumone/throughline-publishing/checks`. Add your own via `accessibilityChecks`:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
publishingPlugin({
|
|
122
|
+
inngest,
|
|
123
|
+
collections: [{ slug: 'pages' }],
|
|
124
|
+
accessibilityChecks: [
|
|
125
|
+
{
|
|
126
|
+
name: 'no-empty-tables',
|
|
127
|
+
run: (doc) => (hasEmptyTable(doc) ? [{ message: 'Table has no rows', severity: 'error' }] : []),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
})
|
|
131
|
+
```
|
|
@@ -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,CA2HxF"}
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
toast.success(t('version:published'));
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
setPublishing(false);
|
|
86
|
+
}
|
|
87
|
+
}, [
|
|
88
|
+
api,
|
|
89
|
+
collectionSlug,
|
|
90
|
+
data,
|
|
91
|
+
id,
|
|
92
|
+
modified,
|
|
93
|
+
props.publishedAtField,
|
|
94
|
+
publishing,
|
|
95
|
+
reset,
|
|
96
|
+
routePrefix,
|
|
97
|
+
serverURL,
|
|
98
|
+
setHasPublishedDoc,
|
|
99
|
+
setMostRecentVersionIsAutosaved,
|
|
100
|
+
setUnpublishedVersionCount,
|
|
101
|
+
submit,
|
|
102
|
+
t,
|
|
103
|
+
]);
|
|
104
|
+
if (!hasPublishPermission)
|
|
105
|
+
return null;
|
|
106
|
+
// Create view — no document for the pipeline to evaluate yet.
|
|
107
|
+
if (id === undefined || id === null || !collectionSlug)
|
|
108
|
+
return null;
|
|
109
|
+
const canPublish = (modified || unpublishedVersionCount > 0 || !hasPublishedDoc) &&
|
|
110
|
+
uploadStatus !== 'uploading' &&
|
|
111
|
+
!publishing;
|
|
112
|
+
return (_jsx(FormSubmit, { buttonId: "action-save", disabled: !canPublish, onClick: () => {
|
|
113
|
+
void publish();
|
|
114
|
+
}, size: "medium", type: "button", children: publishing ? t('version:publishing') : t('version:publishChanges') }));
|
|
115
|
+
}
|
|
116
|
+
//# 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,CAmGjB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
toast.success(t('version:unpublishedSuccessfully'));
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
setUnpublishing(false);
|
|
55
|
+
}
|
|
56
|
+
}, [
|
|
57
|
+
api,
|
|
58
|
+
collectionSlug,
|
|
59
|
+
data,
|
|
60
|
+
id,
|
|
61
|
+
incrementVersionCount,
|
|
62
|
+
reset,
|
|
63
|
+
routePrefix,
|
|
64
|
+
serverURL,
|
|
65
|
+
setHasPublishedDoc,
|
|
66
|
+
setMostRecentVersionIsAutosaved,
|
|
67
|
+
setUnpublishedVersionCount,
|
|
68
|
+
t,
|
|
69
|
+
unpublishing,
|
|
70
|
+
]);
|
|
71
|
+
if (!hasPublishPermission || !hasPublishedDoc || isTrashed)
|
|
72
|
+
return null;
|
|
73
|
+
if (id === undefined || id === null || !collectionSlug)
|
|
74
|
+
return null;
|
|
75
|
+
return (_jsxs(React.Fragment, { children: [_jsx(PopupList.Button, { id: "action-unpublish", onClick: () => {
|
|
76
|
+
toggleModal(modalSlug);
|
|
77
|
+
}, children: t('version:unpublish') }), _jsx(ConfirmationModal, { body: t('version:aboutToUnpublish'), confirmingLabel: t('version:unpublishing'), heading: t('version:confirmUnpublish'), modalSlug: modalSlug, onConfirm: () => {
|
|
78
|
+
void unpublish();
|
|
79
|
+
} })] }));
|
|
80
|
+
}
|
|
81
|
+
//# 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,53 @@
|
|
|
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
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CallPublishingEndpointArgs {
|
|
23
|
+
serverURL: string;
|
|
24
|
+
apiRoute: string;
|
|
25
|
+
routePrefix: string;
|
|
26
|
+
action: 'publish' | 'unpublish';
|
|
27
|
+
collection: string;
|
|
28
|
+
id: number | string;
|
|
29
|
+
}
|
|
30
|
+
export type PublishingCallResult = {
|
|
31
|
+
ok: true;
|
|
32
|
+
body: PublishingResponse;
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* POSTs to the plugin's admin endpoint using the browser's session cookie.
|
|
39
|
+
* Returns a transport-level failure as `ok: false`; a pipeline block comes
|
|
40
|
+
* back as `ok: true` with a `published: false` body.
|
|
41
|
+
*/
|
|
42
|
+
export declare function callPublishingEndpoint(args: CallPublishingEndpointArgs): Promise<PublishingCallResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Turns a pipeline block into something an editor can act on: which step
|
|
45
|
+
* said no, what it objected to, and what to do about it. This is the
|
|
46
|
+
* information the pipeline already returns — it just never had a way to
|
|
47
|
+
* reach the screen.
|
|
48
|
+
*/
|
|
49
|
+
export declare function describeBlock(body: PublishingResponse): {
|
|
50
|
+
title: string;
|
|
51
|
+
description: string;
|
|
52
|
+
};
|
|
53
|
+
//# 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,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;AAkCH;;;;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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Endpoint } from 'payload';
|
|
2
|
+
export interface CreateAdminEndpointsDeps {
|
|
3
|
+
/** Route prefix the plugin is mounted under, e.g. `/publishing`. */
|
|
4
|
+
routePrefix: string;
|
|
5
|
+
/** Collections registered as publishable; anything else is rejected. */
|
|
6
|
+
publishableSlugs: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The admin's path into the pipeline.
|
|
10
|
+
*
|
|
11
|
+
* These endpoints authenticate off the Payload session cookie, so `req.user`
|
|
12
|
+
* is the logged-in editor — no API key in the editorial publish path, and
|
|
13
|
+
* the audit event records the person rather than a service principal. The
|
|
14
|
+
* write still goes through the pipeline, and still runs with
|
|
15
|
+
* `overrideAccess: false`, so neither the policy checks nor the collection's
|
|
16
|
+
* access control are skipped.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createAdminEndpoints(deps: CreateAdminEndpointsDeps): Endpoint[];
|
|
19
|
+
//# sourceMappingURL=admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/endpoints/admin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,QAAQ,EAAuB,MAAM,SAAS,CAAA;AAStE,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAA;IACnB,wEAAwE;IACxE,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,wBAAwB,GAAG,QAAQ,EAAE,CAa/E"}
|