@lincyaw/workbench 0.1.0 → 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -8
- package/package.json +27 -22
- package/src/MediaClipEditor.tsx +122 -0
- package/src/StepProductionEditor.tsx +130 -0
- package/src/approvals.tsx +60 -0
- package/src/assistant-mascot.ts +62 -0
- package/src/audio-recording-messages.ts +44 -0
- package/src/audio-recording.ts +192 -0
- package/src/audio-wav.ts +25 -0
- package/src/audio-waveform.ts +110 -0
- package/src/audition.ts +84 -0
- package/src/authoring-client.ts +136 -0
- package/src/authoring-draft-fragment.ts +28 -0
- package/src/base-controls.tsx +47 -0
- package/src/caption-timing-drafts.ts +61 -0
- package/src/caption-timing-messages.ts +14 -0
- package/src/caption-timing.css +20 -0
- package/src/caption-timing.ts +14 -0
- package/src/component-draft-actions.ts +54 -0
- package/src/component-draft-inspection.ts +35 -0
- package/src/component-properties.css +24 -0
- package/src/component-properties.ts +45 -0
- package/src/component-property-drafts.ts +273 -0
- package/src/component-property-messages.ts +31 -0
- package/src/components.tsx +547 -90
- package/src/context.tsx +27 -0
- package/src/diagnostics.ts +113 -0
- package/src/document-draft-store.ts +179 -0
- package/src/document-drafts.ts +178 -0
- package/src/document-editor/BlockNoteSurface.tsx +209 -0
- package/src/document-editor/ComponentMaterials.tsx +62 -0
- package/src/document-editor/ComponentPropertiesDialog.tsx +282 -0
- package/src/document-editor/DocumentBlockFallback.tsx +31 -0
- package/src/document-editor/DocumentBlockPreview.tsx +239 -0
- package/src/document-editor/DocumentComments.tsx +282 -0
- package/src/document-editor/DocumentEditor.tsx +192 -0
- package/src/document-editor/DocumentStepNarration.tsx +93 -0
- package/src/document-editor/NarrationEditor.tsx +110 -0
- package/src/document-editor/NewCommentDrafts.tsx +224 -0
- package/src/document-editor/authoring.tsx +259 -0
- package/src/document-editor/block-playback.ts +90 -0
- package/src/document-editor/block-preview-messages.ts +13 -0
- package/src/document-editor/blocknote-messages.ts +98 -0
- package/src/document-editor/clipboard.ts +40 -0
- package/src/document-editor/colors.ts +57 -0
- package/src/document-editor/comment-draft-actions.ts +58 -0
- package/src/document-editor/comment-draft-inspection.ts +38 -0
- package/src/document-editor/comment-drafts.ts +361 -0
- package/src/document-editor/comments.ts +410 -0
- package/src/document-editor/file-input.ts +71 -0
- package/src/document-editor/focus.ts +54 -0
- package/src/document-editor/inline.ts +104 -0
- package/src/document-editor/math.tsx +63 -0
- package/src/document-editor/messages.ts +14 -0
- package/src/document-editor/narration-schema.tsx +85 -0
- package/src/document-editor/narration.ts +37 -0
- package/src/document-editor/popover.tsx +107 -0
- package/src/document-editor/projection.ts +423 -0
- package/src/document-editor/rich-text.ts +152 -0
- package/src/document-editor/schema.tsx +117 -0
- package/src/document-editor/selection.ts +152 -0
- package/src/document-editor/style.css +9 -0
- package/src/document-editor/styles.d.ts +1 -0
- package/src/document-editor/table-identities.ts +67 -0
- package/src/document-preview.ts +112 -0
- package/src/document-return-point.ts +59 -0
- package/src/document-session.ts +678 -0
- package/src/document-timing-drafts.ts +61 -0
- package/src/document-voices.ts +2 -0
- package/src/history.ts +167 -0
- package/src/index.ts +33 -43
- package/src/jobs.ts +111 -0
- package/src/mascot-preference.ts +37 -0
- package/src/media-drafts.ts +83 -0
- package/src/media-messages.ts +53 -0
- package/src/media-time.ts +12 -0
- package/src/messages.ts +751 -203
- package/src/outline.ts +97 -0
- package/src/panels/ChangeTrail.tsx +79 -0
- package/src/panels/DocumentNarrationPanel.tsx +27 -0
- package/src/panels/DocumentView.tsx +456 -44
- package/src/panels/DocumentVoiceDialog.tsx +179 -0
- package/src/panels/DocumentVoiceField.tsx +35 -0
- package/src/panels/ExportPanel.tsx +133 -0
- package/src/panels/HistoryPanel.tsx +88 -0
- package/src/panels/HomePanel.tsx +184 -0
- package/src/panels/PageDeleteDialog.tsx +33 -0
- package/src/panels/PageRenameDialog.tsx +27 -0
- package/src/panels/PreviewPanel.tsx +79 -0
- package/src/panels/ProductionCaptionTiming.tsx +122 -0
- package/src/panels/ProductionMediaClips.tsx +156 -0
- package/src/panels/ProductionPanel.tsx +272 -0
- package/src/panels/ProductionRecording.tsx +107 -0
- package/src/panels/ProductionRecordingRange.tsx +118 -0
- package/src/panels/ProductionTimeline.tsx +62 -0
- package/src/panels/ProductionTranscription.tsx +140 -0
- package/src/panels/SourceDocumentPanel.tsx +215 -0
- package/src/panels/SpeechSettingsPanel.tsx +64 -0
- package/src/panels/TranscriptionAdoption.tsx +147 -0
- package/src/panels/VoiceCloneDialog.tsx +152 -0
- package/src/panels/VoicePreviewDialog.tsx +74 -0
- package/src/panels/VoiceScopeField.tsx +30 -0
- package/src/panels/VoiceoverDialog.tsx +216 -0
- package/src/places.ts +74 -6
- package/src/preview-playback.ts +125 -0
- package/src/preview-pool.ts +320 -0
- package/src/preview.ts +57 -6
- package/src/production-messages.ts +151 -0
- package/src/production-playback.ts +109 -0
- package/src/production-voice-playback.ts +114 -0
- package/src/recording-adoption.ts +33 -0
- package/src/schema-form/PmSchemaForm.tsx +187 -0
- package/src/schema-form/context.ts +26 -0
- package/src/schema-form/fields.tsx +59 -0
- package/src/schema-form/templates.tsx +50 -0
- package/src/schema-form/validator.ts +36 -0
- package/src/schema-form/widgets.tsx +60 -0
- package/src/shortcuts.ts +8 -0
- package/src/sortable-list.tsx +201 -0
- package/src/speech-settings-messages.ts +40 -0
- package/src/step-production.css +5 -0
- package/src/step-production.ts +51 -0
- package/src/styles.css +757 -361
- package/src/telemetry.ts +152 -0
- package/src/transcription-messages.ts +26 -0
- package/src/transcription-session.ts +159 -0
- package/src/use-recording-adoption.ts +84 -0
- package/src/voice-clone-messages.ts +92 -0
- package/src/voice-drafts.ts +89 -0
- package/src/voice-messages.ts +146 -0
- package/src/voice-snapshot.ts +55 -0
- package/src/voiceover-jobs.ts +87 -0
- package/src/workbench.ts +2328 -0
- package/src/workspace-client.ts +480 -0
- package/src/api.ts +0 -126
- package/src/editor.ts +0 -223
- package/src/merge.ts +0 -30
- package/src/panels/Changes.tsx +0 -128
- package/src/panels/Files.tsx +0 -73
- package/src/panels/Home.tsx +0 -224
- package/src/panels/Preview.tsx +0 -68
- package/src/panels/Problems.tsx +0 -42
- package/src/panels/Source.tsx +0 -118
- package/src/panels/Status.tsx +0 -26
- package/src/react.tsx +0 -59
- package/src/service.ts +0 -82
- package/src/store.ts +0 -599
package/src/panels/Home.tsx
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { Command } from 'cmdk'
|
|
2
|
-
import { useEffect, useRef, useState } from 'react'
|
|
3
|
-
import type { DocumentOperation, WorkspaceDocument } from '../api.ts'
|
|
4
|
-
import { PmBadge, PmButton, PmCard, PmDialog, PmEmpty, PmField, PmIcon, PmMenu, PmNotice } from '../components.tsx'
|
|
5
|
-
import { useMessages, useWorkbench, useWorkbenchStore } from '../react.tsx'
|
|
6
|
-
|
|
7
|
-
type Editing =
|
|
8
|
-
| { op: 'create' }
|
|
9
|
-
| { op: 'rename' | 'copy' | 'delete', doc: WorkspaceDocument }
|
|
10
|
-
|
|
11
|
-
function freeDir(dir: string, taken: string[]): string {
|
|
12
|
-
for (let n = 2; ; n++) {
|
|
13
|
-
const candidate = `${dir}-${n}`
|
|
14
|
-
if (!taken.includes(candidate)) return candidate
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function HomePanel() {
|
|
19
|
-
const t = useMessages()
|
|
20
|
-
const workbench = useWorkbenchStore()
|
|
21
|
-
const answer = useWorkbench(state => state.documents)
|
|
22
|
-
const failure = useWorkbench(state => state.failure)
|
|
23
|
-
const noRoutes = useWorkbench(state => state.noRoutes)
|
|
24
|
-
const [editing, setEditing] = useState<Editing>()
|
|
25
|
-
const [done, setDone] = useState<string>()
|
|
26
|
-
const [error, setError] = useState<string>()
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
void workbench.loadDocuments()
|
|
29
|
-
}, [workbench])
|
|
30
|
-
const documents = answer?.documents ?? []
|
|
31
|
-
const operable = Boolean(answer?.git) && !answer?.single
|
|
32
|
-
|
|
33
|
-
const run = async (operation: DocumentOperation, said: (dir: string) => string) => {
|
|
34
|
-
setError(undefined)
|
|
35
|
-
try {
|
|
36
|
-
const result = await workbench.operate(operation)
|
|
37
|
-
setDone(said(result.dir ?? operation.dir))
|
|
38
|
-
setEditing(undefined)
|
|
39
|
-
} catch (failed) {
|
|
40
|
-
setError(failed instanceof Error ? failed.message : String(failed))
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<section className="pmw-home" aria-labelledby="pmw-home-title">
|
|
46
|
-
<header className="pmw-home__bar">
|
|
47
|
-
<h1 id="pmw-home-title" className="pmw-home__title">{answer?.title ?? t.workspace}</h1>
|
|
48
|
-
<Search documents={documents} onCreate={() => setEditing({ op: 'create' })} />
|
|
49
|
-
{operable && <PmButton variant="primary" icon="plus" onClick={() => setEditing({ op: 'create' })}>{t.newDocument}</PmButton>}
|
|
50
|
-
</header>
|
|
51
|
-
{failure && <PmNotice tone="danger">{noRoutes ? t.noRoutes : failure}</PmNotice>}
|
|
52
|
-
{done && <PmNotice tone="ok">{done}</PmNotice>}
|
|
53
|
-
{answer && !answer.git && !answer.single && <PmNotice tone="warn">{t.notGit}</PmNotice>}
|
|
54
|
-
<h2 className="pmw-home__heading">{t.documents}</h2>
|
|
55
|
-
{answer && documents.length === 0
|
|
56
|
-
? <PmEmpty icon="document" title={t.noDocuments} />
|
|
57
|
-
: (
|
|
58
|
-
<ul className="pmw-home__cards" aria-label={t.documents}>
|
|
59
|
-
{documents.map(doc => (
|
|
60
|
-
<li key={doc.dir} className="pmw-home__card" data-document={doc.dir}>
|
|
61
|
-
<PmCard variant="interactive">
|
|
62
|
-
<button type="button" className="pmw-home__open" onClick={() => void workbench.openDocument(doc.dir)}>
|
|
63
|
-
<span className="pmw-home__doc-title">{doc.title}</span>
|
|
64
|
-
<span className="pmw-home__doc-dir">{doc.dir || '.'}</span>
|
|
65
|
-
</button>
|
|
66
|
-
<div className="pmw-home__meta">
|
|
67
|
-
{doc.lang && <PmBadge tone="outline">{doc.lang}</PmBadge>}
|
|
68
|
-
{operable && (
|
|
69
|
-
<PmMenu
|
|
70
|
-
label={t.more(doc.title)}
|
|
71
|
-
items={[{ id: 'rename', label: t.rename, icon: 'edit' }, { id: 'copy', label: t.copy, icon: 'copy' }, { id: 'delete', label: t.remove, icon: 'trash', danger: true }]}
|
|
72
|
-
onSelect={id => setEditing({ op: id as 'rename' | 'copy' | 'delete', doc })}
|
|
73
|
-
/>
|
|
74
|
-
)}
|
|
75
|
-
</div>
|
|
76
|
-
</PmCard>
|
|
77
|
-
</li>
|
|
78
|
-
))}
|
|
79
|
-
</ul>
|
|
80
|
-
)}
|
|
81
|
-
<McpCard />
|
|
82
|
-
{editing && (
|
|
83
|
-
<OperationDialog
|
|
84
|
-
editing={editing}
|
|
85
|
-
templates={answer?.templates ?? []}
|
|
86
|
-
taken={documents.map(doc => doc.dir)}
|
|
87
|
-
error={error}
|
|
88
|
-
onClose={() => {
|
|
89
|
-
setEditing(undefined)
|
|
90
|
-
setError(undefined)
|
|
91
|
-
}}
|
|
92
|
-
onRun={(operation) => {
|
|
93
|
-
const said = { create: t.created, rename: t.renamed, copy: t.copied, delete: t.removed }[operation.op]
|
|
94
|
-
void run(operation, said)
|
|
95
|
-
}}
|
|
96
|
-
/>
|
|
97
|
-
)}
|
|
98
|
-
</section>
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function OperationDialog({ editing, templates, taken, error, onClose, onRun }: { editing: Editing, templates: string[], taken: string[], error?: string, onClose(): void, onRun(operation: DocumentOperation): void }) {
|
|
103
|
-
const t = useMessages()
|
|
104
|
-
const [dir, setDir] = useState(editing.op === 'create' ? '' : editing.op === 'copy' ? freeDir(editing.doc.dir, taken) : editing.op === 'rename' ? editing.doc.dir : '')
|
|
105
|
-
const [title, setTitle] = useState('')
|
|
106
|
-
const [template, setTemplate] = useState(templates.includes('default') ? 'default' : templates[0] ?? '')
|
|
107
|
-
const label = editing.op === 'create' ? t.newDocument : `${{ rename: t.rename, copy: t.copy, delete: t.remove }[editing.op]} · ${editing.doc.title}`
|
|
108
|
-
const submit = () => {
|
|
109
|
-
if (editing.op === 'create') onRun({ op: 'create', dir: dir.trim(), ...title.trim() ? { title: title.trim() } : {}, ...template ? { template } : {} })
|
|
110
|
-
else if (editing.op === 'delete') onRun({ op: 'delete', dir: editing.doc.dir })
|
|
111
|
-
else onRun({ op: editing.op, dir: editing.doc.dir, to: dir.trim() })
|
|
112
|
-
}
|
|
113
|
-
return (
|
|
114
|
-
<PmDialog open label={label} closeLabel={t.close} name="pmw-operation" onClose={onClose}>
|
|
115
|
-
<form
|
|
116
|
-
className="pmw-operation"
|
|
117
|
-
onSubmit={(event) => {
|
|
118
|
-
event.preventDefault()
|
|
119
|
-
submit()
|
|
120
|
-
}}
|
|
121
|
-
>
|
|
122
|
-
{editing.op === 'delete'
|
|
123
|
-
? <p>{t.confirmRemove(editing.doc.title)}</p>
|
|
124
|
-
: (
|
|
125
|
-
<PmField label={editing.op === 'create' ? t.dir : editing.op === 'rename' ? t.renameTo : t.copyTo}>
|
|
126
|
-
<input className="pm-ui-input" name="dir" value={dir} required onChange={event => setDir(event.target.value)} />
|
|
127
|
-
</PmField>
|
|
128
|
-
)}
|
|
129
|
-
{editing.op === 'create' && (
|
|
130
|
-
<>
|
|
131
|
-
<PmField label={t.title}>
|
|
132
|
-
<input className="pm-ui-input" name="title" value={title} onChange={event => setTitle(event.target.value)} />
|
|
133
|
-
</PmField>
|
|
134
|
-
{templates.length > 1 && (
|
|
135
|
-
<PmField label={t.template}>
|
|
136
|
-
<select className="pm-ui-select" name="template" value={template} onChange={event => setTemplate(event.target.value)}>
|
|
137
|
-
{templates.map(name => <option key={name} value={name}>{name}</option>)}
|
|
138
|
-
</select>
|
|
139
|
-
</PmField>
|
|
140
|
-
)}
|
|
141
|
-
</>
|
|
142
|
-
)}
|
|
143
|
-
{error && <PmNotice tone="danger">{error}</PmNotice>}
|
|
144
|
-
<div className="pmw-operation__actions">
|
|
145
|
-
<PmButton onClick={onClose}>{t.cancel}</PmButton>
|
|
146
|
-
<PmButton type="submit" variant={editing.op === 'delete' ? 'danger' : 'primary'}>
|
|
147
|
-
{editing.op === 'create' ? t.create : { rename: t.rename, copy: t.copy, delete: t.remove }[editing.op]}
|
|
148
|
-
</PmButton>
|
|
149
|
-
</div>
|
|
150
|
-
</form>
|
|
151
|
-
</PmDialog>
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function Search({ documents, onCreate }: { documents: WorkspaceDocument[], onCreate(): void }) {
|
|
156
|
-
const t = useMessages()
|
|
157
|
-
const workbench = useWorkbenchStore()
|
|
158
|
-
const [query, setQuery] = useState('')
|
|
159
|
-
const input = useRef<HTMLInputElement>(null)
|
|
160
|
-
useEffect(() => {
|
|
161
|
-
const onKey = (event: KeyboardEvent) => {
|
|
162
|
-
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
|
|
163
|
-
event.preventDefault()
|
|
164
|
-
input.current?.focus()
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
window.addEventListener('keydown', onKey)
|
|
168
|
-
return () => window.removeEventListener('keydown', onKey)
|
|
169
|
-
}, [])
|
|
170
|
-
return (
|
|
171
|
-
<Command className="pmw-search" label={t.search}>
|
|
172
|
-
<label className="pm-ui-search">
|
|
173
|
-
<PmIcon name="search" size={15} strokeWidth={1.8} />
|
|
174
|
-
<Command.Input ref={input} value={query} aria-label={t.search} placeholder={`${t.search}(${t.searchHint})`} onValueChange={setQuery} onKeyDown={event => { if (event.key === 'Escape') setQuery('') }} />
|
|
175
|
-
</label>
|
|
176
|
-
{query.trim() && (
|
|
177
|
-
<Command.List className="pmw-search__results" aria-label={t.search}>
|
|
178
|
-
<Command.Empty className="pmw-muted">{t.noMatch}</Command.Empty>
|
|
179
|
-
<Command.Group heading={t.documents}>
|
|
180
|
-
{documents.map(doc => (
|
|
181
|
-
<Command.Item key={doc.dir} value={`${doc.title} ${doc.dir}`} className="pmw-search__item" onSelect={() => { setQuery(''); void workbench.openDocument(doc.dir) }}>
|
|
182
|
-
<PmIcon name="document" size={15} />{doc.title}<span className="pmw-muted">{doc.dir}</span>
|
|
183
|
-
</Command.Item>
|
|
184
|
-
))}
|
|
185
|
-
</Command.Group>
|
|
186
|
-
<Command.Group heading={t.commands}>
|
|
187
|
-
<Command.Item value={t.newDocument} className="pmw-search__item" onSelect={() => { setQuery(''); onCreate() }}><PmIcon name="plus" size={15} />{t.newDocument}</Command.Item>
|
|
188
|
-
</Command.Group>
|
|
189
|
-
</Command.List>
|
|
190
|
-
)}
|
|
191
|
-
</Command>
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function McpCard() {
|
|
196
|
-
const t = useMessages()
|
|
197
|
-
const workbench = useWorkbenchStore()
|
|
198
|
-
const [address, setAddress] = useState<string>()
|
|
199
|
-
const [copied, setCopied] = useState(false)
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
workbench.client.workbench().then(answer => setAddress(answer.mcp), () => {})
|
|
202
|
-
}, [workbench])
|
|
203
|
-
if (!address) return null
|
|
204
|
-
return (
|
|
205
|
-
<PmCard className="pmw-home__mcp">
|
|
206
|
-
<h2 className="pmw-home__heading">{t.mcp}</h2>
|
|
207
|
-
<div className="pmw-home__address">
|
|
208
|
-
<code>{address}</code>
|
|
209
|
-
<PmButton
|
|
210
|
-
small
|
|
211
|
-
icon={copied ? 'check' : 'copy'}
|
|
212
|
-
onClick={() => {
|
|
213
|
-
void navigator.clipboard?.writeText(address).catch(() => {})
|
|
214
|
-
setCopied(true)
|
|
215
|
-
setTimeout(() => setCopied(false), 2000)
|
|
216
|
-
}}
|
|
217
|
-
>
|
|
218
|
-
{copied ? t.copiedAddress : t.copyAddress}
|
|
219
|
-
</PmButton>
|
|
220
|
-
</div>
|
|
221
|
-
<p className="pmw-muted">{t.mcpHint}</p>
|
|
222
|
-
</PmCard>
|
|
223
|
-
)
|
|
224
|
-
}
|
package/src/panels/Preview.tsx
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { connectPreview } from '../preview.ts'
|
|
3
|
-
import { PmButton, PmIconButton, PmNotice, PmSegmented } from '../components.tsx'
|
|
4
|
-
import { useMessages, useWorkbench, useWorkbenchStore } from '../react.tsx'
|
|
5
|
-
import type { PreviewView } from '../store.ts'
|
|
6
|
-
|
|
7
|
-
const READY_MS = 45_000
|
|
8
|
-
const RELOADS = 3
|
|
9
|
-
|
|
10
|
-
export function PreviewPanel() {
|
|
11
|
-
const t = useMessages()
|
|
12
|
-
const workbench = useWorkbenchStore()
|
|
13
|
-
const document = useWorkbench(state => state.document)
|
|
14
|
-
const documents = useWorkbench(state => state.documents)
|
|
15
|
-
const preview = useWorkbench(state => state.preview)
|
|
16
|
-
const failure = useWorkbench(state => state.previewFailure)
|
|
17
|
-
const view = useWorkbench(state => state.view)
|
|
18
|
-
const picking = useWorkbench(state => state.picking)
|
|
19
|
-
const frame = useRef<HTMLIFrameElement>(null)
|
|
20
|
-
const [loads, setLoads] = useState(0)
|
|
21
|
-
const [ready, setReady] = useState(false)
|
|
22
|
-
const [mismatch, setMismatch] = useState(false)
|
|
23
|
-
const address = view === 'slides' ? preview?.slides : preview?.web
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (!address || !frame.current) return
|
|
27
|
-
setReady(false)
|
|
28
|
-
const connection = connectPreview(() => frame.current, address, {
|
|
29
|
-
report(report) {
|
|
30
|
-
if (report.type === 'ready') setReady(true)
|
|
31
|
-
workbench.previewReport(report)
|
|
32
|
-
},
|
|
33
|
-
mismatch: () => setMismatch(true),
|
|
34
|
-
})
|
|
35
|
-
frame.current.src = connection.src
|
|
36
|
-
const detach = workbench.attachPreview(connection)
|
|
37
|
-
let reloads = 0
|
|
38
|
-
const timer = setInterval(() => {
|
|
39
|
-
if (connection.ready || reloads >= RELOADS || !frame.current) return
|
|
40
|
-
reloads++
|
|
41
|
-
frame.current.src = connection.src
|
|
42
|
-
}, READY_MS)
|
|
43
|
-
return () => {
|
|
44
|
-
clearInterval(timer)
|
|
45
|
-
detach()
|
|
46
|
-
connection.dispose()
|
|
47
|
-
}
|
|
48
|
-
}, [address, workbench, loads])
|
|
49
|
-
|
|
50
|
-
if (document === undefined) return null
|
|
51
|
-
const title = documents?.documents.find(doc => doc.dir === document)?.title ?? document
|
|
52
|
-
const views: { value: PreviewView, label: string }[] = [{ value: 'web', label: t.views.web }, ...preview?.slides ? [{ value: 'slides' as const, label: t.views.slides }] : []]
|
|
53
|
-
return (
|
|
54
|
-
<section className="pmw-preview" aria-label={t.preview} data-ready={ready || undefined}>
|
|
55
|
-
<div className="pmw-preview__bar">
|
|
56
|
-
{views.length > 1 && <PmSegmented value={view} options={views} label={t.view} onChange={value => workbench.setView(value)} />}
|
|
57
|
-
<span className="pmw-spacer" />
|
|
58
|
-
<PmButton small icon="edit" variant={picking ? 'accent' : 'ghost'} aria-pressed={picking} title={t.pickHint} onClick={() => workbench.setPicking(!picking)}>{t.pick}</PmButton>
|
|
59
|
-
<PmIconButton icon="redo" label={t.reloadPreview} small onClick={() => (preview ? setLoads(n => n + 1) : workbench.retryPreview())} />
|
|
60
|
-
</div>
|
|
61
|
-
{mismatch && <PmNotice tone="warn">{t.versionMismatch}</PmNotice>}
|
|
62
|
-
{failure && <PmNotice tone="danger">{t.previewFailed(failure)}</PmNotice>}
|
|
63
|
-
{!preview && !failure && <p className="pmw-muted pmw-preview__loading">{t.previewLoading}</p>}
|
|
64
|
-
{view === 'slides' && preview && !preview.slides && <p className="pmw-muted">{t.noSlides}</p>}
|
|
65
|
-
<iframe ref={frame} className="pmw-preview__frame" title={t.previewTitle(title)} hidden={!address} />
|
|
66
|
-
</section>
|
|
67
|
-
)
|
|
68
|
-
}
|
package/src/panels/Problems.tsx
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { PmIcon } from '../components.tsx'
|
|
2
|
-
import { useMessages, useWorkbench, useWorkbenchStore } from '../react.tsx'
|
|
3
|
-
|
|
4
|
-
export function ProblemsPanel() {
|
|
5
|
-
const t = useMessages()
|
|
6
|
-
const workbench = useWorkbenchStore()
|
|
7
|
-
const ir = useWorkbench(state => state.ir)
|
|
8
|
-
const issues = useWorkbench(state => state.issues)
|
|
9
|
-
const findings = ir?.diagnostics ?? []
|
|
10
|
-
const count = findings.length + issues.length
|
|
11
|
-
return (
|
|
12
|
-
<section className="pmw-problems" aria-label={t.problems}>
|
|
13
|
-
<h2 className="pmw-problems__head">
|
|
14
|
-
{t.problems}
|
|
15
|
-
<span className="pm-ui-badge" data-count={count}>{count}</span>
|
|
16
|
-
</h2>
|
|
17
|
-
{ir && count === 0 && <p className="pmw-muted pmw-problems__none">{t.noProblems}</p>}
|
|
18
|
-
<ul className="pmw-problems__list">
|
|
19
|
-
{findings.map((finding, i) => (
|
|
20
|
-
<li key={`f${i}`}>
|
|
21
|
-
<button type="button" className="pmw-problem" data-severity={finding.severity} disabled={!finding.source?.file} onClick={() => void workbench.revealProblem(finding)}>
|
|
22
|
-
<PmIcon name={finding.severity === 'error' ? 'error' : finding.severity === 'warning' ? 'warning' : 'info'} size={15} />
|
|
23
|
-
<span className="pmw-problem__severity">{t.severity[finding.severity] ?? finding.severity}</span>
|
|
24
|
-
<span className="pmw-problem__where">{finding.source ? `${finding.source.file}${finding.source.line ? `:${finding.source.line}` : ''}` : ''}</span>
|
|
25
|
-
<span className="pmw-problem__message">{finding.message}{finding.hint ? ` ${finding.hint}` : ''}</span>
|
|
26
|
-
</button>
|
|
27
|
-
</li>
|
|
28
|
-
))}
|
|
29
|
-
{issues.map((issue, i) => (
|
|
30
|
-
<li key={`p${i}`}>
|
|
31
|
-
<div className="pmw-problem" data-severity={issue.level}>
|
|
32
|
-
<PmIcon name={issue.level === 'error' ? 'error' : 'warning'} size={15} />
|
|
33
|
-
<span className="pmw-problem__severity">{t.severity[issue.level]}</span>
|
|
34
|
-
<span className="pmw-problem__where">{t.page}{issue.plugin ? ` · ${issue.plugin}` : ''}</span>
|
|
35
|
-
<span className="pmw-problem__message">{issue.message}</span>
|
|
36
|
-
</div>
|
|
37
|
-
</li>
|
|
38
|
-
))}
|
|
39
|
-
</ul>
|
|
40
|
-
</section>
|
|
41
|
-
)
|
|
42
|
-
}
|
package/src/panels/Source.tsx
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import * as Tabs from '@radix-ui/react-tabs'
|
|
2
|
-
import { useEffect, useRef } from 'react'
|
|
3
|
-
import type { KeyboardEvent } from 'react'
|
|
4
|
-
import { createEditor } from '../editor.ts'
|
|
5
|
-
import type { Problem, SourceEditor } from '../editor.ts'
|
|
6
|
-
import { PmButton, PmIconButton, PmNotice } from '../components.tsx'
|
|
7
|
-
import { useMessages, useWorkbench, useWorkbenchStore } from '../react.tsx'
|
|
8
|
-
import type { OpenFile } from '../store.ts'
|
|
9
|
-
|
|
10
|
-
const nameOf = (path: string) => path.slice(path.lastIndexOf('/') + 1)
|
|
11
|
-
|
|
12
|
-
export function SourcePanel() {
|
|
13
|
-
const t = useMessages()
|
|
14
|
-
const workbench = useWorkbenchStore()
|
|
15
|
-
const open = useWorkbench(state => state.open)
|
|
16
|
-
const active = useWorkbench(state => state.active)
|
|
17
|
-
const file = open.find(f => f.path === active)
|
|
18
|
-
const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {
|
|
19
|
-
if (event.defaultPrevented || !(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return
|
|
20
|
-
event.preventDefault()
|
|
21
|
-
void workbench.save()
|
|
22
|
-
}
|
|
23
|
-
return (
|
|
24
|
-
<Tabs.Root asChild value={active ?? ''} onValueChange={path => workbench.showFile(path)}>
|
|
25
|
-
<section className="pmw-source" aria-label={t.source} onKeyDown={onKeyDown}>
|
|
26
|
-
<Tabs.List className="pmw-source__tabs" aria-label={t.source}>
|
|
27
|
-
{open.map(f => (
|
|
28
|
-
<span key={f.path} className="pmw-source__tab" data-active={f.path === active || undefined}>
|
|
29
|
-
<Tabs.Trigger value={f.path} title={f.path}>
|
|
30
|
-
{nameOf(f.path)}
|
|
31
|
-
{f.dirty && <span className="pmw-source__dot" aria-label={t.unsaved} role="img" />}
|
|
32
|
-
</Tabs.Trigger>
|
|
33
|
-
<PmIconButton icon="close" label={t.closeFile(nameOf(f.path))} small onClick={() => workbench.closeFile(f.path)} />
|
|
34
|
-
</span>
|
|
35
|
-
))}
|
|
36
|
-
<span className="pmw-spacer" />
|
|
37
|
-
{file && <PmButton small icon="check" variant={file.dirty ? 'primary' : 'default'} onClick={() => void workbench.save(file.path)}>{t.save}</PmButton>}
|
|
38
|
-
</Tabs.List>
|
|
39
|
-
{file?.notice && <FileNoticeBox file={file} />}
|
|
40
|
-
<Tabs.Content value={active ?? ''} forceMount className="pmw-source__body"><EditorPane /></Tabs.Content>
|
|
41
|
-
{!file && <p className="pmw-source__empty pmw-muted">{t.noFile}</p>}
|
|
42
|
-
</section>
|
|
43
|
-
</Tabs.Root>
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function FileNoticeBox({ file }: { file: OpenFile }) {
|
|
48
|
-
const t = useMessages()
|
|
49
|
-
const workbench = useWorkbenchStore()
|
|
50
|
-
const notice = file.notice!
|
|
51
|
-
switch (notice.kind) {
|
|
52
|
-
case 'changed':
|
|
53
|
-
return (
|
|
54
|
-
<PmNotice tone="warn">
|
|
55
|
-
<span>{t.changedOnDisk(nameOf(file.path))}</span>
|
|
56
|
-
<span className="pmw-notice__actions">
|
|
57
|
-
<PmButton small onClick={() => workbench.merge(file.path, { mine: t.mergeMine, theirs: t.mergeTheirs })}>{t.merge}</PmButton>
|
|
58
|
-
<PmButton small onClick={() => workbench.reload(file.path)}>{t.reload}</PmButton>
|
|
59
|
-
</span>
|
|
60
|
-
</PmNotice>
|
|
61
|
-
)
|
|
62
|
-
case 'merged': return <PmNotice tone="accent">{t.merged(notice.conflicts)}</PmNotice>
|
|
63
|
-
case 'locked': return <PmNotice tone="warn">{t.locked}</PmNotice>
|
|
64
|
-
case 'failed': return <PmNotice tone="danger">{t.saveFailed(notice.message)}</PmNotice>
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function EditorPane() {
|
|
69
|
-
const t = useMessages()
|
|
70
|
-
const workbench = useWorkbenchStore()
|
|
71
|
-
const parent = useRef<HTMLDivElement>(null)
|
|
72
|
-
const editor = useRef<SourceEditor>(undefined)
|
|
73
|
-
const active = useWorkbench(state => state.active)
|
|
74
|
-
const reveal = useWorkbench(state => state.reveal)
|
|
75
|
-
const ir = useWorkbench(state => state.ir)
|
|
76
|
-
const opened = useWorkbench(state => state.open.length)
|
|
77
|
-
const label = useRef(t.editor(''))
|
|
78
|
-
label.current = t.editor(active ?? '')
|
|
79
|
-
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
const made = createEditor(parent.current!, {
|
|
82
|
-
label: label.current,
|
|
83
|
-
save: () => void workbench.save(),
|
|
84
|
-
changed: (path, text) => workbench.edited(path, text),
|
|
85
|
-
moved: line => workbench.moved(line),
|
|
86
|
-
selected: selection => workbench.selectSource(selection),
|
|
87
|
-
})
|
|
88
|
-
editor.current = made
|
|
89
|
-
const unbind = workbench.bindEditor({ text: path => made.text(path), replace: (path, text, fresh) => made.replace(path, text, fresh), focus: () => made.focus() })
|
|
90
|
-
return () => {
|
|
91
|
-
unbind()
|
|
92
|
-
made.destroy()
|
|
93
|
-
editor.current = undefined
|
|
94
|
-
}
|
|
95
|
-
}, [workbench])
|
|
96
|
-
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
const made = editor.current
|
|
99
|
-
if (!made) return
|
|
100
|
-
for (const path of made.paths()) if (!workbench.state.open.some(file => file.path === path)) made.forget(path)
|
|
101
|
-
if (!active) return
|
|
102
|
-
made.show(active, workbench.textOf(active) ?? '')
|
|
103
|
-
}, [active, workbench, opened])
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
if (reveal && reveal.path === active) editor.current?.reveal(reveal.line, reveal.endLine, reveal.mark)
|
|
107
|
-
}, [reveal, active])
|
|
108
|
-
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
if (!active || !editor.current) return
|
|
111
|
-
const problems: Problem[] = (ir?.diagnostics ?? []).filter(finding => finding.source?.file === active && finding.source.line).map(finding => ({ line: finding.source!.line!, severity: finding.severity === 'error' ? 'error' : finding.severity === 'warning' ? 'warning' : 'info', message: finding.message }))
|
|
112
|
-
editor.current.problems(active, problems)
|
|
113
|
-
}, [ir, active])
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<div className="pmw-source__editor" ref={parent} hidden={!active} data-path={active} />
|
|
117
|
-
)
|
|
118
|
-
}
|
package/src/panels/Status.tsx
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { PmIcon } from '../components.tsx'
|
|
2
|
-
import { useMessages, useWorkbench } from '../react.tsx'
|
|
3
|
-
|
|
4
|
-
export function StatusItems() {
|
|
5
|
-
const t = useMessages()
|
|
6
|
-
const documents = useWorkbench(state => state.documents)
|
|
7
|
-
const document = useWorkbench(state => state.document)
|
|
8
|
-
const active = useWorkbench(state => state.active)
|
|
9
|
-
const line = useWorkbench(state => state.line)
|
|
10
|
-
const lang = documents?.documents.find(doc => doc.dir === document)?.lang
|
|
11
|
-
return (
|
|
12
|
-
<>
|
|
13
|
-
{documents && (
|
|
14
|
-
<span className="pmw-status__item" data-status="git">
|
|
15
|
-
<PmIcon name="branch" size={13} />
|
|
16
|
-
{documents.git ? (documents.branch ? t.branch(documents.branch) : 'HEAD') : t.noGit}
|
|
17
|
-
{documents.git && documents.uncommitted ? ` · ${t.uncommitted(documents.uncommitted)}` : ''}
|
|
18
|
-
</span>
|
|
19
|
-
)}
|
|
20
|
-
<span className="pmw-spacer" />
|
|
21
|
-
{document !== undefined && lang && <span className="pmw-status__item" data-status="lang">{t.language(lang)}</span>}
|
|
22
|
-
{active && <span className="pmw-status__item" data-status="encoding">{t.encoding}</span>}
|
|
23
|
-
{active && <span className="pmw-status__item" data-status="line">{t.line(line)}</span>}
|
|
24
|
-
</>
|
|
25
|
-
)
|
|
26
|
-
}
|
package/src/react.tsx
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useEffect, useSyncExternalStore } from 'react'
|
|
2
|
-
import type { ReactNode } from 'react'
|
|
3
|
-
import { MESSAGES } from './messages.ts'
|
|
4
|
-
import type { WorkbenchMessages } from './messages.ts'
|
|
5
|
-
import type { Workbench, WorkbenchState } from './store.ts'
|
|
6
|
-
import { PmButton, PmDialog, PmNotice } from './components.tsx'
|
|
7
|
-
|
|
8
|
-
const WORKBENCH = createContext<Workbench | undefined>(undefined)
|
|
9
|
-
|
|
10
|
-
export function WorkbenchProvider({ workbench, children, protectChanges = true }: { workbench: Workbench, children?: ReactNode, protectChanges?: boolean }) {
|
|
11
|
-
return <WORKBENCH.Provider value={workbench}>{children}{protectChanges && <UnsavedChanges />}</WORKBENCH.Provider>
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function UnsavedChanges() {
|
|
15
|
-
const workbench = useWorkbenchStore()
|
|
16
|
-
const files = useWorkbench(state => state.leaving)
|
|
17
|
-
const saving = useWorkbench(state => state.leaveSaving)
|
|
18
|
-
const failed = useWorkbench(state => state.leaveFailed)
|
|
19
|
-
const t = useMessages()
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const beforeUnload = (event: BeforeUnloadEvent) => {
|
|
22
|
-
if (!workbench.state.open.some(file => file.dirty)) return
|
|
23
|
-
event.preventDefault()
|
|
24
|
-
event.returnValue = ''
|
|
25
|
-
}
|
|
26
|
-
window.addEventListener('beforeunload', beforeUnload)
|
|
27
|
-
const refreshFiles = () => { void workbench.checkDisk() }
|
|
28
|
-
window.addEventListener('focus', refreshFiles)
|
|
29
|
-
return () => {
|
|
30
|
-
window.removeEventListener('beforeunload', beforeUnload)
|
|
31
|
-
window.removeEventListener('focus', refreshFiles)
|
|
32
|
-
}
|
|
33
|
-
}, [workbench])
|
|
34
|
-
return <PmDialog open={Boolean(files)} label={t.saveChanges} closeLabel={t.close} onClose={() => void workbench.resolveLeave('cancel')}>
|
|
35
|
-
<p>{t.unsavedFiles}</p>
|
|
36
|
-
<ul>{files?.map(path => <li key={path}>{path}</li>)}</ul>
|
|
37
|
-
{failed && <PmNotice tone="danger">{t.saveBeforeLeavingFailed}</PmNotice>}
|
|
38
|
-
<div className="pmw-notice__actions">
|
|
39
|
-
<PmButton disabled={saving} onClick={() => void workbench.resolveLeave('cancel')}>{t.cancel}</PmButton>
|
|
40
|
-
<PmButton disabled={saving} onClick={() => void workbench.resolveLeave('discard')}>{t.discardChanges}</PmButton>
|
|
41
|
-
<PmButton disabled={saving} variant="primary" onClick={() => void workbench.resolveLeave('save')}>{t.save}</PmButton>
|
|
42
|
-
</div>
|
|
43
|
-
</PmDialog>
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function useWorkbenchStore(): Workbench {
|
|
47
|
-
const workbench = useContext(WORKBENCH)
|
|
48
|
-
if (!workbench) throw new Error('a workbench panel is drawn outside WorkbenchProvider')
|
|
49
|
-
return workbench
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function useWorkbench<T>(select: (state: WorkbenchState) => T): T {
|
|
53
|
-
const workbench = useWorkbenchStore()
|
|
54
|
-
return useSyncExternalStore(workbench.subscribe, () => select(workbench.getState()))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function useMessages(): WorkbenchMessages {
|
|
58
|
-
return MESSAGES[useWorkbench(state => state.locale)]
|
|
59
|
-
}
|
package/src/service.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { FocusReport } from '@lincyaw/kernel'
|
|
2
|
-
import type { DocumentOperation, DocumentsAnswer, ErrorAnswer, FilesAnswer, FocusAnswer, HistoryRevision, IrAnswer, IrEvent, OperationAnswer, PreviewAnswer, RevertAnswer, ReviewEnd, ReviewFollowed, ReviewView, SavedAnswer, SourceAnswer, UncommittedFile, WorkbenchAnswer } from './api.ts'
|
|
3
|
-
|
|
4
|
-
export class ServiceError extends Error {
|
|
5
|
-
readonly status: number
|
|
6
|
-
readonly answer: ErrorAnswer
|
|
7
|
-
constructor(status: number, answer: ErrorAnswer) {
|
|
8
|
-
super(answer.error ?? answer.detail ?? answer.title ?? `HTTP ${status}`)
|
|
9
|
-
this.status = status
|
|
10
|
-
this.answer = answer
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface WorkspaceClient {
|
|
15
|
-
documents(document?: string): Promise<DocumentsAnswer>
|
|
16
|
-
operate(operation: DocumentOperation): Promise<OperationAnswer>
|
|
17
|
-
files(document: string): Promise<FilesAnswer>
|
|
18
|
-
source(document: string, path: string): Promise<SourceAnswer>
|
|
19
|
-
save(document: string, path: string, content: string, base: string): Promise<SavedAnswer>
|
|
20
|
-
ir(document: string): Promise<IrAnswer>
|
|
21
|
-
preview(document: string): Promise<PreviewAnswer>
|
|
22
|
-
workbench(): Promise<WorkbenchAnswer>
|
|
23
|
-
focus(document: string, focus?: FocusReport[]): Promise<FocusAnswer>
|
|
24
|
-
events(document: string, revision: (event: IrEvent) => void, failed?: () => void): () => void
|
|
25
|
-
review(document: string, followed: (event: ReviewFollowed) => void): () => void
|
|
26
|
-
accept(document: string, body: { reject?: string[], message?: string }): Promise<{ ended: ReviewEnd }>
|
|
27
|
-
reject(document: string, paths?: string[]): Promise<ReviewFollowed>
|
|
28
|
-
restore(document: string, step: string): Promise<{ review: ReviewView }>
|
|
29
|
-
changes(document: string): Promise<{ files: UncommittedFile[] }>
|
|
30
|
-
commit(document: string, message: string, paths?: string[]): Promise<unknown>
|
|
31
|
-
discard(document: string, paths?: string[]): Promise<unknown>
|
|
32
|
-
revisions(document: string): Promise<{ revisions: HistoryRevision[] }>
|
|
33
|
-
revert(document: string, id: string): Promise<RevertAnswer>
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function workspaceClient(base = 'api/presmium/'): WorkspaceClient {
|
|
37
|
-
const url = (path: string, document?: string, query: Record<string, string> = {}) => {
|
|
38
|
-
const address = new URL(`${base}${path}`, globalThis.document.baseURI)
|
|
39
|
-
if (document !== undefined) address.searchParams.set('document', document)
|
|
40
|
-
for (const [name, value] of Object.entries(query)) address.searchParams.set(name, value)
|
|
41
|
-
return address.href
|
|
42
|
-
}
|
|
43
|
-
const call = async <T>(address: string, init?: RequestInit): Promise<T> => {
|
|
44
|
-
const response = await fetch(address, { credentials: 'same-origin', ...init })
|
|
45
|
-
const data = await response.json().catch(() => ({})) as T & ErrorAnswer
|
|
46
|
-
if (!response.ok) throw new ServiceError(response.status, data)
|
|
47
|
-
return data
|
|
48
|
-
}
|
|
49
|
-
const post = <T>(address: string, body: unknown) => call<T>(address, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
50
|
-
const follow = <T>(address: string, name: string, on: (data: T) => void, failed?: () => void) => {
|
|
51
|
-
const source = new EventSource(address)
|
|
52
|
-
source.addEventListener(name, (event) => {
|
|
53
|
-
try {
|
|
54
|
-
on(JSON.parse((event as MessageEvent<string>).data) as T)
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
if (failed) source.addEventListener('error', failed)
|
|
59
|
-
return () => source.close()
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
documents: document => call(url('documents', document)),
|
|
63
|
-
operate: operation => post(url('documents'), operation),
|
|
64
|
-
files: document => call(url('files', document)),
|
|
65
|
-
source: (document, path) => call(url('source', document, { path })),
|
|
66
|
-
save: (document, path, content, base) => post(url('source', document), { path, content, base }),
|
|
67
|
-
ir: document => call(url('ir', document)),
|
|
68
|
-
preview: document => call(url('preview', document)),
|
|
69
|
-
workbench: () => call(url('workbench')),
|
|
70
|
-
focus: (document, focus) => focus === undefined ? call(url('focus', document)) : post(url('focus'), { document, focus }),
|
|
71
|
-
events: (document, revision, failed) => follow(url('ir/events', document), 'revision', revision, failed),
|
|
72
|
-
review: (document, followed) => follow(url('review', document), 'review', followed),
|
|
73
|
-
accept: (document, body) => post(url('review/accept', document), body),
|
|
74
|
-
reject: (document, paths) => post(url('review/reject', document), paths ? { paths } : {}),
|
|
75
|
-
restore: (document, step) => post(url('review/restore', document), { step }),
|
|
76
|
-
changes: document => call(url('changes', document)),
|
|
77
|
-
commit: (document, message, paths) => post(url('changes/commit', document), { message, ...paths ? { paths } : {} }),
|
|
78
|
-
discard: (document, paths) => post(url('changes/discard', document), paths ? { paths } : {}),
|
|
79
|
-
revisions: document => call(url('revisions', document, { limit: '50' })),
|
|
80
|
-
revert: (document, id) => post(url('revisions/revert', document), { id }),
|
|
81
|
-
}
|
|
82
|
-
}
|