@qkix/strapi-plugin-rewind 0.1.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/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/_chunks/en-BHfx2OWt.js +1 -0
- package/dist/_chunks/en-DCctmu-a.mjs +7 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin/index.mjs +254 -0
- package/dist/admin/src/components/ChangesDialog.d.ts +21 -0
- package/dist/admin/src/components/HistoryPanel.d.ts +17 -0
- package/dist/admin/src/diffRegistry.d.ts +45 -0
- package/dist/admin/src/index.d.ts +18 -0
- package/dist/admin/src/pluginId.d.ts +6 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.mjs +930 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +58 -0
- package/dist/server/src/content-types/index.d.ts +95 -0
- package/dist/server/src/controllers/index.d.ts +12 -0
- package/dist/server/src/controllers/version.d.ts +11 -0
- package/dist/server/src/destroy.d.ts +9 -0
- package/dist/server/src/index.d.ts +166 -0
- package/dist/server/src/routes/admin.d.ts +16 -0
- package/dist/server/src/routes/index.d.ts +14 -0
- package/dist/server/src/services/diff.d.ts +55 -0
- package/dist/server/src/services/index.d.ts +35 -0
- package/dist/server/src/services/lock.d.ts +20 -0
- package/dist/server/src/services/restore.d.ts +44 -0
- package/dist/server/src/services/retention.d.ts +18 -0
- package/dist/server/src/services/serializer.d.ts +65 -0
- package/dist/server/src/services/snapshot.d.ts +17 -0
- package/dist/server/src/utils/backfillLabels.d.ts +18 -0
- package/dist/server/src/utils/captureContext.d.ts +45 -0
- package/dist/server/src/utils/persistTable.d.ts +15 -0
- package/dist/server/src/utils/textDiff.d.ts +50 -0
- package/dist/server/src/utils/thinning.d.ts +35 -0
- package/dist/server/src/utils/versionLabel.d.ts +14 -0
- package/package.json +115 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qkix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Rewind
|
|
2
|
+
|
|
3
|
+
Document version history for Strapi 5. Every save is snapshotted, and any
|
|
4
|
+
snapshot can be put back.
|
|
5
|
+
|
|
6
|
+
<img src="https://raw.githubusercontent.com/qkix/strapi-plugins/main/packages/strapi-plugin-rewind/docs/edit-view.png" alt="The history panel beside a document in the Strapi edit view" width="900" />
|
|
7
|
+
|
|
8
|
+
The panel sits in the edit view, next to the document it belongs to. Each entry
|
|
9
|
+
is a point you can go back to: what the document said, when, and who saved it.
|
|
10
|
+
|
|
11
|
+
## What this is, and what it is not
|
|
12
|
+
|
|
13
|
+
Strapi sells a feature called **Content History** on its Growth and Enterprise
|
|
14
|
+
plans. Rewind is **not that feature**, is not a drop-in replacement for it, and
|
|
15
|
+
does not unlock it.
|
|
16
|
+
|
|
17
|
+
It is the poor cousin. If you pay for Growth, use Strapi's — it is deeper, it is
|
|
18
|
+
supported by the people who wrote the CMS, and it will stay in step with Strapi
|
|
19
|
+
in a way a third-party plugin cannot promise. Rewind exists for everyone on
|
|
20
|
+
Community Edition, who today has nothing at all: no way to see what a document
|
|
21
|
+
said last Tuesday, and no way to get it back after someone pastes over it.
|
|
22
|
+
|
|
23
|
+
A few things it does that the paid feature does not, mostly because they were
|
|
24
|
+
cheap to add once the plumbing existed:
|
|
25
|
+
|
|
26
|
+
- **Restoring keeps fields that were added after the version was taken.**
|
|
27
|
+
Strapi's restore sets them to `null`, which quietly loses whatever was in
|
|
28
|
+
them.
|
|
29
|
+
- **Discarding a draft is recorded before the draft is gone**, so the work you
|
|
30
|
+
threw away is still recoverable. A snapshot taken afterwards can only ever
|
|
31
|
+
contain the published copy you already had.
|
|
32
|
+
- **A save that changed nothing does not create a version.** Strapi writes a row
|
|
33
|
+
either way.
|
|
34
|
+
- **The schema snapshot descends into nested components.** Strapi's stops one
|
|
35
|
+
level down and carries an open TODO about it.
|
|
36
|
+
- **Writes from outside the Content Manager can be versioned** (opt-in), where
|
|
37
|
+
Strapi only ever records admin-panel edits.
|
|
38
|
+
|
|
39
|
+
And a good deal it does _not_ do — see [Limits](#limits).
|
|
40
|
+
|
|
41
|
+
### On not touching the paywall
|
|
42
|
+
|
|
43
|
+
Rewind never reads or writes `strapi.ee`, never inspects a licence, and never
|
|
44
|
+
enables a gated feature. It is an independent implementation writing to its own
|
|
45
|
+
table, and it works the same whether or not you have a Strapi licence. Strapi's
|
|
46
|
+
history code is MIT-licensed and was read while working out what the problem
|
|
47
|
+
actually is — the way you would read any open-source implementation before
|
|
48
|
+
writing your own — but the code here is written for this plugin.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install @qkix/strapi-plugin-rewind
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// config/plugins.ts
|
|
58
|
+
export default {
|
|
59
|
+
rewind: {
|
|
60
|
+
enabled: true,
|
|
61
|
+
config: {
|
|
62
|
+
// Nothing is tracked until you say so.
|
|
63
|
+
contentTypes: ['api::article.article'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That empty default is deliberate. A plugin that starts writing a database row on
|
|
70
|
+
every save the moment it is installed is a plugin that gets uninstalled after
|
|
71
|
+
the first disk alert.
|
|
72
|
+
|
|
73
|
+
## What gets recorded
|
|
74
|
+
|
|
75
|
+
Six actions on a tracked content type: `create`, `update`, `clone`, `publish`,
|
|
76
|
+
`unpublish` and `discardDraft`. Each version stores the document's content, its
|
|
77
|
+
relations, and a snapshot of the schema at the time — the last of which is what
|
|
78
|
+
lets a restore tell "this field was empty" apart from "this field did not exist
|
|
79
|
+
yet".
|
|
80
|
+
|
|
81
|
+
Publishing records **one** version, not two, even though the Content Manager
|
|
82
|
+
saves the draft and publishes it as two separate operations.
|
|
83
|
+
|
|
84
|
+
A save that is rolled back records nothing.
|
|
85
|
+
|
|
86
|
+
<img src="https://raw.githubusercontent.com/qkix/strapi-plugins/main/packages/strapi-plugin-rewind/docs/panel.png" alt="History entries labelled Replaced by restore, Draft discarded and Edited" width="320" />
|
|
87
|
+
|
|
88
|
+
The badge says what happened to the document. **Replaced by restore** marks the
|
|
89
|
+
state a restore was about to overwrite — the undo point for that restore.
|
|
90
|
+
|
|
91
|
+
## Restoring
|
|
92
|
+
|
|
93
|
+
<img src="https://raw.githubusercontent.com/qkix/strapi-plugins/main/packages/strapi-plugin-rewind/docs/restore.png" alt="The restore confirmation dialog" width="400" />
|
|
94
|
+
|
|
95
|
+
Restore writes to the **draft only**. The document moves to _Modified_, and
|
|
96
|
+
publishing stays something a person decides to do.
|
|
97
|
+
|
|
98
|
+
Before anything is written, the state being replaced is itself recorded, so a
|
|
99
|
+
restore can always be undone.
|
|
100
|
+
|
|
101
|
+
The panel shows a preview before asking you to confirm, because the interesting
|
|
102
|
+
part of a restore is what it will _not_ do:
|
|
103
|
+
|
|
104
|
+
- fields not present in that version keep their current values
|
|
105
|
+
- fields no longer in the model are skipped
|
|
106
|
+
- links to documents or media that have since been deleted are dropped, and
|
|
107
|
+
reported
|
|
108
|
+
- **fields that are not translated per locale change in every locale at once** —
|
|
109
|
+
restoring the Polish version of a document rewrites a shared field for every
|
|
110
|
+
other language too. This is not a choice the plugin makes; a non-localised
|
|
111
|
+
field physically has one value. The dialog names those fields and counts the
|
|
112
|
+
locales before you commit.
|
|
113
|
+
|
|
114
|
+
## Seeing what changed
|
|
115
|
+
|
|
116
|
+
Every entry in the panel has a **What changed** link, comparing that version
|
|
117
|
+
with the one saved immediately before it.
|
|
118
|
+
|
|
119
|
+
<img src="https://raw.githubusercontent.com/qkix/strapi-plugins/main/packages/strapi-plugin-rewind/docs/changes.png" alt="A word-level diff: 'put back' struck through and replaced by 'restored in one click', and 'ten' by 'twelve'" width="760" />
|
|
120
|
+
|
|
121
|
+
Scalars are shown as a plain before and after. Prose — including rich text
|
|
122
|
+
stored as JSON — is compared word by word, with the unchanged stretches
|
|
123
|
+
collapsed, so a one-word edit in a long article reads as one word rather than
|
|
124
|
+
as the whole article with something green in it somewhere. Relations are
|
|
125
|
+
reported as linked and unlinked.
|
|
126
|
+
|
|
127
|
+
Where a field's stored value changed but its readable text did not — a mark
|
|
128
|
+
applied, blocks reordered — it says so, rather than showing an empty diff.
|
|
129
|
+
|
|
130
|
+
### Rendering a field type properly
|
|
131
|
+
|
|
132
|
+
Pulling the words out of a rich-text field tells an editor _that_ a paragraph
|
|
133
|
+
changed, and nothing about a block moving or an image being swapped. Only the
|
|
134
|
+
package that owns the format can show that, so the mapping is a registry:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { registerDiffRenderer } from '@qkix/strapi-plugin-rewind/strapi-admin';
|
|
138
|
+
|
|
139
|
+
registerDiffRenderer('plugin::better-blocks.better-blocks', BlocksDiff);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Keyed by a custom field's uid, or by an attribute `type`. A uid wins over a
|
|
143
|
+
type, since every custom field stores itself as `json`.
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
| Option | Default | Meaning |
|
|
148
|
+
| ---------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
|
|
149
|
+
| `contentTypes` | `[]` | Which content types to version. Empty means none. |
|
|
150
|
+
| `trackApiWrites` | `false` | Also version writes from outside the Content Manager (REST, GraphQL, programmatic). `userId` is null for those. |
|
|
151
|
+
|
|
152
|
+
## Limits
|
|
153
|
+
|
|
154
|
+
Worth knowing before you install it, not after:
|
|
155
|
+
|
|
156
|
+
- **Polymorphic relations are skipped**, and reported as unsupported in the
|
|
157
|
+
restore preview rather than silently mangled.
|
|
158
|
+
- **Only the Document Service is visible.** Writes made through
|
|
159
|
+
`strapi.db.query()` or the legacy entity service bypass the middleware
|
|
160
|
+
entirely and cannot be captured at any setting.
|
|
161
|
+
- **Nothing pins a version from the panel yet.** The column is there and prune
|
|
162
|
+
honours it, but there is no button; pin by setting `pinned` on the row.
|
|
163
|
+
- **Deleted relation targets lose their names.** A version stores which document
|
|
164
|
+
a relation pointed at, not what it was called, so a link to something since
|
|
165
|
+
deleted shows as missing rather than as "the article it used to be".
|
|
166
|
+
- **Media is referenced, never copied.** Delete the file and the version knows
|
|
167
|
+
the file is gone; it cannot bring it back.
|
|
168
|
+
|
|
169
|
+
## Keeping your history when the plugin is off
|
|
170
|
+
|
|
171
|
+
Strapi's schema sync drops tables belonging to content types it no longer sees.
|
|
172
|
+
Disable this plugin for a single boot and the history would go with it — so on
|
|
173
|
+
boot Rewind adds its table to Strapi's `persisted_tables` list, which is the
|
|
174
|
+
same mechanism Strapi uses to protect its own. Nothing to configure.
|
|
175
|
+
|
|
176
|
+
## Requirements
|
|
177
|
+
|
|
178
|
+
Strapi 5, Node 20 or 22.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e={"plugin.name":"Rewind","panel.title":"History"};exports.default=e;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("react/jsx-runtime"),q=require("react"),E=require("@strapi/admin/strapi-admin"),t=require("@strapi/design-system");function H(n){if(n&&n.__esModule)return n;const r=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const i in n)if(i!=="default"){const a=Object.getOwnPropertyDescriptor(n,i);Object.defineProperty(r,i,a.get?a:{enumerable:!0,get:()=>n[i]})}}return r.default=n,Object.freeze(r)}const l=H(q),G=(n,r,i)=>{const a=n[r];return a?typeof a=="function"?a():Promise.resolve(a):new Promise((j,d)=>{(typeof queueMicrotask=="function"?queueMicrotask:setTimeout)(d.bind(null,new Error("Unknown variable dynamic import: "+r+(r.split("/").length!==i?". Note that variables only represent file names one level deep.":""))))})},D=new Map,W=(n,r)=>{D.set(n,r)},b=n=>D.get(n),z=({spans:n})=>e.jsx(t.Typography,{variant:"omega",tag:"p",children:n.map((r,i)=>r.op==="equal"?e.jsx(t.Typography,{variant:"omega",textColor:"neutral600",children:r.value},i):e.jsx(t.Typography,{variant:"omega",textColor:r.op==="added"?"success600":"danger600",style:r.op==="removed"?{textDecoration:"line-through"}:{fontWeight:600},children:r.value},i))}),S=n=>n==null||n===""?"(empty)":typeof n=="object"?JSON.stringify(n):String(n),U=({change:n})=>{const r=(n.customField?b(n.customField):void 0)??b(n.type);return r?e.jsx(r,{change:n}):n.spans?e.jsx(z,{spans:n.spans}):n.linked||n.unlinked?e.jsxs(t.Flex,{direction:"column",alignItems:"flex-start",gap:1,children:[n.linked?.length?e.jsxs(t.Typography,{variant:"pi",textColor:"success600",children:["+ ",n.linked.length," linked"]}):null,n.unlinked?.length?e.jsxs(t.Typography,{variant:"pi",textColor:"danger600",children:["− ",n.unlinked.length," unlinked"]}):null]}):n.before!==void 0||n.after!==void 0?e.jsxs(t.Flex,{gap:2,alignItems:"center",wrap:"wrap",children:[e.jsx(t.Typography,{variant:"omega",textColor:"danger600",style:{textDecoration:"line-through"},children:S(n.before)}),e.jsx(t.Typography,{variant:"omega",textColor:"neutral500",children:"→"}),e.jsx(t.Typography,{variant:"omega",textColor:"success600",children:S(n.after)})]}):e.jsx(t.Typography,{variant:"pi",textColor:"neutral600",children:"Formatting or structure changed; the text is the same."})},K=({diff:n,loading:r,onClose:i})=>e.jsx(t.Modal.Root,{open:!0,onOpenChange:i,children:e.jsxs(t.Modal.Content,{children:[e.jsx(t.Modal.Header,{children:e.jsx(t.Modal.Title,{children:"What changed"})}),e.jsx(t.Modal.Body,{children:r?e.jsx(t.Flex,{justifyContent:"center",padding:4,children:e.jsx(t.Loader,{small:!0,children:"Comparing"})}):n?e.jsxs(t.Flex,{direction:"column",alignItems:"stretch",gap:4,children:[e.jsx(t.Typography,{variant:"pi",textColor:"neutral600",children:n.from?"Compared with the version saved just before it.":"This is the earliest version of the document, so everything in it is new."}),n.identical?e.jsx(t.Typography,{textColor:"neutral600",children:"Nothing changed between these two versions."}):n.changes.map(a=>e.jsxs(t.Box,{children:[e.jsx(t.Typography,{variant:"sigma",textColor:"neutral600",children:a.field}),e.jsx(t.Box,{paddingTop:1,children:e.jsx(U,{change:a})})]},a.field))]}):e.jsx(t.Typography,{textColor:"danger600",children:"Could not compare these versions."})}),e.jsx(t.Modal.Footer,{children:e.jsx(t.Button,{variant:"tertiary",onClick:i,children:"Close"})})]})}),u="rewind",V={create:"Created",update:"Edited",clone:"Cloned",publish:"Published",unpublish:"Unpublished",discardDraft:"Draft discarded",restore:"Replaced by restore"},k=new Set(["publish","unpublish","discardDraft","restore"]),J=10,Z=n=>{const r=new Date(n),i=Math.round((Date.now()-r.getTime())/6e4);return i<1?"just now":i<60?`${i} min ago`:i<60*24?`${Math.round(i/60)} h ago`:r.toLocaleString()},Q=({model:n,documentId:r,updatedAt:i})=>{const{get:a,post:j}=E.useFetchClient(),[d,R]=l.useState([]),[F,I]=l.useState(0),[B,L]=l.useState(1),[f,m]=l.useState(!0),[h,p]=l.useState(null),[o,g]=l.useState(null),[v,C]=l.useState(!1),[M,w]=l.useState(!1),[P,x]=l.useState(null),[O,T]=l.useState(!1),y=l.useCallback(async s=>{m(!0),p(null);try{const{data:c}=await a(`/${u}/versions`,{params:{contentType:n,documentId:r,page:s,pageSize:J}});R(A=>s===1?c.data:[...A,...c.data]),I(c.meta.total),L(s)}catch{p("Could not load the history for this document.")}finally{m(!1)}},[a,n,r]);l.useEffect(()=>{y(1)},[y,i]);const _=async s=>{w(!0),T(!0),x(null);try{const{data:c}=await a(`/${u}/versions/${s.id}/diff`);x(c.data)}catch{x(null)}finally{T(!1)}},N=async s=>{try{const{data:c}=await a(`/${u}/versions/${s.id}/preview`);g({version:s,preview:c.data})}catch{p("Could not work out what restoring this version would change.")}},$=async()=>{if(o){C(!0);try{await j(`/${u}/versions/${o.version.id}/restore`,{}),window.location.reload()}catch{p("The restore failed. Nothing was changed."),C(!1),g(null)}}};return f&&d.length===0?e.jsx(t.Flex,{justifyContent:"center",padding:4,children:e.jsx(t.Loader,{small:!0,children:"Loading history"})}):h&&d.length===0?e.jsx(t.Typography,{variant:"pi",textColor:"danger600",children:h}):d.length===0?e.jsx(t.Typography,{variant:"pi",textColor:"neutral600",children:"No versions yet. The next time this document is saved, a version will appear here."}):e.jsxs(t.Flex,{direction:"column",alignItems:"stretch",gap:2,width:"100%",children:[d.map(s=>e.jsxs(t.Box,{paddingBottom:2,children:[e.jsxs(t.Flex,{justifyContent:"space-between",alignItems:"center",gap:2,children:[e.jsxs(t.Flex,{direction:"column",alignItems:"flex-start",gap:1,grow:1,minWidth:0,children:[e.jsxs(t.Flex,{gap:2,alignItems:"center",children:[e.jsx(t.Badge,{backgroundColor:k.has(s.origin)?"primary100":"neutral150",textColor:k.has(s.origin)?"primary600":"neutral600",children:V[s.origin]??s.origin}),s.locale?e.jsx(t.Typography,{variant:"pi",textColor:"neutral500",children:s.locale}):null]}),s.label?e.jsx(t.Box,{width:"100%",children:e.jsx(t.Typography,{variant:"omega",textColor:"neutral800",ellipsis:!0,children:s.label})}):null,e.jsxs(t.Typography,{variant:"pi",textColor:"neutral600",children:[Z(s.createdAt),s.user?` · ${s.user.name}`:""]}),e.jsx(t.TextButton,{onClick:()=>_(s),children:"What changed"})]}),e.jsx(t.Box,{shrink:0,children:e.jsx(t.Button,{size:"S",variant:"tertiary",onClick:()=>N(s),children:"Restore"})})]}),e.jsx(t.Box,{paddingTop:2,children:e.jsx(t.Divider,{})})]},s.id)),d.length<F?e.jsx(t.Button,{size:"S",variant:"tertiary",loading:f,onClick:()=>y(B+1),children:"Show older versions"}):null,h?e.jsx(t.Typography,{variant:"pi",textColor:"danger600",children:h}):null,M?e.jsx(K,{diff:P,loading:O,onClose:()=>w(!1)}):null,e.jsx(t.Dialog.Root,{open:o!==null,onOpenChange:()=>g(null),children:e.jsxs(t.Dialog.Content,{children:[e.jsx(t.Dialog.Header,{children:"Restore this version?"}),e.jsx(t.Dialog.Body,{children:e.jsxs(t.Flex,{direction:"column",alignItems:"flex-start",gap:2,children:[e.jsx(t.Typography,{children:"The current draft is saved as a version first, so this can be undone. The published version is not touched."}),o?.preview.crossLocaleFields.length?e.jsxs(t.Typography,{textColor:"danger600",children:[o.preview.crossLocaleFields.join(", ")," ",o.preview.crossLocaleFields.length===1?"is":"are"," not translated per locale, so restoring"," ",o.preview.crossLocaleFields.length===1?"it":"them"," changes"," ",o.preview.affectedLocales.length," locales at once:"," ",o.preview.affectedLocales.join(", "),"."]}):null,o?.preview.fieldsKeptAsIs.length?e.jsxs(t.Typography,{variant:"pi",textColor:"neutral600",children:["Added since this version and left untouched:"," ",o.preview.fieldsKeptAsIs.join(", "),"."]}):null,o?.preview.fieldsDropped.length?e.jsxs(t.Typography,{variant:"pi",textColor:"neutral600",children:["No longer in the model and skipped: ",o.preview.fieldsDropped.join(", "),"."]}):null,o?.preview.brokenRelations.length||o?.preview.missingMedia.length?e.jsxs(t.Typography,{variant:"pi",textColor:"warning600",children:[(o.preview.brokenRelations.length??0)+(o.preview.missingMedia.length??0)," ","linked item(s) no longer exist and will be left out."]}):null]})}),e.jsxs(t.Dialog.Footer,{children:[e.jsx(t.Dialog.Cancel,{children:e.jsx(t.Button,{variant:"tertiary",disabled:v,children:"Cancel"})}),e.jsx(t.Button,{onClick:$,loading:v,children:"Restore"})]})]})})]})},X=({model:n,documentId:r,document:i})=>r?{title:"History",content:e.jsx(Q,{model:n,documentId:r,updatedAt:i?.updatedAt})}:null,Y={register(n){n.registerPlugin({id:u,name:u,isReady:!0})},bootstrap(n){n.getPlugin("content-manager").apis.addEditViewSidePanel([X])},async registerTrads({locales:n}){return Promise.all(n.map(async r=>{try{const{default:i}=await G(Object.assign({"./translations/en.json":()=>Promise.resolve().then(()=>require("../_chunks/en-BHfx2OWt.js"))}),`./translations/${r}.json`,3);return{data:i,locale:r}}catch{return{data:{},locale:r}}}))}};exports.PLUGIN_ID=u;exports.default=Y;exports.registerDiffRenderer=W;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { jsx as t, jsxs as o } from "react/jsx-runtime";
|
|
2
|
+
import * as d from "react";
|
|
3
|
+
import { useFetchClient as K } from "@strapi/admin/strapi-admin";
|
|
4
|
+
import { Modal as g, Flex as c, Loader as $, Typography as r, Box as f, Button as v, Badge as V, TextButton as J, Divider as Z, Dialog as p } from "@strapi/design-system";
|
|
5
|
+
const Q = (e, n, a) => {
|
|
6
|
+
const s = e[n];
|
|
7
|
+
return s ? typeof s == "function" ? s() : Promise.resolve(s) : new Promise((k, u) => {
|
|
8
|
+
(typeof queueMicrotask == "function" ? queueMicrotask : setTimeout)(
|
|
9
|
+
u.bind(
|
|
10
|
+
null,
|
|
11
|
+
new Error(
|
|
12
|
+
"Unknown variable dynamic import: " + n + (n.split("/").length !== a ? ". Note that variables only represent file names one level deep." : "")
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
}, N = /* @__PURE__ */ new Map(), de = (e, n) => {
|
|
18
|
+
N.set(e, n);
|
|
19
|
+
}, L = (e) => N.get(e), X = ({ spans: e }) => /* @__PURE__ */ t(r, { variant: "omega", tag: "p", children: e.map((n, a) => n.op === "equal" ? /* @__PURE__ */ t(r, { variant: "omega", textColor: "neutral600", children: n.value }, a) : /* @__PURE__ */ t(
|
|
20
|
+
r,
|
|
21
|
+
{
|
|
22
|
+
variant: "omega",
|
|
23
|
+
textColor: n.op === "added" ? "success600" : "danger600",
|
|
24
|
+
style: n.op === "removed" ? { textDecoration: "line-through" } : { fontWeight: 600 },
|
|
25
|
+
children: n.value
|
|
26
|
+
},
|
|
27
|
+
a
|
|
28
|
+
)) }), F = (e) => e == null || e === "" ? "(empty)" : typeof e == "object" ? JSON.stringify(e) : String(e), Y = ({ change: e }) => {
|
|
29
|
+
const n = (e.customField ? L(e.customField) : void 0) ?? L(e.type);
|
|
30
|
+
return n ? /* @__PURE__ */ t(n, { change: e }) : e.spans ? /* @__PURE__ */ t(X, { spans: e.spans }) : e.linked || e.unlinked ? /* @__PURE__ */ o(c, { direction: "column", alignItems: "flex-start", gap: 1, children: [
|
|
31
|
+
e.linked?.length ? /* @__PURE__ */ o(r, { variant: "pi", textColor: "success600", children: [
|
|
32
|
+
"+ ",
|
|
33
|
+
e.linked.length,
|
|
34
|
+
" linked"
|
|
35
|
+
] }) : null,
|
|
36
|
+
e.unlinked?.length ? /* @__PURE__ */ o(r, { variant: "pi", textColor: "danger600", children: [
|
|
37
|
+
"− ",
|
|
38
|
+
e.unlinked.length,
|
|
39
|
+
" unlinked"
|
|
40
|
+
] }) : null
|
|
41
|
+
] }) : e.before !== void 0 || e.after !== void 0 ? /* @__PURE__ */ o(c, { gap: 2, alignItems: "center", wrap: "wrap", children: [
|
|
42
|
+
/* @__PURE__ */ t(
|
|
43
|
+
r,
|
|
44
|
+
{
|
|
45
|
+
variant: "omega",
|
|
46
|
+
textColor: "danger600",
|
|
47
|
+
style: { textDecoration: "line-through" },
|
|
48
|
+
children: F(e.before)
|
|
49
|
+
}
|
|
50
|
+
),
|
|
51
|
+
/* @__PURE__ */ t(r, { variant: "omega", textColor: "neutral500", children: "→" }),
|
|
52
|
+
/* @__PURE__ */ t(r, { variant: "omega", textColor: "success600", children: F(e.after) })
|
|
53
|
+
] }) : /* @__PURE__ */ t(r, { variant: "pi", textColor: "neutral600", children: "Formatting or structure changed; the text is the same." });
|
|
54
|
+
}, ee = ({
|
|
55
|
+
diff: e,
|
|
56
|
+
loading: n,
|
|
57
|
+
onClose: a
|
|
58
|
+
}) => /* @__PURE__ */ t(g.Root, { open: !0, onOpenChange: a, children: /* @__PURE__ */ o(g.Content, { children: [
|
|
59
|
+
/* @__PURE__ */ t(g.Header, { children: /* @__PURE__ */ t(g.Title, { children: "What changed" }) }),
|
|
60
|
+
/* @__PURE__ */ t(g.Body, { children: n ? /* @__PURE__ */ t(c, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ t($, { small: !0, children: "Comparing" }) }) : e ? /* @__PURE__ */ o(c, { direction: "column", alignItems: "stretch", gap: 4, children: [
|
|
61
|
+
/* @__PURE__ */ t(r, { variant: "pi", textColor: "neutral600", children: e.from ? "Compared with the version saved just before it." : "This is the earliest version of the document, so everything in it is new." }),
|
|
62
|
+
e.identical ? /* @__PURE__ */ t(r, { textColor: "neutral600", children: "Nothing changed between these two versions." }) : e.changes.map((s) => /* @__PURE__ */ o(f, { children: [
|
|
63
|
+
/* @__PURE__ */ t(r, { variant: "sigma", textColor: "neutral600", children: s.field }),
|
|
64
|
+
/* @__PURE__ */ t(f, { paddingTop: 1, children: /* @__PURE__ */ t(Y, { change: s }) })
|
|
65
|
+
] }, s.field))
|
|
66
|
+
] }) : /* @__PURE__ */ t(r, { textColor: "danger600", children: "Could not compare these versions." }) }),
|
|
67
|
+
/* @__PURE__ */ t(g.Footer, { children: /* @__PURE__ */ t(v, { variant: "tertiary", onClick: a, children: "Close" }) })
|
|
68
|
+
] }) }), m = "rewind", te = {
|
|
69
|
+
create: "Created",
|
|
70
|
+
update: "Edited",
|
|
71
|
+
clone: "Cloned",
|
|
72
|
+
publish: "Published",
|
|
73
|
+
unpublish: "Unpublished",
|
|
74
|
+
discardDraft: "Draft discarded",
|
|
75
|
+
restore: "Replaced by restore"
|
|
76
|
+
}, P = /* @__PURE__ */ new Set(["publish", "unpublish", "discardDraft", "restore"]), ne = 10, re = (e) => {
|
|
77
|
+
const n = new Date(e), a = Math.round((Date.now() - n.getTime()) / 6e4);
|
|
78
|
+
return a < 1 ? "just now" : a < 60 ? `${a} min ago` : a < 60 * 24 ? `${Math.round(a / 60)} h ago` : n.toLocaleString();
|
|
79
|
+
}, ie = ({
|
|
80
|
+
model: e,
|
|
81
|
+
documentId: n,
|
|
82
|
+
/**
|
|
83
|
+
* Changes whenever the Content Manager re-reads the document, which is what
|
|
84
|
+
* it does after a save. Used purely as a refresh signal: without it the panel
|
|
85
|
+
* fetches once on mount and then quietly goes stale, so the version you just
|
|
86
|
+
* created by saving does not show up until the page is reloaded.
|
|
87
|
+
*/
|
|
88
|
+
updatedAt: a
|
|
89
|
+
}) => {
|
|
90
|
+
const { get: s, post: k } = K(), [u, A] = d.useState([]), [O, B] = d.useState(0), [E, M] = d.useState(1), [S, R] = d.useState(!0), [C, w] = d.useState(null), [l, y] = d.useState(null), [T, D] = d.useState(!1), [_, I] = d.useState(!1), [H, x] = d.useState(null), [W, j] = d.useState(!1), b = d.useCallback(
|
|
91
|
+
async (i) => {
|
|
92
|
+
R(!0), w(null);
|
|
93
|
+
try {
|
|
94
|
+
const { data: h } = await s(`/${m}/versions`, {
|
|
95
|
+
params: {
|
|
96
|
+
contentType: e,
|
|
97
|
+
documentId: n,
|
|
98
|
+
page: i,
|
|
99
|
+
pageSize: ne
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
A((U) => i === 1 ? h.data : [...U, ...h.data]), B(h.meta.total), M(i);
|
|
103
|
+
} catch {
|
|
104
|
+
w("Could not load the history for this document.");
|
|
105
|
+
} finally {
|
|
106
|
+
R(!1);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
[s, e, n]
|
|
110
|
+
);
|
|
111
|
+
d.useEffect(() => {
|
|
112
|
+
b(1);
|
|
113
|
+
}, [b, a]);
|
|
114
|
+
const G = async (i) => {
|
|
115
|
+
I(!0), j(!0), x(null);
|
|
116
|
+
try {
|
|
117
|
+
const { data: h } = await s(
|
|
118
|
+
`/${m}/versions/${i.id}/diff`
|
|
119
|
+
);
|
|
120
|
+
x(h.data);
|
|
121
|
+
} catch {
|
|
122
|
+
x(null);
|
|
123
|
+
} finally {
|
|
124
|
+
j(!1);
|
|
125
|
+
}
|
|
126
|
+
}, q = async (i) => {
|
|
127
|
+
try {
|
|
128
|
+
const { data: h } = await s(
|
|
129
|
+
`/${m}/versions/${i.id}/preview`
|
|
130
|
+
);
|
|
131
|
+
y({ version: i, preview: h.data });
|
|
132
|
+
} catch {
|
|
133
|
+
w("Could not work out what restoring this version would change.");
|
|
134
|
+
}
|
|
135
|
+
}, z = async () => {
|
|
136
|
+
if (l) {
|
|
137
|
+
D(!0);
|
|
138
|
+
try {
|
|
139
|
+
await k(`/${m}/versions/${l.version.id}/restore`, {}), window.location.reload();
|
|
140
|
+
} catch {
|
|
141
|
+
w("The restore failed. Nothing was changed."), D(!1), y(null);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return S && u.length === 0 ? /* @__PURE__ */ t(c, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ t($, { small: !0, children: "Loading history" }) }) : C && u.length === 0 ? /* @__PURE__ */ t(r, { variant: "pi", textColor: "danger600", children: C }) : u.length === 0 ? /* @__PURE__ */ t(r, { variant: "pi", textColor: "neutral600", children: "No versions yet. The next time this document is saved, a version will appear here." }) : /* @__PURE__ */ o(c, { direction: "column", alignItems: "stretch", gap: 2, width: "100%", children: [
|
|
146
|
+
u.map((i) => /* @__PURE__ */ o(f, { paddingBottom: 2, children: [
|
|
147
|
+
/* @__PURE__ */ o(c, { justifyContent: "space-between", alignItems: "center", gap: 2, children: [
|
|
148
|
+
/* @__PURE__ */ o(c, { direction: "column", alignItems: "flex-start", gap: 1, grow: 1, minWidth: 0, children: [
|
|
149
|
+
/* @__PURE__ */ o(c, { gap: 2, alignItems: "center", children: [
|
|
150
|
+
/* @__PURE__ */ t(
|
|
151
|
+
V,
|
|
152
|
+
{
|
|
153
|
+
backgroundColor: P.has(i.origin) ? "primary100" : "neutral150",
|
|
154
|
+
textColor: P.has(i.origin) ? "primary600" : "neutral600",
|
|
155
|
+
children: te[i.origin] ?? i.origin
|
|
156
|
+
}
|
|
157
|
+
),
|
|
158
|
+
i.locale ? /* @__PURE__ */ t(r, { variant: "pi", textColor: "neutral500", children: i.locale }) : null
|
|
159
|
+
] }),
|
|
160
|
+
i.label ? /* @__PURE__ */ t(f, { width: "100%", children: /* @__PURE__ */ t(r, { variant: "omega", textColor: "neutral800", ellipsis: !0, children: i.label }) }) : null,
|
|
161
|
+
/* @__PURE__ */ o(r, { variant: "pi", textColor: "neutral600", children: [
|
|
162
|
+
re(i.createdAt),
|
|
163
|
+
i.user ? ` · ${i.user.name}` : ""
|
|
164
|
+
] }),
|
|
165
|
+
/* @__PURE__ */ t(J, { onClick: () => G(i), children: "What changed" })
|
|
166
|
+
] }),
|
|
167
|
+
/* @__PURE__ */ t(f, { shrink: 0, children: /* @__PURE__ */ t(v, { size: "S", variant: "tertiary", onClick: () => q(i), children: "Restore" }) })
|
|
168
|
+
] }),
|
|
169
|
+
/* @__PURE__ */ t(f, { paddingTop: 2, children: /* @__PURE__ */ t(Z, {}) })
|
|
170
|
+
] }, i.id)),
|
|
171
|
+
u.length < O ? /* @__PURE__ */ t(v, { size: "S", variant: "tertiary", loading: S, onClick: () => b(E + 1), children: "Show older versions" }) : null,
|
|
172
|
+
C ? /* @__PURE__ */ t(r, { variant: "pi", textColor: "danger600", children: C }) : null,
|
|
173
|
+
_ ? /* @__PURE__ */ t(
|
|
174
|
+
ee,
|
|
175
|
+
{
|
|
176
|
+
diff: H,
|
|
177
|
+
loading: W,
|
|
178
|
+
onClose: () => I(!1)
|
|
179
|
+
}
|
|
180
|
+
) : null,
|
|
181
|
+
/* @__PURE__ */ t(p.Root, { open: l !== null, onOpenChange: () => y(null), children: /* @__PURE__ */ o(p.Content, { children: [
|
|
182
|
+
/* @__PURE__ */ t(p.Header, { children: "Restore this version?" }),
|
|
183
|
+
/* @__PURE__ */ t(p.Body, { children: /* @__PURE__ */ o(c, { direction: "column", alignItems: "flex-start", gap: 2, children: [
|
|
184
|
+
/* @__PURE__ */ t(r, { children: "The current draft is saved as a version first, so this can be undone. The published version is not touched." }),
|
|
185
|
+
l?.preview.crossLocaleFields.length ? /* @__PURE__ */ o(r, { textColor: "danger600", children: [
|
|
186
|
+
l.preview.crossLocaleFields.join(", "),
|
|
187
|
+
" ",
|
|
188
|
+
l.preview.crossLocaleFields.length === 1 ? "is" : "are",
|
|
189
|
+
" not translated per locale, so restoring",
|
|
190
|
+
" ",
|
|
191
|
+
l.preview.crossLocaleFields.length === 1 ? "it" : "them",
|
|
192
|
+
" changes",
|
|
193
|
+
" ",
|
|
194
|
+
l.preview.affectedLocales.length,
|
|
195
|
+
" locales at once:",
|
|
196
|
+
" ",
|
|
197
|
+
l.preview.affectedLocales.join(", "),
|
|
198
|
+
"."
|
|
199
|
+
] }) : null,
|
|
200
|
+
l?.preview.fieldsKeptAsIs.length ? /* @__PURE__ */ o(r, { variant: "pi", textColor: "neutral600", children: [
|
|
201
|
+
"Added since this version and left untouched:",
|
|
202
|
+
" ",
|
|
203
|
+
l.preview.fieldsKeptAsIs.join(", "),
|
|
204
|
+
"."
|
|
205
|
+
] }) : null,
|
|
206
|
+
l?.preview.fieldsDropped.length ? /* @__PURE__ */ o(r, { variant: "pi", textColor: "neutral600", children: [
|
|
207
|
+
"No longer in the model and skipped: ",
|
|
208
|
+
l.preview.fieldsDropped.join(", "),
|
|
209
|
+
"."
|
|
210
|
+
] }) : null,
|
|
211
|
+
l?.preview.brokenRelations.length || l?.preview.missingMedia.length ? /* @__PURE__ */ o(r, { variant: "pi", textColor: "warning600", children: [
|
|
212
|
+
(l.preview.brokenRelations.length ?? 0) + (l.preview.missingMedia.length ?? 0),
|
|
213
|
+
" ",
|
|
214
|
+
"linked item(s) no longer exist and will be left out."
|
|
215
|
+
] }) : null
|
|
216
|
+
] }) }),
|
|
217
|
+
/* @__PURE__ */ o(p.Footer, { children: [
|
|
218
|
+
/* @__PURE__ */ t(p.Cancel, { children: /* @__PURE__ */ t(v, { variant: "tertiary", disabled: T, children: "Cancel" }) }),
|
|
219
|
+
/* @__PURE__ */ t(v, { onClick: z, loading: T, children: "Restore" })
|
|
220
|
+
] })
|
|
221
|
+
] }) })
|
|
222
|
+
] });
|
|
223
|
+
}, oe = ({
|
|
224
|
+
model: e,
|
|
225
|
+
documentId: n,
|
|
226
|
+
document: a
|
|
227
|
+
}) => n ? {
|
|
228
|
+
title: "History",
|
|
229
|
+
content: /* @__PURE__ */ t(ie, { model: e, documentId: n, updatedAt: a?.updatedAt })
|
|
230
|
+
} : null, ce = {
|
|
231
|
+
register(e) {
|
|
232
|
+
e.registerPlugin({ id: m, name: m, isReady: !0 });
|
|
233
|
+
},
|
|
234
|
+
bootstrap(e) {
|
|
235
|
+
e.getPlugin("content-manager").apis.addEditViewSidePanel([oe]);
|
|
236
|
+
},
|
|
237
|
+
async registerTrads({ locales: e }) {
|
|
238
|
+
return Promise.all(
|
|
239
|
+
e.map(async (n) => {
|
|
240
|
+
try {
|
|
241
|
+
const { default: a } = await Q(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-DCctmu-a.mjs") }), `./translations/${n}.json`, 3);
|
|
242
|
+
return { data: a, locale: n };
|
|
243
|
+
} catch {
|
|
244
|
+
return { data: {}, locale: n };
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
export {
|
|
251
|
+
m as PLUGIN_ID,
|
|
252
|
+
ce as default,
|
|
253
|
+
de as registerDiffRenderer
|
|
254
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type FieldChange } from '../diffRegistry';
|
|
3
|
+
export interface VersionDiff {
|
|
4
|
+
from: {
|
|
5
|
+
id: number;
|
|
6
|
+
label: string | null;
|
|
7
|
+
origin: string;
|
|
8
|
+
} | null;
|
|
9
|
+
to: {
|
|
10
|
+
id: number;
|
|
11
|
+
label: string | null;
|
|
12
|
+
origin: string;
|
|
13
|
+
};
|
|
14
|
+
changes: FieldChange[];
|
|
15
|
+
identical: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare const ChangesDialog: ({ diff, loading, onClose, }: {
|
|
18
|
+
diff: VersionDiff | null;
|
|
19
|
+
loading: boolean;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
}) => React.JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The panel itself.
|
|
4
|
+
*
|
|
5
|
+
* Returning null while there is no `documentId` keeps it out of the way on the
|
|
6
|
+
* create screen, where there is nothing to have a history of yet.
|
|
7
|
+
*/
|
|
8
|
+
export declare const HistoryPanel: ({ model, documentId, document, }: {
|
|
9
|
+
model: string;
|
|
10
|
+
documentId?: string;
|
|
11
|
+
document?: {
|
|
12
|
+
updatedAt?: string;
|
|
13
|
+
};
|
|
14
|
+
}) => {
|
|
15
|
+
title: string;
|
|
16
|
+
content: React.JSX.Element;
|
|
17
|
+
} | null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
export type DiffOp = 'equal' | 'added' | 'removed';
|
|
3
|
+
export interface DiffSpan {
|
|
4
|
+
op: DiffOp;
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FieldChange {
|
|
8
|
+
field: string;
|
|
9
|
+
type: string;
|
|
10
|
+
/** Set when the attribute is a custom field; more specific than `type`. */
|
|
11
|
+
customField?: string;
|
|
12
|
+
kind: 'added' | 'removed' | 'changed';
|
|
13
|
+
before?: unknown;
|
|
14
|
+
after?: unknown;
|
|
15
|
+
spans?: DiffSpan[];
|
|
16
|
+
linked?: {
|
|
17
|
+
documentId?: string;
|
|
18
|
+
id?: number;
|
|
19
|
+
targetUid: string;
|
|
20
|
+
}[];
|
|
21
|
+
unlinked?: {
|
|
22
|
+
documentId?: string;
|
|
23
|
+
id?: number;
|
|
24
|
+
targetUid: string;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
export type DiffRenderer = React.ComponentType<{
|
|
28
|
+
change: FieldChange;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Lets another package render the diff for its own field type.
|
|
32
|
+
*
|
|
33
|
+
* Rewind compares a rich-text field by pulling the readable words out of the
|
|
34
|
+
* JSON, which tells an editor *that* a paragraph changed but nothing about
|
|
35
|
+
* blocks moving, an image being swapped, or a table gaining a row. The package
|
|
36
|
+
* that owns the field format is the only one that can show that properly, so
|
|
37
|
+
* the type-to-component mapping is a registry rather than a switch:
|
|
38
|
+
*
|
|
39
|
+
* import { registerDiffRenderer } from '@qkix/strapi-plugin-rewind/strapi-admin';
|
|
40
|
+
* registerDiffRenderer('plugin::better-blocks.better-blocks', BlocksDiff);
|
|
41
|
+
*
|
|
42
|
+
* Keyed by the attribute's `type`, or by a custom field's uid.
|
|
43
|
+
*/
|
|
44
|
+
export declare const registerDiffRenderer: (type: string, renderer: DiffRenderer) => void;
|
|
45
|
+
export declare const getDiffRenderer: (type: string) => DiffRenderer | undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
register(app: any): void;
|
|
3
|
+
bootstrap(app: any): void;
|
|
4
|
+
registerTrads({ locales }: {
|
|
5
|
+
locales: string[];
|
|
6
|
+
}): Promise<{
|
|
7
|
+
data: any;
|
|
8
|
+
locale: string;
|
|
9
|
+
}[]>;
|
|
10
|
+
};
|
|
11
|
+
export default _default;
|
|
12
|
+
export { PLUGIN_ID } from './pluginId';
|
|
13
|
+
/**
|
|
14
|
+
* The hook a package owning a field format uses to render its own diff — see
|
|
15
|
+
* diffRegistry for why this is a registry and not a switch.
|
|
16
|
+
*/
|
|
17
|
+
export { registerDiffRenderer } from './diffRegistry';
|
|
18
|
+
export type { DiffRenderer, FieldChange, DiffSpan } from './diffRegistry';
|