@qkix/strapi-plugin-rewind 0.1.3 → 0.2.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/README.md +33 -19
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +145 -121
- package/dist/admin/src/diffRegistry.d.ts +1 -1
- package/dist/admin/src/index.d.ts +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/index.mjs +204 -171
- package/dist/server/src/config/index.d.ts +1 -1
- package/dist/server/src/content-types/index.d.ts +2 -2
- package/dist/server/src/controllers/index.d.ts +1 -0
- package/dist/server/src/controllers/version.d.ts +6 -0
- package/dist/server/src/index.d.ts +9 -0
- package/dist/server/src/services/diff.d.ts +6 -6
- package/dist/server/src/services/index.d.ts +8 -0
- package/dist/server/src/services/lock.d.ts +1 -1
- package/dist/server/src/services/pin.d.ts +19 -0
- package/dist/server/src/services/restore.d.ts +4 -4
- package/dist/server/src/services/serializer.d.ts +3 -3
- package/dist/server/src/services/snapshot.d.ts +1 -1
- package/dist/server/src/utils/backfillLabels.d.ts +1 -1
- package/dist/server/src/utils/captureContext.d.ts +1 -1
- package/dist/server/src/utils/persistTable.d.ts +2 -2
- package/dist/server/src/utils/textDiff.d.ts +2 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Strapi sells a feature called **Content History** on its Growth and Enterprise
|
|
|
34
34
|
plans. Rewind is **not that feature**, is not a drop-in replacement for it, and
|
|
35
35
|
does not unlock it.
|
|
36
36
|
|
|
37
|
-
It is the poor cousin. If you pay for Growth, use Strapi's
|
|
37
|
+
It is the poor cousin. If you pay for Growth, use Strapi's - it is deeper, it is
|
|
38
38
|
supported by the people who wrote the CMS, and it will stay in step with Strapi
|
|
39
39
|
in a way a third-party plugin cannot promise. Rewind exists for everyone on
|
|
40
40
|
Community Edition, who today has nothing at all: no way to see what a document
|
|
@@ -56,7 +56,7 @@ cheap to add once the plumbing existed:
|
|
|
56
56
|
- **Writes from outside the Content Manager can be versioned** (opt-in), where
|
|
57
57
|
Strapi only ever records admin-panel edits.
|
|
58
58
|
|
|
59
|
-
And a good deal it does _not_ do
|
|
59
|
+
And a good deal it does _not_ do - see [Limits](#limits).
|
|
60
60
|
|
|
61
61
|
### On not touching the paywall
|
|
62
62
|
|
|
@@ -64,8 +64,8 @@ Rewind never reads or writes `strapi.ee`, never inspects a licence, and never
|
|
|
64
64
|
enables a gated feature. It is an independent implementation writing to its own
|
|
65
65
|
table, and it works the same whether or not you have a Strapi licence. Strapi's
|
|
66
66
|
history code is MIT-licensed and was read while working out what the problem
|
|
67
|
-
actually is
|
|
68
|
-
writing your own
|
|
67
|
+
actually is - the way you would read any open-source implementation before
|
|
68
|
+
writing your own - but the code here is written for this plugin.
|
|
69
69
|
|
|
70
70
|
## Install
|
|
71
71
|
|
|
@@ -94,7 +94,7 @@ the first disk alert.
|
|
|
94
94
|
|
|
95
95
|
Six actions on a tracked content type: `create`, `update`, `clone`, `publish`,
|
|
96
96
|
`unpublish` and `discardDraft`. Each version stores the document's content, its
|
|
97
|
-
relations, and a snapshot of the schema at the time
|
|
97
|
+
relations, and a snapshot of the schema at the time - the last of which is what
|
|
98
98
|
lets a restore tell "this field was empty" apart from "this field did not exist
|
|
99
99
|
yet".
|
|
100
100
|
|
|
@@ -106,7 +106,7 @@ A save that is rolled back records nothing.
|
|
|
106
106
|
<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" />
|
|
107
107
|
|
|
108
108
|
The badge says what happened to the document. **Replaced by restore** marks the
|
|
109
|
-
state a restore was about to overwrite
|
|
109
|
+
state a restore was about to overwrite - the undo point for that restore.
|
|
110
110
|
|
|
111
111
|
## Restoring
|
|
112
112
|
|
|
@@ -125,7 +125,7 @@ part of a restore is what it will _not_ do:
|
|
|
125
125
|
- fields no longer in the model are skipped
|
|
126
126
|
- links to documents or media that have since been deleted are dropped, and
|
|
127
127
|
reported
|
|
128
|
-
- **fields that are not translated per locale change in every locale at once**
|
|
128
|
+
- **fields that are not translated per locale change in every locale at once** -
|
|
129
129
|
restoring the Polish version of a document rewrites a shared field for every
|
|
130
130
|
other language too. This is not a choice the plugin makes; a non-localised
|
|
131
131
|
field physically has one value. The dialog names those fields and counts the
|
|
@@ -138,14 +138,14 @@ with the one saved immediately before it.
|
|
|
138
138
|
|
|
139
139
|
<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" />
|
|
140
140
|
|
|
141
|
-
Scalars are shown as a plain before and after. Prose
|
|
142
|
-
stored as JSON
|
|
141
|
+
Scalars are shown as a plain before and after. Prose - including rich text
|
|
142
|
+
stored as JSON - is compared word by word, with the unchanged stretches
|
|
143
143
|
collapsed, so a one-word edit in a long article reads as one word rather than
|
|
144
144
|
as the whole article with something green in it somewhere. Relations are
|
|
145
145
|
reported as linked and unlinked.
|
|
146
146
|
|
|
147
|
-
Where a field's stored value changed but its readable text did not
|
|
148
|
-
applied, blocks reordered
|
|
147
|
+
Where a field's stored value changed but its readable text did not - a mark
|
|
148
|
+
applied, blocks reordered - it says so, rather than showing an empty diff.
|
|
149
149
|
|
|
150
150
|
### Rendering a field type properly
|
|
151
151
|
|
|
@@ -156,7 +156,7 @@ package that owns the format can show that, so the mapping is a registry:
|
|
|
156
156
|
```ts
|
|
157
157
|
import { registerDiffRenderer } from '@qkix/strapi-plugin-rewind/strapi-admin';
|
|
158
158
|
|
|
159
|
-
import { MyBlocksDiff } from './MyBlocksDiff'; // yours
|
|
159
|
+
import { MyBlocksDiff } from './MyBlocksDiff'; // yours - see below
|
|
160
160
|
|
|
161
161
|
registerDiffRenderer('plugin::better-blocks.better-blocks', MyBlocksDiff);
|
|
162
162
|
```
|
|
@@ -165,13 +165,24 @@ Keyed by a custom field's uid, or by an attribute `type`. A uid wins over a
|
|
|
165
165
|
type, since every custom field stores itself as `json`.
|
|
166
166
|
|
|
167
167
|
> **Bring your own renderer.** Rewind ships the registry, not the renderers. No
|
|
168
|
-
> `@qkix` package currently exports a block-aware diff component
|
|
169
|
-
> Better Blocks
|
|
168
|
+
> `@qkix` package currently exports a block-aware diff component - including
|
|
169
|
+
> Better Blocks - so the component above is one you write. A renderer receives
|
|
170
170
|
> the two values and returns the `FieldChange` shape exported alongside
|
|
171
171
|
> `registerDiffRenderer`; without one registered, a field falls back to the
|
|
172
172
|
> generic text diff. A ready-made renderer for Better Blocks documents is
|
|
173
173
|
> wanted, and not yet written.
|
|
174
174
|
|
|
175
|
+
## Pinning a version
|
|
176
|
+
|
|
177
|
+
Every entry has a pin. A pinned version is never thinned, whatever the retention
|
|
178
|
+
settings say and however old it gets - the state the site launched in, the draft
|
|
179
|
+
before a rewrite, the version somebody will ask about in a year.
|
|
180
|
+
|
|
181
|
+
Pinning is the one thing in this panel that writes to the history rather than to
|
|
182
|
+
the document, and it is the whole of it: a version is pinned or it is not.
|
|
183
|
+
Nothing else about the version changes, and unpinning hands it straight back to
|
|
184
|
+
the next prune.
|
|
185
|
+
|
|
175
186
|
## Configuration
|
|
176
187
|
|
|
177
188
|
| Option | Default | Meaning |
|
|
@@ -185,8 +196,8 @@ type, since every custom field stores itself as `json`.
|
|
|
185
196
|
| `retention.keepAnchors` | `true` | Never thin publish, unpublish, discard or restore versions, whatever their age. |
|
|
186
197
|
| `cron` | `'0 3 * * *'` | When the thinning runs, in the server's timezone. |
|
|
187
198
|
|
|
188
|
-
The three windows must widen in order
|
|
189
|
-
maxAgeDays`
|
|
199
|
+
The three windows must widen in order - `keepAllDays <= dailyUntilDays <=
|
|
200
|
+
maxAgeDays` - and the plugin refuses to boot rather than delete on a guess if
|
|
190
201
|
they do not. Pinned versions are never thinned either, independently of
|
|
191
202
|
`keepAnchors`.
|
|
192
203
|
|
|
@@ -202,8 +213,6 @@ Worth knowing before you install it, not after:
|
|
|
202
213
|
- **Only the Document Service is visible.** Writes made through
|
|
203
214
|
`strapi.db.query()` or the legacy entity service bypass the middleware
|
|
204
215
|
entirely and cannot be captured at any setting.
|
|
205
|
-
- **Nothing pins a version from the panel yet.** The column is there and prune
|
|
206
|
-
honours it, but there is no button; pin by setting `pinned` on the row.
|
|
207
216
|
- **Deleted relation targets lose their names.** A version stores which document
|
|
208
217
|
a relation pointed at, not what it was called, so a link to something since
|
|
209
218
|
deleted shows as missing rather than as "the article it used to be".
|
|
@@ -213,7 +222,7 @@ Worth knowing before you install it, not after:
|
|
|
213
222
|
## Keeping your history when the plugin is off
|
|
214
223
|
|
|
215
224
|
Strapi's schema sync drops tables belonging to content types it no longer sees.
|
|
216
|
-
Disable this plugin for a single boot and the history would go with it
|
|
225
|
+
Disable this plugin for a single boot and the history would go with it - so on
|
|
217
226
|
boot Rewind adds its table to Strapi's `persisted_tables` list, which is the
|
|
218
227
|
same mechanism Strapi uses to protect its own. Nothing to configure.
|
|
219
228
|
|
|
@@ -221,6 +230,11 @@ same mechanism Strapi uses to protect its own. Nothing to configure.
|
|
|
221
230
|
|
|
222
231
|
Strapi 5, Node 20 or 22.
|
|
223
232
|
|
|
233
|
+
## Roadmap
|
|
234
|
+
|
|
235
|
+
What is worth building next, what is deliberately not planned, and why:
|
|
236
|
+
[ROADMAP.md](https://github.com/qkix/strapi-plugins/blob/main/packages/strapi-plugin-rewind/ROADMAP.md).
|
|
237
|
+
|
|
224
238
|
## License
|
|
225
239
|
|
|
226
240
|
MIT.
|
package/dist/admin/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("react/jsx-runtime"),
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("react/jsx-runtime"),W=require("react"),z=require("@strapi/admin/strapi-admin"),t=require("@strapi/design-system"),G=require("@strapi/icons");function U(n){if(n&&n.__esModule)return n;const r=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const s in n)if(s!=="default"){const a=Object.getOwnPropertyDescriptor(n,s);Object.defineProperty(r,s,a.get?a:{enumerable:!0,get:()=>n[s]})}}return r.default=n,Object.freeze(r)}const d=U(W),K=(n,r,s)=>{const a=n[r];return a?typeof a=="function"?a():Promise.resolve(a):new Promise((C,y)=>{(typeof queueMicrotask=="function"?queueMicrotask:setTimeout)(y.bind(null,new Error("Unknown variable dynamic import: "+r+(r.split("/").length!==s?". Note that variables only represent file names one level deep.":""))))})},P=new Map,V=(n,r)=>{P.set(n,r)},R=n=>P.get(n),J=({spans:n})=>e.jsx(t.Typography,{variant:"omega",tag:"p",children:n.map((r,s)=>r.op==="equal"?e.jsx(t.Typography,{variant:"omega",textColor:"neutral600",children:r.value},s):e.jsx(t.Typography,{variant:"omega",textColor:r.op==="added"?"success600":"danger600",style:r.op==="removed"?{textDecoration:"line-through"}:{fontWeight:600},children:r.value},s))}),F=n=>n==null||n===""?"(empty)":typeof n=="object"?JSON.stringify(n):String(n),Z=({change:n})=>{const r=(n.customField?R(n.customField):void 0)??R(n.type);return r?e.jsx(r,{change:n}):n.spans?e.jsx(J,{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:F(n.before)}),e.jsx(t.Typography,{variant:"omega",textColor:"neutral500",children:"→"}),e.jsx(t.Typography,{variant:"omega",textColor:"success600",children:F(n.after)})]}):e.jsx(t.Typography,{variant:"pi",textColor:"neutral600",children:"Formatting or structure changed; the text is the same."})},Q=({diff:n,loading:r,onClose:s})=>e.jsx(t.Modal.Root,{open:!0,onOpenChange:s,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(Z,{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:s,children:"Close"})})]})}),c="rewind",X={create:"Created",update:"Edited",clone:"Cloned",publish:"Published",unpublish:"Unpublished",discardDraft:"Draft discarded",restore:"Replaced by restore"},I=new Set(["publish","unpublish","discardDraft","restore"]),Y=10,ee=n=>{const r=new Date(n),s=Math.round((Date.now()-r.getTime())/6e4);return s<1?"just now":s<60?`${s} min ago`:s<60*24?`${Math.round(s/60)} h ago`:r.toLocaleString()},te=({model:n,documentId:r,updatedAt:s})=>{const{get:a,post:C,put:y}=z.useFetchClient(),[p,j]=d.useState([]),[B,L]=d.useState(0),[M,O]=d.useState(1),[w,T]=d.useState(!0),[x,h]=d.useState(null),[o,f]=d.useState(null),[b,S]=d.useState(!1),[_,k]=d.useState(!1),[$,m]=d.useState(null),[N,D]=d.useState(!1),v=d.useCallback(async i=>{T(!0),h(null);try{const{data:l}=await a(`/${c}/versions`,{params:{contentType:n,documentId:r,page:i,pageSize:Y}});j(g=>i===1?l.data:[...g,...l.data]),L(l.meta.total),O(i)}catch{h("Could not load the history for this document.")}finally{T(!1)}},[a,n,r]);d.useEffect(()=>{v(1)},[v,s]);const A=async i=>{k(!0),D(!0),m(null);try{const{data:l}=await a(`/${c}/versions/${i.id}/diff`);m(l.data)}catch{m(null)}finally{D(!1)}},q=async i=>{const l=!i.pinned;j(g=>g.map(u=>u.id===i.id?{...u,pinned:l}:u));try{await y(`/${c}/versions/${i.id}/pin`,{pinned:l})}catch{j(g=>g.map(u=>u.id===i.id?{...u,pinned:!l}:u)),h(l?"Could not pin that version.":"Could not unpin that version.")}},E=async i=>{try{const{data:l}=await a(`/${c}/versions/${i.id}/preview`);f({version:i,preview:l.data})}catch{h("Could not work out what restoring this version would change.")}},H=async()=>{if(o){S(!0);try{await C(`/${c}/versions/${o.version.id}/restore`,{}),window.location.reload()}catch{h("The restore failed. Nothing was changed."),S(!1),f(null)}}};return w&&p.length===0?e.jsx(t.Flex,{justifyContent:"center",padding:4,children:e.jsx(t.Loader,{small:!0,children:"Loading history"})}):x&&p.length===0?e.jsx(t.Typography,{variant:"pi",textColor:"danger600",children:x}):p.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:[p.map(i=>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:I.has(i.origin)?"primary100":"neutral150",textColor:I.has(i.origin)?"primary600":"neutral600",children:X[i.origin]??i.origin}),i.locale?e.jsx(t.Typography,{variant:"pi",textColor:"neutral500",children:i.locale}):null]}),i.label?e.jsx(t.Box,{width:"100%",children:e.jsx(t.Typography,{variant:"omega",textColor:"neutral800",ellipsis:!0,children:i.label})}):null,e.jsxs(t.Typography,{variant:"pi",textColor:"neutral600",children:[ee(i.createdAt),i.user?` · ${i.user.name}`:"",i.pinned?e.jsx(t.Typography,{variant:"pi",textColor:"primary600",fontWeight:"bold",children:" · Pinned"}):null]}),e.jsx(t.TextButton,{onClick:()=>A(i),children:"What changed"})]}),e.jsxs(t.Flex,{shrink:0,gap:1,alignItems:"center",children:[e.jsx(t.IconButton,{size:"S",variant:"ghost",label:i.pinned?"Unpin this version, so tidying up can remove it":"Pin this version, so tidying up never removes it",onClick:()=>q(i),children:e.jsx(G.Pin,{fill:i.pinned?"primary600":"neutral500"})}),e.jsx(t.Button,{size:"S",variant:"tertiary",onClick:()=>E(i),children:"Restore"})]})]}),e.jsx(t.Box,{paddingTop:2,children:e.jsx(t.Divider,{})})]},i.id)),p.length<B?e.jsx(t.Button,{size:"S",variant:"tertiary",loading:w,onClick:()=>v(M+1),children:"Show older versions"}):null,x?e.jsx(t.Typography,{variant:"pi",textColor:"danger600",children:x}):null,_?e.jsx(Q,{diff:$,loading:N,onClose:()=>k(!1)}):null,e.jsx(t.Dialog.Root,{open:o!==null,onOpenChange:()=>f(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:b,children:"Cancel"})}),e.jsx(t.Button,{onClick:H,loading:b,children:"Restore"})]})]})})]})},ne=({model:n,documentId:r,document:s})=>r?{title:"History",content:e.jsx(te,{model:n,documentId:r,updatedAt:s?.updatedAt})}:null,re={register(n){n.registerPlugin({id:c,name:c,isReady:!0})},bootstrap(n){n.getPlugin("content-manager").apis.addEditViewSidePanel([ne])},async registerTrads({locales:n}){return Promise.all(n.map(async r=>{try{const{default:s}=await K(Object.assign({"./translations/en.json":()=>Promise.resolve().then(()=>require("../_chunks/en-BHfx2OWt.js"))}),`./translations/${r}.json`,3);return{data:s,locale:r}}catch{return{data:{},locale:r}}}))}};exports.PLUGIN_ID=c;exports.default=re;exports.registerDiffRenderer=V;
|
package/dist/admin/index.mjs
CHANGED
|
@@ -1,71 +1,72 @@
|
|
|
1
1
|
import { jsx as t, jsxs as o } from "react/jsx-runtime";
|
|
2
|
-
import * as
|
|
3
|
-
import { useFetchClient as
|
|
4
|
-
import { Modal as g, Flex as
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import * as c from "react";
|
|
3
|
+
import { useFetchClient as Z } from "@strapi/admin/strapi-admin";
|
|
4
|
+
import { Modal as g, Flex as u, Loader as O, Typography as i, Box as w, Button as y, Badge as Q, TextButton as X, IconButton as Y, Divider as ee, Dialog as m } from "@strapi/design-system";
|
|
5
|
+
import { Pin as te } from "@strapi/icons";
|
|
6
|
+
const ne = (e, r, a) => {
|
|
7
|
+
const s = e[r];
|
|
8
|
+
return s ? typeof s == "function" ? s() : Promise.resolve(s) : new Promise((T, b) => {
|
|
8
9
|
(typeof queueMicrotask == "function" ? queueMicrotask : setTimeout)(
|
|
9
|
-
|
|
10
|
+
b.bind(
|
|
10
11
|
null,
|
|
11
12
|
new Error(
|
|
12
|
-
"Unknown variable dynamic import: " +
|
|
13
|
+
"Unknown variable dynamic import: " + r + (r.split("/").length !== a ? ". Note that variables only represent file names one level deep." : "")
|
|
13
14
|
)
|
|
14
15
|
)
|
|
15
16
|
);
|
|
16
17
|
});
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
},
|
|
20
|
-
|
|
18
|
+
}, E = /* @__PURE__ */ new Map(), me = (e, r) => {
|
|
19
|
+
E.set(e, r);
|
|
20
|
+
}, N = (e) => E.get(e), re = ({ spans: e }) => /* @__PURE__ */ t(i, { variant: "omega", tag: "p", children: e.map((r, a) => r.op === "equal" ? /* @__PURE__ */ t(i, { variant: "omega", textColor: "neutral600", children: r.value }, a) : /* @__PURE__ */ t(
|
|
21
|
+
i,
|
|
21
22
|
{
|
|
22
23
|
variant: "omega",
|
|
23
|
-
textColor:
|
|
24
|
-
style:
|
|
25
|
-
children:
|
|
24
|
+
textColor: r.op === "added" ? "success600" : "danger600",
|
|
25
|
+
style: r.op === "removed" ? { textDecoration: "line-through" } : { fontWeight: 600 },
|
|
26
|
+
children: r.value
|
|
26
27
|
},
|
|
27
28
|
a
|
|
28
|
-
)) }),
|
|
29
|
-
const
|
|
30
|
-
return
|
|
31
|
-
e.linked?.length ? /* @__PURE__ */ o(
|
|
29
|
+
)) }), A = (e) => e == null || e === "" ? "(empty)" : typeof e == "object" ? JSON.stringify(e) : String(e), ie = ({ change: e }) => {
|
|
30
|
+
const r = (e.customField ? N(e.customField) : void 0) ?? N(e.type);
|
|
31
|
+
return r ? /* @__PURE__ */ t(r, { change: e }) : e.spans ? /* @__PURE__ */ t(re, { spans: e.spans }) : e.linked || e.unlinked ? /* @__PURE__ */ o(u, { direction: "column", alignItems: "flex-start", gap: 1, children: [
|
|
32
|
+
e.linked?.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "success600", children: [
|
|
32
33
|
"+ ",
|
|
33
34
|
e.linked.length,
|
|
34
35
|
" linked"
|
|
35
36
|
] }) : null,
|
|
36
|
-
e.unlinked?.length ? /* @__PURE__ */ o(
|
|
37
|
+
e.unlinked?.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "danger600", children: [
|
|
37
38
|
"− ",
|
|
38
39
|
e.unlinked.length,
|
|
39
40
|
" unlinked"
|
|
40
41
|
] }) : null
|
|
41
|
-
] }) : e.before !== void 0 || e.after !== void 0 ? /* @__PURE__ */ o(
|
|
42
|
+
] }) : e.before !== void 0 || e.after !== void 0 ? /* @__PURE__ */ o(u, { gap: 2, alignItems: "center", wrap: "wrap", children: [
|
|
42
43
|
/* @__PURE__ */ t(
|
|
43
|
-
|
|
44
|
+
i,
|
|
44
45
|
{
|
|
45
46
|
variant: "omega",
|
|
46
47
|
textColor: "danger600",
|
|
47
48
|
style: { textDecoration: "line-through" },
|
|
48
|
-
children:
|
|
49
|
+
children: A(e.before)
|
|
49
50
|
}
|
|
50
51
|
),
|
|
51
|
-
/* @__PURE__ */ t(
|
|
52
|
-
/* @__PURE__ */ t(
|
|
53
|
-
] }) : /* @__PURE__ */ t(
|
|
54
|
-
},
|
|
52
|
+
/* @__PURE__ */ t(i, { variant: "omega", textColor: "neutral500", children: "→" }),
|
|
53
|
+
/* @__PURE__ */ t(i, { variant: "omega", textColor: "success600", children: A(e.after) })
|
|
54
|
+
] }) : /* @__PURE__ */ t(i, { variant: "pi", textColor: "neutral600", children: "Formatting or structure changed; the text is the same." });
|
|
55
|
+
}, oe = ({
|
|
55
56
|
diff: e,
|
|
56
|
-
loading:
|
|
57
|
+
loading: r,
|
|
57
58
|
onClose: a
|
|
58
59
|
}) => /* @__PURE__ */ t(g.Root, { open: !0, onOpenChange: a, children: /* @__PURE__ */ o(g.Content, { children: [
|
|
59
60
|
/* @__PURE__ */ t(g.Header, { children: /* @__PURE__ */ t(g.Title, { children: "What changed" }) }),
|
|
60
|
-
/* @__PURE__ */ t(g.Body, { children:
|
|
61
|
-
/* @__PURE__ */ t(
|
|
62
|
-
e.identical ? /* @__PURE__ */ t(
|
|
63
|
-
/* @__PURE__ */ t(
|
|
64
|
-
/* @__PURE__ */ t(
|
|
61
|
+
/* @__PURE__ */ t(g.Body, { children: r ? /* @__PURE__ */ t(u, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ t(O, { small: !0, children: "Comparing" }) }) : e ? /* @__PURE__ */ o(u, { direction: "column", alignItems: "stretch", gap: 4, children: [
|
|
62
|
+
/* @__PURE__ */ t(i, { 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." }),
|
|
63
|
+
e.identical ? /* @__PURE__ */ t(i, { textColor: "neutral600", children: "Nothing changed between these two versions." }) : e.changes.map((s) => /* @__PURE__ */ o(w, { children: [
|
|
64
|
+
/* @__PURE__ */ t(i, { variant: "sigma", textColor: "neutral600", children: s.field }),
|
|
65
|
+
/* @__PURE__ */ t(w, { paddingTop: 1, children: /* @__PURE__ */ t(ie, { change: s }) })
|
|
65
66
|
] }, s.field))
|
|
66
|
-
] }) : /* @__PURE__ */ t(
|
|
67
|
-
/* @__PURE__ */ t(g.Footer, { children: /* @__PURE__ */ t(
|
|
68
|
-
] }) }),
|
|
67
|
+
] }) : /* @__PURE__ */ t(i, { textColor: "danger600", children: "Could not compare these versions." }) }),
|
|
68
|
+
/* @__PURE__ */ t(g.Footer, { children: /* @__PURE__ */ t(y, { variant: "tertiary", onClick: a, children: "Close" }) })
|
|
69
|
+
] }) }), p = "rewind", ae = {
|
|
69
70
|
create: "Created",
|
|
70
71
|
update: "Edited",
|
|
71
72
|
clone: "Cloned",
|
|
@@ -73,12 +74,12 @@ const Q = (e, n, a) => {
|
|
|
73
74
|
unpublish: "Unpublished",
|
|
74
75
|
discardDraft: "Draft discarded",
|
|
75
76
|
restore: "Replaced by restore"
|
|
76
|
-
},
|
|
77
|
-
const
|
|
78
|
-
return a < 1 ? "just now" : a < 60 ? `${a} min ago` : a < 60 * 24 ? `${Math.round(a / 60)} h ago` :
|
|
79
|
-
},
|
|
77
|
+
}, B = /* @__PURE__ */ new Set(["publish", "unpublish", "discardDraft", "restore"]), le = 10, se = (e) => {
|
|
78
|
+
const r = new Date(e), a = Math.round((Date.now() - r.getTime()) / 6e4);
|
|
79
|
+
return a < 1 ? "just now" : a < 60 ? `${a} min ago` : a < 60 * 24 ? `${Math.round(a / 60)} h ago` : r.toLocaleString();
|
|
80
|
+
}, de = ({
|
|
80
81
|
model: e,
|
|
81
|
-
documentId:
|
|
82
|
+
documentId: r,
|
|
82
83
|
/**
|
|
83
84
|
* Changes whenever the Content Manager re-reads the document, which is what
|
|
84
85
|
* it does after a save. Used purely as a refresh signal: without it the panel
|
|
@@ -87,102 +88,125 @@ const Q = (e, n, a) => {
|
|
|
87
88
|
*/
|
|
88
89
|
updatedAt: a
|
|
89
90
|
}) => {
|
|
90
|
-
const { get: s, post:
|
|
91
|
-
async (
|
|
92
|
-
|
|
91
|
+
const { get: s, post: T, put: b } = Z(), [f, k] = c.useState([]), [M, _] = c.useState(0), [H, W] = c.useState(1), [D, j] = c.useState(!0), [x, C] = c.useState(null), [l, S] = c.useState(null), [L, P] = c.useState(!1), [z, $] = c.useState(!1), [G, R] = c.useState(null), [U, F] = c.useState(!1), I = c.useCallback(
|
|
92
|
+
async (n) => {
|
|
93
|
+
j(!0), C(null);
|
|
93
94
|
try {
|
|
94
|
-
const { data:
|
|
95
|
+
const { data: d } = await s(`/${p}/versions`, {
|
|
95
96
|
params: {
|
|
96
97
|
contentType: e,
|
|
97
|
-
documentId:
|
|
98
|
-
page:
|
|
99
|
-
pageSize:
|
|
98
|
+
documentId: r,
|
|
99
|
+
page: n,
|
|
100
|
+
pageSize: le
|
|
100
101
|
}
|
|
101
102
|
});
|
|
102
|
-
|
|
103
|
+
k((v) => n === 1 ? d.data : [...v, ...d.data]), _(d.meta.total), W(n);
|
|
103
104
|
} catch {
|
|
104
|
-
|
|
105
|
+
C("Could not load the history for this document.");
|
|
105
106
|
} finally {
|
|
106
|
-
|
|
107
|
+
j(!1);
|
|
107
108
|
}
|
|
108
109
|
},
|
|
109
|
-
[s, e,
|
|
110
|
+
[s, e, r]
|
|
110
111
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}, [
|
|
114
|
-
const
|
|
115
|
-
|
|
112
|
+
c.useEffect(() => {
|
|
113
|
+
I(1);
|
|
114
|
+
}, [I, a]);
|
|
115
|
+
const q = async (n) => {
|
|
116
|
+
$(!0), F(!0), R(null);
|
|
116
117
|
try {
|
|
117
|
-
const { data:
|
|
118
|
-
`/${
|
|
118
|
+
const { data: d } = await s(
|
|
119
|
+
`/${p}/versions/${n.id}/diff`
|
|
119
120
|
);
|
|
120
|
-
|
|
121
|
+
R(d.data);
|
|
121
122
|
} catch {
|
|
122
|
-
|
|
123
|
+
R(null);
|
|
123
124
|
} finally {
|
|
124
|
-
|
|
125
|
+
F(!1);
|
|
125
126
|
}
|
|
126
|
-
},
|
|
127
|
+
}, K = async (n) => {
|
|
128
|
+
const d = !n.pinned;
|
|
129
|
+
k((v) => v.map((h) => h.id === n.id ? { ...h, pinned: d } : h));
|
|
127
130
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
await b(`/${p}/versions/${n.id}/pin`, { pinned: d });
|
|
132
|
+
} catch {
|
|
133
|
+
k(
|
|
134
|
+
(v) => v.map((h) => h.id === n.id ? { ...h, pinned: !d } : h)
|
|
135
|
+
), C(d ? "Could not pin that version." : "Could not unpin that version.");
|
|
136
|
+
}
|
|
137
|
+
}, V = async (n) => {
|
|
138
|
+
try {
|
|
139
|
+
const { data: d } = await s(
|
|
140
|
+
`/${p}/versions/${n.id}/preview`
|
|
130
141
|
);
|
|
131
|
-
|
|
142
|
+
S({ version: n, preview: d.data });
|
|
132
143
|
} catch {
|
|
133
|
-
|
|
144
|
+
C("Could not work out what restoring this version would change.");
|
|
134
145
|
}
|
|
135
|
-
},
|
|
146
|
+
}, J = async () => {
|
|
136
147
|
if (l) {
|
|
137
|
-
|
|
148
|
+
P(!0);
|
|
138
149
|
try {
|
|
139
|
-
await
|
|
150
|
+
await T(`/${p}/versions/${l.version.id}/restore`, {}), window.location.reload();
|
|
140
151
|
} catch {
|
|
141
|
-
|
|
152
|
+
C("The restore failed. Nothing was changed."), P(!1), S(null);
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
};
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
/* @__PURE__ */ o(
|
|
148
|
-
/* @__PURE__ */ o(
|
|
149
|
-
/* @__PURE__ */ o(
|
|
156
|
+
return D && f.length === 0 ? /* @__PURE__ */ t(u, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ t(O, { small: !0, children: "Loading history" }) }) : x && f.length === 0 ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "danger600", children: x }) : f.length === 0 ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "neutral600", children: "No versions yet. The next time this document is saved, a version will appear here." }) : /* @__PURE__ */ o(u, { direction: "column", alignItems: "stretch", gap: 2, width: "100%", children: [
|
|
157
|
+
f.map((n) => /* @__PURE__ */ o(w, { paddingBottom: 2, children: [
|
|
158
|
+
/* @__PURE__ */ o(u, { justifyContent: "space-between", alignItems: "center", gap: 2, children: [
|
|
159
|
+
/* @__PURE__ */ o(u, { direction: "column", alignItems: "flex-start", gap: 1, grow: 1, minWidth: 0, children: [
|
|
160
|
+
/* @__PURE__ */ o(u, { gap: 2, alignItems: "center", children: [
|
|
150
161
|
/* @__PURE__ */ t(
|
|
151
|
-
|
|
162
|
+
Q,
|
|
152
163
|
{
|
|
153
|
-
backgroundColor:
|
|
154
|
-
textColor:
|
|
155
|
-
children:
|
|
164
|
+
backgroundColor: B.has(n.origin) ? "primary100" : "neutral150",
|
|
165
|
+
textColor: B.has(n.origin) ? "primary600" : "neutral600",
|
|
166
|
+
children: ae[n.origin] ?? n.origin
|
|
156
167
|
}
|
|
157
168
|
),
|
|
158
|
-
|
|
169
|
+
n.locale ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "neutral500", children: n.locale }) : null
|
|
159
170
|
] }),
|
|
160
|
-
|
|
161
|
-
/* @__PURE__ */ o(
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
n.label ? /* @__PURE__ */ t(w, { width: "100%", children: /* @__PURE__ */ t(i, { variant: "omega", textColor: "neutral800", ellipsis: !0, children: n.label }) }) : null,
|
|
172
|
+
/* @__PURE__ */ o(i, { variant: "pi", textColor: "neutral600", children: [
|
|
173
|
+
se(n.createdAt),
|
|
174
|
+
n.user ? ` · ${n.user.name}` : "",
|
|
175
|
+
n.pinned ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "primary600", fontWeight: "bold", children: " · Pinned" }) : null
|
|
164
176
|
] }),
|
|
165
|
-
/* @__PURE__ */ t(
|
|
177
|
+
/* @__PURE__ */ t(X, { onClick: () => q(n), children: "What changed" })
|
|
166
178
|
] }),
|
|
167
|
-
/* @__PURE__ */
|
|
179
|
+
/* @__PURE__ */ o(u, { shrink: 0, gap: 1, alignItems: "center", children: [
|
|
180
|
+
/* @__PURE__ */ t(
|
|
181
|
+
Y,
|
|
182
|
+
{
|
|
183
|
+
size: "S",
|
|
184
|
+
variant: "ghost",
|
|
185
|
+
label: n.pinned ? "Unpin this version, so tidying up can remove it" : "Pin this version, so tidying up never removes it",
|
|
186
|
+
onClick: () => K(n),
|
|
187
|
+
children: /* @__PURE__ */ t(te, { fill: n.pinned ? "primary600" : "neutral500" })
|
|
188
|
+
}
|
|
189
|
+
),
|
|
190
|
+
/* @__PURE__ */ t(y, { size: "S", variant: "tertiary", onClick: () => V(n), children: "Restore" })
|
|
191
|
+
] })
|
|
168
192
|
] }),
|
|
169
|
-
/* @__PURE__ */ t(
|
|
170
|
-
] },
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
193
|
+
/* @__PURE__ */ t(w, { paddingTop: 2, children: /* @__PURE__ */ t(ee, {}) })
|
|
194
|
+
] }, n.id)),
|
|
195
|
+
f.length < M ? /* @__PURE__ */ t(y, { size: "S", variant: "tertiary", loading: D, onClick: () => I(H + 1), children: "Show older versions" }) : null,
|
|
196
|
+
x ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "danger600", children: x }) : null,
|
|
197
|
+
z ? /* @__PURE__ */ t(
|
|
198
|
+
oe,
|
|
175
199
|
{
|
|
176
|
-
diff:
|
|
177
|
-
loading:
|
|
178
|
-
onClose: () =>
|
|
200
|
+
diff: G,
|
|
201
|
+
loading: U,
|
|
202
|
+
onClose: () => $(!1)
|
|
179
203
|
}
|
|
180
204
|
) : null,
|
|
181
|
-
/* @__PURE__ */ t(
|
|
182
|
-
/* @__PURE__ */ t(
|
|
183
|
-
/* @__PURE__ */ t(
|
|
184
|
-
/* @__PURE__ */ t(
|
|
185
|
-
l?.preview.crossLocaleFields.length ? /* @__PURE__ */ o(
|
|
205
|
+
/* @__PURE__ */ t(m.Root, { open: l !== null, onOpenChange: () => S(null), children: /* @__PURE__ */ o(m.Content, { children: [
|
|
206
|
+
/* @__PURE__ */ t(m.Header, { children: "Restore this version?" }),
|
|
207
|
+
/* @__PURE__ */ t(m.Body, { children: /* @__PURE__ */ o(u, { direction: "column", alignItems: "flex-start", gap: 2, children: [
|
|
208
|
+
/* @__PURE__ */ t(i, { children: "The current draft is saved as a version first, so this can be undone. The published version is not touched." }),
|
|
209
|
+
l?.preview.crossLocaleFields.length ? /* @__PURE__ */ o(i, { textColor: "danger600", children: [
|
|
186
210
|
l.preview.crossLocaleFields.join(", "),
|
|
187
211
|
" ",
|
|
188
212
|
l.preview.crossLocaleFields.length === 1 ? "is" : "are",
|
|
@@ -197,58 +221,58 @@ const Q = (e, n, a) => {
|
|
|
197
221
|
l.preview.affectedLocales.join(", "),
|
|
198
222
|
"."
|
|
199
223
|
] }) : null,
|
|
200
|
-
l?.preview.fieldsKeptAsIs.length ? /* @__PURE__ */ o(
|
|
224
|
+
l?.preview.fieldsKeptAsIs.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "neutral600", children: [
|
|
201
225
|
"Added since this version and left untouched:",
|
|
202
226
|
" ",
|
|
203
227
|
l.preview.fieldsKeptAsIs.join(", "),
|
|
204
228
|
"."
|
|
205
229
|
] }) : null,
|
|
206
|
-
l?.preview.fieldsDropped.length ? /* @__PURE__ */ o(
|
|
230
|
+
l?.preview.fieldsDropped.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "neutral600", children: [
|
|
207
231
|
"No longer in the model and skipped: ",
|
|
208
232
|
l.preview.fieldsDropped.join(", "),
|
|
209
233
|
"."
|
|
210
234
|
] }) : null,
|
|
211
|
-
l?.preview.brokenRelations.length || l?.preview.missingMedia.length ? /* @__PURE__ */ o(
|
|
235
|
+
l?.preview.brokenRelations.length || l?.preview.missingMedia.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "warning600", children: [
|
|
212
236
|
(l.preview.brokenRelations.length ?? 0) + (l.preview.missingMedia.length ?? 0),
|
|
213
237
|
" ",
|
|
214
238
|
"linked item(s) no longer exist and will be left out."
|
|
215
239
|
] }) : null
|
|
216
240
|
] }) }),
|
|
217
|
-
/* @__PURE__ */ o(
|
|
218
|
-
/* @__PURE__ */ t(
|
|
219
|
-
/* @__PURE__ */ t(
|
|
241
|
+
/* @__PURE__ */ o(m.Footer, { children: [
|
|
242
|
+
/* @__PURE__ */ t(m.Cancel, { children: /* @__PURE__ */ t(y, { variant: "tertiary", disabled: L, children: "Cancel" }) }),
|
|
243
|
+
/* @__PURE__ */ t(y, { onClick: J, loading: L, children: "Restore" })
|
|
220
244
|
] })
|
|
221
245
|
] }) })
|
|
222
246
|
] });
|
|
223
|
-
},
|
|
247
|
+
}, ce = ({
|
|
224
248
|
model: e,
|
|
225
|
-
documentId:
|
|
249
|
+
documentId: r,
|
|
226
250
|
document: a
|
|
227
|
-
}) =>
|
|
251
|
+
}) => r ? {
|
|
228
252
|
title: "History",
|
|
229
|
-
content: /* @__PURE__ */ t(
|
|
230
|
-
} : null,
|
|
253
|
+
content: /* @__PURE__ */ t(de, { model: e, documentId: r, updatedAt: a?.updatedAt })
|
|
254
|
+
} : null, fe = {
|
|
231
255
|
register(e) {
|
|
232
|
-
e.registerPlugin({ id:
|
|
256
|
+
e.registerPlugin({ id: p, name: p, isReady: !0 });
|
|
233
257
|
},
|
|
234
258
|
bootstrap(e) {
|
|
235
|
-
e.getPlugin("content-manager").apis.addEditViewSidePanel([
|
|
259
|
+
e.getPlugin("content-manager").apis.addEditViewSidePanel([ce]);
|
|
236
260
|
},
|
|
237
261
|
async registerTrads({ locales: e }) {
|
|
238
262
|
return Promise.all(
|
|
239
|
-
e.map(async (
|
|
263
|
+
e.map(async (r) => {
|
|
240
264
|
try {
|
|
241
|
-
const { default: a } = await
|
|
242
|
-
return { data: a, locale:
|
|
265
|
+
const { default: a } = await ne(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-DCctmu-a.mjs") }), `./translations/${r}.json`, 3);
|
|
266
|
+
return { data: a, locale: r };
|
|
243
267
|
} catch {
|
|
244
|
-
return { data: {}, locale:
|
|
268
|
+
return { data: {}, locale: r };
|
|
245
269
|
}
|
|
246
270
|
})
|
|
247
271
|
);
|
|
248
272
|
}
|
|
249
273
|
};
|
|
250
274
|
export {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
275
|
+
p as PLUGIN_ID,
|
|
276
|
+
fe as default,
|
|
277
|
+
me as registerDiffRenderer
|
|
254
278
|
};
|
|
@@ -42,7 +42,7 @@ export type DiffRenderer = React.ComponentType<{
|
|
|
42
42
|
* Keyed by the attribute's `type`, or by a custom field's uid.
|
|
43
43
|
*
|
|
44
44
|
* `MyBlocksDiff` above is the caller's own component. No @qkix package exports
|
|
45
|
-
* one today
|
|
45
|
+
* one today - this is the hook for writing it, not a wiring instruction for
|
|
46
46
|
* something that already exists. Without a renderer registered, the field falls
|
|
47
47
|
* back to the generic text diff.
|
|
48
48
|
*/
|