@startsimpli/ui 0.4.142 → 0.4.144
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/package.json +38 -12
- package/src/components/blog/MarkdownRenderer.tsx +23 -5
- package/src/components/track-changes/ChangesRail.tsx +687 -0
- package/src/components/track-changes/TrackedField.tsx +293 -0
- package/src/components/track-changes/TrackedReadView.tsx +273 -0
- package/src/components/track-changes/TrackedTextEditor.tsx +595 -0
- package/src/components/track-changes/__tests__/extension.test.ts +350 -0
- package/src/components/track-changes/__tests__/model.test.ts +461 -0
- package/src/components/track-changes/__tests__/rails.test.tsx +604 -0
- package/src/components/track-changes/__tests__/tracked-text-editor.test.tsx +476 -0
- package/src/components/track-changes/describe.ts +76 -0
- package/src/components/track-changes/extension.ts +724 -0
- package/src/components/track-changes/final-text.ts +54 -0
- package/src/components/track-changes/http-client.ts +109 -0
- package/src/components/track-changes/index.ts +90 -0
- package/src/components/track-changes/normalize.ts +393 -0
- package/src/components/track-changes/read-marks.ts +262 -0
- package/src/components/track-changes/types.ts +451 -0
- package/src/components/track-changes/useTrackChanges.ts +407 -0
- package/src/components/track-changes/word-diff.ts +97 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @startsimpli/ui/track-changes — the shapes the track-changes editor surface
|
|
3
|
+
* reads and the one call it writes (bd startsim-sjuai; design startsim-xyn25).
|
|
4
|
+
*
|
|
5
|
+
* THE FIELD STAYS A PLAIN STRING. Suggestions and comments are separate server
|
|
6
|
+
* rows anchored to character offsets of that string at a known version, and
|
|
7
|
+
* blame is derived server-side from the revision chain. The editor renders all
|
|
8
|
+
* three as DECORATIONS over the stored string and never writes a mark into it.
|
|
9
|
+
*
|
|
10
|
+
* STRUCTURAL ON PURPOSE, like `history/types.ts`: packages/ui imports no API
|
|
11
|
+
* client. The consuming app injects its own authed client
|
|
12
|
+
* ({@link TrackChangesClient}). Both spellings of every key are declared
|
|
13
|
+
* because the shared API client camelCases responses recursively; the
|
|
14
|
+
* normalizers in `normalize.ts` read either.
|
|
15
|
+
*
|
|
16
|
+
* ── OFFSET UNITS ────────────────────────────────────────────────────────────
|
|
17
|
+
* The server is Python, so its string offsets count CODE POINTS. The editor is
|
|
18
|
+
* JavaScript, so its offsets count UTF-16 CODE UNITS. They differ by one for
|
|
19
|
+
* every character outside the Basic Multilingual Plane before the offset, and
|
|
20
|
+
* LinkedIn posts are full of emoji. `offset_unit` on a response says which unit
|
|
21
|
+
* it uses; absent means `code_point`. The normalizers convert at the boundary,
|
|
22
|
+
* so everything past them is UTF-16.
|
|
23
|
+
*
|
|
24
|
+
* ── STATUS OF THESE SHAPES ──────────────────────────────────────────────────
|
|
25
|
+
* All three follow the FINAL contracts: blame start-simpli-api #507
|
|
26
|
+
* (startsim-gulrr), suggestions and comments #508 (startsim-txfrl,
|
|
27
|
+
* startsim-a76zp). Every response declares `offset_unit: "utf16"` and the
|
|
28
|
+
* `version` its positions were mapped to; suggestions and comments carry a
|
|
29
|
+
* nested `author` and a `position {state, from, to, version}` (null when not
|
|
30
|
+
* placed); blame spans carry flat actor columns, and an unknown span carries
|
|
31
|
+
* `via: "unknown"`, a `reason` code and a `detail` sentence. Older spellings
|
|
32
|
+
* the normalizer still reads are kept only so a camelising client works.
|
|
33
|
+
*/
|
|
34
|
+
import type { ActorKind, ActorKindSource } from '../ActorIdentity';
|
|
35
|
+
|
|
36
|
+
export type { ActorKind, ActorKindSource };
|
|
37
|
+
|
|
38
|
+
/** Which unit a response's offsets count. */
|
|
39
|
+
export type OffsetUnit = 'code_point' | 'utf16';
|
|
40
|
+
|
|
41
|
+
/** The actor columns every annotation and blame span carries, both spellings. */
|
|
42
|
+
export interface WireActor {
|
|
43
|
+
actor_sub?: string | null;
|
|
44
|
+
actorSub?: string | null;
|
|
45
|
+
actor_label?: string | null;
|
|
46
|
+
actorLabel?: string | null;
|
|
47
|
+
actor_kind?: ActorKind | string | null;
|
|
48
|
+
actorKind?: ActorKind | string | null;
|
|
49
|
+
actor_kind_source?: ActorKindSource | string | null;
|
|
50
|
+
actorKindSource?: ActorKindSource | string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The nested actor object the suggestion and comment routes answer with
|
|
54
|
+
* (start-simpli-api RED branch feat/tc-suggestions-comments): `author`,
|
|
55
|
+
* `resolution.by`. */
|
|
56
|
+
export interface WireAuthor {
|
|
57
|
+
sub?: string | null;
|
|
58
|
+
label?: string | null;
|
|
59
|
+
kind?: ActorKind | string | null;
|
|
60
|
+
kind_source?: ActorKindSource | string | null;
|
|
61
|
+
kindSource?: ActorKindSource | string | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Where the server mapped an anchor to on read: current-version offsets and
|
|
65
|
+
* the mapping's outcome. `from`/`to` are `null` when orphaned. */
|
|
66
|
+
export interface PositionWire {
|
|
67
|
+
from?: number | null;
|
|
68
|
+
to?: number | null;
|
|
69
|
+
state?: AnchorState | string | null;
|
|
70
|
+
/** The record version the position was mapped to. */
|
|
71
|
+
version?: number | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** An actor after normalization. `label` may be `''` (a service token can carry
|
|
75
|
+
* no email); `kind` is three-state and `unknown` is calm, not a defect. */
|
|
76
|
+
export interface TrackActor {
|
|
77
|
+
sub: string;
|
|
78
|
+
label: string;
|
|
79
|
+
kind: ActorKind;
|
|
80
|
+
kindSource?: ActorKindSource;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* -------------------------------------------------------------------------- */
|
|
84
|
+
/* BLAME GET /api/v1/entities/<id>/blame/?field=<f> (startsim-gulrr) */
|
|
85
|
+
/* -------------------------------------------------------------------------- */
|
|
86
|
+
|
|
87
|
+
/** How a span's text got there. `restore` credits the restorer; an accepted
|
|
88
|
+
* suggestion credits its AUTHOR, with `accepted_by` the acceptor; `unknown`
|
|
89
|
+
* spans carry a `reason` and no actor at all. */
|
|
90
|
+
export type BlameVia = 'edit' | 'accepted_suggestion' | 'restore' | 'unknown';
|
|
91
|
+
|
|
92
|
+
/** Why nobody can be credited with a span. */
|
|
93
|
+
export type BlameReason = 'before_history' | 'truncated' | 'values_omitted' | 'chain_break' | 'unrecorded_change';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One span of GET /api/v1/entities/<id>/blame/?field=<f> (start-simpli-api
|
|
97
|
+
* #507, final contract in startsim-gulrr). Spans are contiguous, non-empty,
|
|
98
|
+
* in order and cover the whole text. Author columns are FLAT on a known span;
|
|
99
|
+
* an unknown span has none of them.
|
|
100
|
+
*/
|
|
101
|
+
export interface BlameSpanWire extends WireActor {
|
|
102
|
+
from: number;
|
|
103
|
+
to: number;
|
|
104
|
+
via?: BlameVia | string | null;
|
|
105
|
+
/** `null` on an unknown span that no revision touched. */
|
|
106
|
+
version?: number | null;
|
|
107
|
+
at?: string | null;
|
|
108
|
+
/** accepted_suggestion only: the ACCEPTOR, with actor_* keys. */
|
|
109
|
+
accepted_by?: WireActor | null;
|
|
110
|
+
acceptedBy?: WireActor | null;
|
|
111
|
+
suggestion_id?: string | null;
|
|
112
|
+
set_id?: string | null;
|
|
113
|
+
/** restore only. */
|
|
114
|
+
restored_from?: number | null;
|
|
115
|
+
restoredFrom?: number | null;
|
|
116
|
+
/** unknown only. */
|
|
117
|
+
reason?: BlameReason | string | null;
|
|
118
|
+
/** unknown only: a readable sentence. */
|
|
119
|
+
detail?: string | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface BlameResponseWire {
|
|
123
|
+
entity_id?: string;
|
|
124
|
+
field?: string;
|
|
125
|
+
/** The record's current version; the spans describe its text. */
|
|
126
|
+
version?: number | null;
|
|
127
|
+
/** Always "utf16" on this route. */
|
|
128
|
+
offset_unit?: OffsetUnit | null;
|
|
129
|
+
offsetUnit?: OffsetUnit | null;
|
|
130
|
+
/** UTF-16 length of the current text. */
|
|
131
|
+
length?: number | null;
|
|
132
|
+
/** False when the type's history is off: then `spans` is null and
|
|
133
|
+
* `detail` says why. */
|
|
134
|
+
recorded?: boolean | null;
|
|
135
|
+
history?: { enabled?: boolean; declared_by?: string } | null;
|
|
136
|
+
trail_starts?: { version?: number; at?: string; from_creation?: boolean } | null;
|
|
137
|
+
spans?: BlameSpanWire[] | null;
|
|
138
|
+
detail?: string | null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* -------------------------------------------------------------------------- */
|
|
142
|
+
/* SUGGESTIONS GET/POST /api/v1/entities/<id>/suggestions/ (startsim-txfrl) */
|
|
143
|
+
/* -------------------------------------------------------------------------- */
|
|
144
|
+
|
|
145
|
+
export type SuggestionStatus = 'open' | 'accepted' | 'rejected' | 'withdrawn' | 'superseded';
|
|
146
|
+
|
|
147
|
+
/** Where an anchor landed after the server mapped it to the current version
|
|
148
|
+
* (design section c). An orphaned anchor is NEVER painted at a guessed place. */
|
|
149
|
+
export type AnchorState = 'mapped' | 'reanchored' | 'orphaned';
|
|
150
|
+
|
|
151
|
+
export interface AnchorWire {
|
|
152
|
+
from: number;
|
|
153
|
+
to: number;
|
|
154
|
+
quote?: string | null;
|
|
155
|
+
prefix?: string | null;
|
|
156
|
+
suffix?: string | null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface SuggestionWire extends WireActor {
|
|
160
|
+
id: string;
|
|
161
|
+
/** The author, as the RED contract nests it. Flat actor columns are read
|
|
162
|
+
* as a fallback. */
|
|
163
|
+
author?: WireAuthor | null;
|
|
164
|
+
/** The anchor mapped to the CURRENT version, with its state. Preferred
|
|
165
|
+
* over every other spelling below. */
|
|
166
|
+
position?: PositionWire | null;
|
|
167
|
+
set_id?: string | null;
|
|
168
|
+
setId?: string | null;
|
|
169
|
+
field?: string | null;
|
|
170
|
+
base_version?: number | null;
|
|
171
|
+
baseVersion?: number | null;
|
|
172
|
+
/** The anchor as PROPOSED, against `base_version`. */
|
|
173
|
+
anchor?: AnchorWire | null;
|
|
174
|
+
/** ASSUMED: the anchor mapped to the CURRENT version on read. Also read:
|
|
175
|
+
* `current`, `mappedAnchor`. When absent, `anchor` is taken to already be
|
|
176
|
+
* in current-version offsets. */
|
|
177
|
+
mapped_anchor?: AnchorWire | null;
|
|
178
|
+
mappedAnchor?: AnchorWire | null;
|
|
179
|
+
current?: AnchorWire | null;
|
|
180
|
+
replacement?: string | null;
|
|
181
|
+
/** The fold of the suggestion's events. Absent means `open`. */
|
|
182
|
+
status?: SuggestionStatus | string | null;
|
|
183
|
+
/** ASSUMED name. Also read: `anchorStatus`, `anchor_state`, `mapping`. */
|
|
184
|
+
anchor_status?: AnchorState | string | null;
|
|
185
|
+
anchorStatus?: AnchorState | string | null;
|
|
186
|
+
created_at?: string | null;
|
|
187
|
+
createdAt?: string | null;
|
|
188
|
+
/** How a closed suggestion was closed: who, when, and (accepted) the
|
|
189
|
+
* revision it produced. Null while open. */
|
|
190
|
+
resolution?: ResolutionWire | null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface ResolutionWire {
|
|
194
|
+
action?: string | null;
|
|
195
|
+
by?: WireAuthor | null;
|
|
196
|
+
at?: string | null;
|
|
197
|
+
reason?: string | null;
|
|
198
|
+
revision?: number | null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface SuggestionPageWire {
|
|
202
|
+
version?: number | null;
|
|
203
|
+
next?: string | null;
|
|
204
|
+
next_cursor?: string | null;
|
|
205
|
+
nextCursor?: string | null;
|
|
206
|
+
results?: SuggestionWire[] | null;
|
|
207
|
+
offset_unit?: OffsetUnit | null;
|
|
208
|
+
offsetUnit?: OffsetUnit | null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The suggesting-mode post (design section d): the WHOLE proposed text; the
|
|
212
|
+
* server hunks it with the shared word diff and supersedes this author's own
|
|
213
|
+
* still-open set. */
|
|
214
|
+
export interface ProposeRequest {
|
|
215
|
+
field: string;
|
|
216
|
+
base_version: number;
|
|
217
|
+
proposed: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* -------------------------------------------------------------------------- */
|
|
221
|
+
/* COMMENTS GET /api/v1/entities/<id>/comments/?field=<f> (startsim-a76zp) */
|
|
222
|
+
/* -------------------------------------------------------------------------- */
|
|
223
|
+
|
|
224
|
+
export interface CommentWire extends WireActor {
|
|
225
|
+
id: string;
|
|
226
|
+
thread_id?: string | null;
|
|
227
|
+
author?: WireAuthor | null;
|
|
228
|
+
edited_at?: string | null;
|
|
229
|
+
editedAt?: string | null;
|
|
230
|
+
/** Mentioned subs. Shown in-app only; nothing is emailed (Quinn). */
|
|
231
|
+
mentions?: string[] | null;
|
|
232
|
+
body?: string | null;
|
|
233
|
+
created_at?: string | null;
|
|
234
|
+
createdAt?: string | null;
|
|
235
|
+
/** Tombstoned: the fact of the comment stays, the body is gone. */
|
|
236
|
+
deleted?: boolean | null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface CommentThreadWire extends WireActor {
|
|
240
|
+
/** Also read: `id`. */
|
|
241
|
+
thread_id?: string | null;
|
|
242
|
+
threadId?: string | null;
|
|
243
|
+
id?: string | null;
|
|
244
|
+
field?: string | null;
|
|
245
|
+
anchor?: AnchorWire | null;
|
|
246
|
+
mapped_anchor?: AnchorWire | null;
|
|
247
|
+
mappedAnchor?: AnchorWire | null;
|
|
248
|
+
current?: AnchorWire | null;
|
|
249
|
+
anchor_status?: AnchorState | string | null;
|
|
250
|
+
anchorStatus?: AnchorState | string | null;
|
|
251
|
+
position?: PositionWire | null;
|
|
252
|
+
author?: WireAuthor | null;
|
|
253
|
+
/** `open` | `resolved`. `resolved: true` also read. */
|
|
254
|
+
status?: string | null;
|
|
255
|
+
resolution?: ResolutionWire | null;
|
|
256
|
+
base_version?: number | null;
|
|
257
|
+
created_at?: string | null;
|
|
258
|
+
resolved?: boolean | null;
|
|
259
|
+
comments?: CommentWire[] | null;
|
|
260
|
+
/** The body withheld by the per-field read gate. */
|
|
261
|
+
redacted?: boolean | null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface CommentPageWire {
|
|
265
|
+
version?: number | null;
|
|
266
|
+
next?: string | null;
|
|
267
|
+
next_cursor?: string | null;
|
|
268
|
+
nextCursor?: string | null;
|
|
269
|
+
results?: CommentThreadWire[] | null;
|
|
270
|
+
offset_unit?: OffsetUnit | null;
|
|
271
|
+
offsetUnit?: OffsetUnit | null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* -------------------------------------------------------------------------- */
|
|
275
|
+
/* THE INJECTED CLIENT */
|
|
276
|
+
/* -------------------------------------------------------------------------- */
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* What the editor surface needs from the server, and nothing it does not.
|
|
280
|
+
* The consuming app binds the record id:
|
|
281
|
+
*
|
|
282
|
+
* ```ts
|
|
283
|
+
* const client: TrackChangesClient = {
|
|
284
|
+
* blame: (q) => api.entities.blame(id, q),
|
|
285
|
+
* listSuggestions: (q) => api.entities.suggestions(id, q),
|
|
286
|
+
* listComments: (q) => api.entities.comments(id, q),
|
|
287
|
+
* propose: (body) => api.entities.propose(id, body),
|
|
288
|
+
* };
|
|
289
|
+
* ```
|
|
290
|
+
*
|
|
291
|
+
* Every read is optional so a host can mount the editor before a route ships.
|
|
292
|
+
* Accept, reject, reply and resolve are the rail's (startsim-whwxd.3), which
|
|
293
|
+
* extends this interface rather than declaring a second one.
|
|
294
|
+
*
|
|
295
|
+
* A 404 from any read means the caller may not read the record or the field.
|
|
296
|
+
* The editor says so; it never treats it as "no annotations".
|
|
297
|
+
*/
|
|
298
|
+
export interface TrackChangesClient {
|
|
299
|
+
blame?: (query: { field: string }) => Promise<BlameResponseWire>;
|
|
300
|
+
/** Keyset-paginated: the hook passes `cursor` (the previous page's
|
|
301
|
+
* `next_cursor`) until there is none, because a proposal supersedes ALL of
|
|
302
|
+
* the author's open suggestions and the editor must know every one. */
|
|
303
|
+
listSuggestions?: (query: { field: string; cursor?: string }) => Promise<SuggestionPageWire | SuggestionWire[]>;
|
|
304
|
+
/** `status: 'all'` is what the rail asks for, so resolved threads stay
|
|
305
|
+
* readable (and reopenable) in its resolved list. */
|
|
306
|
+
listComments?: (query: { field: string; cursor?: string; status?: 'open' | 'resolved' | 'all' }) => Promise<CommentPageWire | CommentThreadWire[]>;
|
|
307
|
+
propose?: (body: ProposeRequest) => Promise<unknown>;
|
|
308
|
+
|
|
309
|
+
/* ---- the rail's writes (startsim-whwxd.3). Each is optional, like the reads:
|
|
310
|
+
* a host that has not bound one simply does not get that control. ---- */
|
|
311
|
+
|
|
312
|
+
/** POST suggestions/accept/ {ids, expected_version}. One revision per
|
|
313
|
+
* suggestion, document order, all or nothing. Resolves the RECORD (the
|
|
314
|
+
* record serializer's body) plus `accepted`; the host must adopt its
|
|
315
|
+
* `version`, or its own next save 412s against this accept. */
|
|
316
|
+
accept?: (req: AcceptRequest) => Promise<AcceptResponseWire>;
|
|
317
|
+
/** POST suggestions/reject/ {ids, reason?}. The record does not move. */
|
|
318
|
+
reject?: (req: { ids: string[]; reason?: string }) => Promise<unknown>;
|
|
319
|
+
/** POST suggestions/<id>/withdraw/. Author only. */
|
|
320
|
+
withdraw?: (id: string) => Promise<unknown>;
|
|
321
|
+
/** POST comments/ — a span, field or record comment. */
|
|
322
|
+
comment?: (req: CommentRequest) => Promise<CommentThreadWire>;
|
|
323
|
+
/** POST comments/<thread_id>/replies/. */
|
|
324
|
+
reply?: (threadId: string, req: { body: string; mentions?: string[] }) => Promise<CommentWire>;
|
|
325
|
+
/** POST comments/<thread_id>/resolve/ */
|
|
326
|
+
resolveThread?: (threadId: string) => Promise<unknown>;
|
|
327
|
+
/** POST comments/<thread_id>/reopen/ */
|
|
328
|
+
reopenThread?: (threadId: string) => Promise<unknown>;
|
|
329
|
+
/** GET /api/v1/org/roster/ — the colleagues a comment may @mention. In-app
|
|
330
|
+
* only: the server sends nothing to anyone mentioned (Quinn, 2026-10-07). */
|
|
331
|
+
roster?: (query: { cursor?: string }) => Promise<RosterPageWire>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface AcceptRequest {
|
|
335
|
+
ids: string[];
|
|
336
|
+
/** The version the reviewer is looking at. REQUIRED by the route (428
|
|
337
|
+
* without it, 412 when stale). */
|
|
338
|
+
expectedVersion: number;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** The accept answer: the record body, plus what was accepted. */
|
|
342
|
+
export interface AcceptResponseWire {
|
|
343
|
+
id?: string | number;
|
|
344
|
+
version?: number | null;
|
|
345
|
+
data?: Record<string, unknown>;
|
|
346
|
+
accepted?: { suggestions?: string[]; revisions?: number[] } | null;
|
|
347
|
+
[key: string]: unknown;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface CommentRequest {
|
|
351
|
+
body: string;
|
|
352
|
+
/** Omitted: a whole-record comment. */
|
|
353
|
+
field?: string;
|
|
354
|
+
/** A span comment: both offsets, UTF-16 at `base_version`. */
|
|
355
|
+
from?: number;
|
|
356
|
+
to?: number;
|
|
357
|
+
base_version?: number;
|
|
358
|
+
mentions?: string[];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface RosterEntryWire {
|
|
362
|
+
sub?: string | null;
|
|
363
|
+
label?: string | null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export interface RosterPageWire {
|
|
367
|
+
next?: string | null;
|
|
368
|
+
next_cursor?: string | null;
|
|
369
|
+
nextCursor?: string | null;
|
|
370
|
+
results?: RosterEntryWire[] | null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** A colleague who can be @mentioned. */
|
|
374
|
+
export interface RosterMember {
|
|
375
|
+
sub: string;
|
|
376
|
+
label: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* -------------------------------------------------------------------------- */
|
|
380
|
+
/* NORMALIZED SHAPES (UTF-16 offsets into the stored string) */
|
|
381
|
+
/* -------------------------------------------------------------------------- */
|
|
382
|
+
|
|
383
|
+
export interface BlameSpan {
|
|
384
|
+
/** Stable within one response: `blame:<from>`. */
|
|
385
|
+
id: string;
|
|
386
|
+
from: number;
|
|
387
|
+
to: number;
|
|
388
|
+
/** `null` for an unknown span. */
|
|
389
|
+
author: TrackActor | null;
|
|
390
|
+
/** Set when nobody can be credited: the sentence a reader is shown. */
|
|
391
|
+
unknownReason: string | null;
|
|
392
|
+
/** The machine-readable reason code of an unknown span. */
|
|
393
|
+
reason: BlameReason | null;
|
|
394
|
+
version: number | null;
|
|
395
|
+
at: string | null;
|
|
396
|
+
via: BlameVia | null;
|
|
397
|
+
acceptedBy: TrackActor | null;
|
|
398
|
+
restoredFrom: number | null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface Suggestion {
|
|
402
|
+
id: string;
|
|
403
|
+
setId: string | null;
|
|
404
|
+
from: number;
|
|
405
|
+
to: number;
|
|
406
|
+
/** The text the suggestion replaces (empty for a pure insert). */
|
|
407
|
+
quote: string;
|
|
408
|
+
/** The text it proposes (empty for a pure delete). */
|
|
409
|
+
replacement: string;
|
|
410
|
+
status: SuggestionStatus;
|
|
411
|
+
anchorState: AnchorState;
|
|
412
|
+
author: TrackActor;
|
|
413
|
+
createdAt: string | null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface TrackComment {
|
|
417
|
+
id: string;
|
|
418
|
+
body: string | null;
|
|
419
|
+
deleted: boolean;
|
|
420
|
+
author: TrackActor;
|
|
421
|
+
createdAt: string | null;
|
|
422
|
+
/** Mentioned subs; shown in-app only. */
|
|
423
|
+
mentions: string[];
|
|
424
|
+
editedAt: string | null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export interface CommentThread {
|
|
428
|
+
id: string;
|
|
429
|
+
/** The field the thread is on; `null` for a whole-record comment. */
|
|
430
|
+
field: string | null;
|
|
431
|
+
/** Who resolved or reopened it last, and when. */
|
|
432
|
+
resolution: { action: string; by: TrackActor; at: string | null } | null;
|
|
433
|
+
/** `null` for a whole-record or whole-field comment. */
|
|
434
|
+
from: number | null;
|
|
435
|
+
to: number | null;
|
|
436
|
+
quote: string;
|
|
437
|
+
anchorState: AnchorState;
|
|
438
|
+
resolved: boolean;
|
|
439
|
+
redacted: boolean;
|
|
440
|
+
comments: TrackComment[];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** What a selection becomes when a host anchors a comment to it. UTF-16
|
|
444
|
+
* offsets; convert with `toServerOffsets` before posting. */
|
|
445
|
+
export interface SelectionAnchor {
|
|
446
|
+
from: number;
|
|
447
|
+
to: number;
|
|
448
|
+
quote: string;
|
|
449
|
+
prefix: string;
|
|
450
|
+
suffix: string;
|
|
451
|
+
}
|