@nuasite/notes 0.22.2 → 0.23.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 +79 -41
- package/dist/overlay.js +227 -65
- package/package.json +1 -1
- package/src/dev/api-handlers.ts +82 -28
- package/src/dev/middleware.ts +9 -4
- package/src/integration.ts +2 -1
- package/src/overlay/App.tsx +88 -21
- package/src/overlay/components/Sidebar.tsx +41 -8
- package/src/overlay/components/SidebarItem.tsx +57 -16
- package/src/overlay/components/Toolbar.tsx +15 -3
- package/src/overlay/index.tsx +3 -1
- package/src/overlay/lib/cms-bridge.ts +61 -11
- package/src/overlay/lib/notes-fetch.ts +27 -5
- package/src/overlay/lib/url-mode.ts +46 -9
- package/src/overlay/styles.css +388 -138
- package/src/overlay/types.ts +11 -1
- package/src/storage/json-store.ts +57 -13
- package/src/storage/types.ts +54 -4
- package/src/types.ts +11 -0
package/src/overlay/types.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export type NoteType = 'comment' | 'suggestion'
|
|
11
|
-
export type NoteStatus = 'open' | 'resolved' | 'applied' | 'rejected' | 'stale'
|
|
11
|
+
export type NoteStatus = 'open' | 'resolved' | 'applied' | 'rejected' | 'stale' | 'deleted'
|
|
12
|
+
export type NoteRole = 'agency' | 'client'
|
|
13
|
+
export type NoteHistoryAction = 'created' | 'updated' | 'resolved' | 'reopened' | 'applied' | 'deleted' | 'purged' | 'stale'
|
|
12
14
|
|
|
13
15
|
export interface NoteRange {
|
|
14
16
|
anchorText: string
|
|
@@ -24,6 +26,13 @@ export interface NoteReply {
|
|
|
24
26
|
createdAt: string
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
export interface NoteHistoryEntry {
|
|
30
|
+
at: string
|
|
31
|
+
action: NoteHistoryAction
|
|
32
|
+
role?: NoteRole
|
|
33
|
+
note?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
export interface NoteItem {
|
|
28
37
|
id: string
|
|
29
38
|
type: NoteType
|
|
@@ -38,6 +47,7 @@ export interface NoteItem {
|
|
|
38
47
|
updatedAt?: string
|
|
39
48
|
status: NoteStatus
|
|
40
49
|
replies: NoteReply[]
|
|
50
|
+
history: NoteHistoryEntry[]
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
export interface NotesPageFile {
|
|
@@ -16,7 +16,7 @@ import fs from 'node:fs/promises'
|
|
|
16
16
|
import path from 'node:path'
|
|
17
17
|
import { generateNoteId } from './id-gen'
|
|
18
18
|
import { normalizePagePath, pageToSlug } from './slug'
|
|
19
|
-
import type { NoteItem, NoteItemPatch, NotesPageFile } from './types'
|
|
19
|
+
import type { NoteHistoryAction, NoteHistoryEntry, NoteItem, NoteItemPatch, NoteRole, NotesPageFile } from './types'
|
|
20
20
|
|
|
21
21
|
const FILE_VERSION_HEADER = '// @nuasite/notes v1\n'
|
|
22
22
|
|
|
@@ -77,11 +77,20 @@ export class NotesJsonStore {
|
|
|
77
77
|
// Tolerate the optional version header
|
|
78
78
|
const stripped = raw.startsWith('//') ? raw.slice(raw.indexOf('\n') + 1) : raw
|
|
79
79
|
const parsed = JSON.parse(stripped) as NotesPageFile
|
|
80
|
-
// Normalize legacy / partial files
|
|
80
|
+
// Normalize legacy / partial files: items predating the history
|
|
81
|
+
// field get an empty array, items predating replies get an empty
|
|
82
|
+
// array. Everything else passes through untouched.
|
|
83
|
+
const items = Array.isArray(parsed.items)
|
|
84
|
+
? parsed.items.map((it) => ({
|
|
85
|
+
...it,
|
|
86
|
+
replies: Array.isArray(it.replies) ? it.replies : [],
|
|
87
|
+
history: Array.isArray((it as Partial<NoteItem>).history) ? (it as NoteItem).history : [],
|
|
88
|
+
}))
|
|
89
|
+
: []
|
|
81
90
|
return {
|
|
82
91
|
page: parsed.page ?? normalized,
|
|
83
92
|
lastUpdated: parsed.lastUpdated ?? nowIso(),
|
|
84
|
-
items
|
|
93
|
+
items,
|
|
85
94
|
}
|
|
86
95
|
} catch (err) {
|
|
87
96
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
@@ -104,16 +113,19 @@ export class NotesJsonStore {
|
|
|
104
113
|
/** Add a new item. Generates the id, createdAt, and defaults status to "open". */
|
|
105
114
|
async addItem(
|
|
106
115
|
page: string,
|
|
107
|
-
input: Omit<NoteItem, 'id' | 'createdAt' | 'status' | 'replies'> & {
|
|
116
|
+
input: Omit<NoteItem, 'id' | 'createdAt' | 'status' | 'replies' | 'history'> & {
|
|
108
117
|
id?: string
|
|
109
118
|
createdAt?: string
|
|
110
119
|
status?: NoteItem['status']
|
|
111
120
|
replies?: NoteItem['replies']
|
|
121
|
+
history?: NoteItem['history']
|
|
112
122
|
},
|
|
123
|
+
role: NoteRole = 'client',
|
|
113
124
|
): Promise<NoteItem> {
|
|
114
125
|
const normalized = normalizePagePath(page)
|
|
115
126
|
return withLock(normalized, async () => {
|
|
116
127
|
const file = await this.readPage(normalized)
|
|
128
|
+
const now = input.createdAt ?? nowIso()
|
|
117
129
|
const item: NoteItem = {
|
|
118
130
|
id: input.id ?? generateNoteId(),
|
|
119
131
|
type: input.type,
|
|
@@ -124,9 +136,10 @@ export class NotesJsonStore {
|
|
|
124
136
|
range: input.range ?? null,
|
|
125
137
|
body: input.body,
|
|
126
138
|
author: input.author,
|
|
127
|
-
createdAt:
|
|
139
|
+
createdAt: now,
|
|
128
140
|
status: input.status ?? 'open',
|
|
129
141
|
replies: input.replies ?? [],
|
|
142
|
+
history: input.history ?? [{ at: now, action: 'created', role }],
|
|
130
143
|
}
|
|
131
144
|
file.items.push(item)
|
|
132
145
|
file.page = normalized
|
|
@@ -136,34 +149,65 @@ export class NotesJsonStore {
|
|
|
136
149
|
})
|
|
137
150
|
}
|
|
138
151
|
|
|
139
|
-
/**
|
|
140
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Patch an existing item. Returns the updated item, or null if not found.
|
|
154
|
+
*
|
|
155
|
+
* `historyAction` is appended to the item's audit trail. Default is
|
|
156
|
+
* `'updated'`; status-changing helpers (`setStatus`) supply more specific
|
|
157
|
+
* actions like `'resolved'` or `'applied'`.
|
|
158
|
+
*/
|
|
159
|
+
async updateItem(
|
|
160
|
+
page: string,
|
|
161
|
+
id: string,
|
|
162
|
+
patch: NoteItemPatch,
|
|
163
|
+
role: NoteRole = 'client',
|
|
164
|
+
historyAction: NoteHistoryAction = 'updated',
|
|
165
|
+
historyNote?: string,
|
|
166
|
+
): Promise<NoteItem | null> {
|
|
141
167
|
const normalized = normalizePagePath(page)
|
|
142
168
|
return withLock(normalized, async () => {
|
|
143
169
|
const file = await this.readPage(normalized)
|
|
144
|
-
const idx = file.items.findIndex(it => it.id === id)
|
|
170
|
+
const idx = file.items.findIndex((it) => it.id === id)
|
|
145
171
|
if (idx === -1) return null
|
|
146
172
|
const existing = file.items[idx]!
|
|
173
|
+
const now = nowIso()
|
|
174
|
+
const entry: NoteHistoryEntry = { at: now, action: historyAction, role }
|
|
175
|
+
if (historyNote) entry.note = historyNote
|
|
147
176
|
const updated: NoteItem = {
|
|
148
177
|
...existing,
|
|
149
178
|
...patch,
|
|
150
179
|
// `range: null` is a meaningful patch (clearing a range), preserve it
|
|
151
180
|
range: 'range' in patch ? (patch.range ?? null) : existing.range,
|
|
152
|
-
updatedAt:
|
|
181
|
+
updatedAt: now,
|
|
182
|
+
history: [...existing.history, entry],
|
|
153
183
|
}
|
|
154
184
|
file.items[idx] = updated
|
|
155
|
-
file.lastUpdated =
|
|
185
|
+
file.lastUpdated = now
|
|
156
186
|
await this.writePageFile(normalized, file)
|
|
157
187
|
return updated
|
|
158
188
|
})
|
|
159
189
|
}
|
|
160
190
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Soft-delete an item — flip its status to `'deleted'` and append a
|
|
193
|
+
* history entry. The item stays in the JSON file so the agency can
|
|
194
|
+
* always see what happened. Returns the updated item, or null if not
|
|
195
|
+
* found. Use `purgeItem` to hard-remove.
|
|
196
|
+
*/
|
|
197
|
+
async deleteItem(page: string, id: string, role: NoteRole = 'client'): Promise<NoteItem | null> {
|
|
198
|
+
return this.updateItem(page, id, { status: 'deleted' }, role, 'deleted')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Hard-remove an item from the file. The agency uses this on items that
|
|
203
|
+
* have already been soft-deleted (or that they explicitly want gone).
|
|
204
|
+
* Returns true if the item existed.
|
|
205
|
+
*/
|
|
206
|
+
async purgeItem(page: string, id: string): Promise<boolean> {
|
|
163
207
|
const normalized = normalizePagePath(page)
|
|
164
208
|
return withLock(normalized, async () => {
|
|
165
209
|
const file = await this.readPage(normalized)
|
|
166
|
-
const idx = file.items.findIndex(it => it.id === id)
|
|
210
|
+
const idx = file.items.findIndex((it) => it.id === id)
|
|
167
211
|
if (idx === -1) return false
|
|
168
212
|
file.items.splice(idx, 1)
|
|
169
213
|
file.lastUpdated = nowIso()
|
package/src/storage/types.ts
CHANGED
|
@@ -10,14 +10,56 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Item lifecycle. Notes start as `open`.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Item lifecycle. Notes start as `open`. The agency moves them through the
|
|
14
|
+
* other states.
|
|
15
|
+
*
|
|
16
|
+
* - `open` — created, not yet acted on
|
|
17
|
+
* - `resolved` — agency marked it handled (no source change)
|
|
18
|
+
* - `applied` — suggestion was written back to the source file
|
|
19
|
+
* - `rejected` — agency declined the suggestion
|
|
20
|
+
* - `stale` — anchor text drifted; the suggestion can no longer be applied
|
|
21
|
+
* - `deleted` — soft-deleted by the agency. Items in this state stay on
|
|
22
|
+
* disk so the audit trail is preserved; the client UI hides
|
|
23
|
+
* them and the agency UI shows them in a collapsed section.
|
|
24
|
+
* A subsequent `purge` call hard-removes the file entry.
|
|
16
25
|
*/
|
|
17
|
-
export type NoteStatus = 'open' | 'resolved' | 'applied' | 'rejected' | 'stale'
|
|
26
|
+
export type NoteStatus = 'open' | 'resolved' | 'applied' | 'rejected' | 'stale' | 'deleted'
|
|
18
27
|
|
|
19
28
|
export type NoteType = 'comment' | 'suggestion'
|
|
20
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Permission roles. Drives both UI affordances and server-side gating:
|
|
32
|
+
*
|
|
33
|
+
* - `client` — the reviewer (default). Can create comments and suggestions
|
|
34
|
+
* and nothing else. Cannot resolve, apply, delete, or purge.
|
|
35
|
+
* Cannot see deleted items.
|
|
36
|
+
* - `agency` — the agency owner. Full controls. The role is identified by
|
|
37
|
+
* the `?nua-agency` URL flag (which sets a sticky cookie) and
|
|
38
|
+
* a matching `x-nua-role: agency` header on every API call.
|
|
39
|
+
*
|
|
40
|
+
* v0.2 ships role enforcement on a per-instance trust basis: the role flag
|
|
41
|
+
* is unauthenticated and anyone who knows the URL can become "agency".
|
|
42
|
+
* That's intentional for v0.2 — the surface is dev-only and the threat model
|
|
43
|
+
* is "stop a non-technical client from accidentally clicking Apply", not
|
|
44
|
+
* "harden against an adversary". A real auth handshake can come later.
|
|
45
|
+
*/
|
|
46
|
+
export type NoteRole = 'agency' | 'client'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One entry in an item's audit trail. Every mutation appends one of these
|
|
50
|
+
* to `item.history` so the agency can always see what happened to an item,
|
|
51
|
+
* even after a soft delete.
|
|
52
|
+
*/
|
|
53
|
+
export type NoteHistoryAction = 'created' | 'updated' | 'resolved' | 'reopened' | 'applied' | 'deleted' | 'purged' | 'stale'
|
|
54
|
+
|
|
55
|
+
export interface NoteHistoryEntry {
|
|
56
|
+
at: string
|
|
57
|
+
action: NoteHistoryAction
|
|
58
|
+
role?: NoteRole
|
|
59
|
+
/** Optional one-line note for context. Currently used by 'applied' to record file path. */
|
|
60
|
+
note?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
21
63
|
/**
|
|
22
64
|
* Range payload for `type: "suggestion"` items.
|
|
23
65
|
*
|
|
@@ -73,6 +115,14 @@ export interface NoteItem {
|
|
|
73
115
|
|
|
74
116
|
status: NoteStatus
|
|
75
117
|
replies: NoteReply[]
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Audit trail of every mutation that has touched this item, in
|
|
121
|
+
* chronological order. Always contains at least one entry (`created`).
|
|
122
|
+
* Items predating this field get an empty array on read; the create
|
|
123
|
+
* timestamp is preserved separately on `createdAt`.
|
|
124
|
+
*/
|
|
125
|
+
history: NoteHistoryEntry[]
|
|
76
126
|
}
|
|
77
127
|
|
|
78
128
|
/**
|
package/src/types.ts
CHANGED
|
@@ -39,6 +39,17 @@ export interface NuaNotesOptions {
|
|
|
39
39
|
*/
|
|
40
40
|
urlFlag?: string
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* URL query flag that activates agency mode. When set on a URL it grants
|
|
44
|
+
* agency permissions (Apply, Resolve, Delete, Purge) and persists a sticky
|
|
45
|
+
* cookie so subsequent navigation stays in agency mode without re-typing.
|
|
46
|
+
* Without this flag — and without the cookie — the overlay runs in client
|
|
47
|
+
* mode and the destructive actions are hidden.
|
|
48
|
+
*
|
|
49
|
+
* Default: `nua-agency`
|
|
50
|
+
*/
|
|
51
|
+
agencyFlag?: string
|
|
52
|
+
|
|
42
53
|
/**
|
|
43
54
|
* Forward `/_nua/notes/*` requests through this proxy target. Mirrors the
|
|
44
55
|
* pattern used by `@nuasite/cms` for sandbox/hosted dev. The target backend
|