@kernhq/module-quire 0.11.0 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernhq/module-quire",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Kern Quire: collaborative documents, spaces and page trees",
5
5
  "homepage": "https://github.com/KernAIO/module-quire#readme",
6
6
  "license": "AGPL-3.0-only",
@@ -17,6 +17,7 @@ import {
17
17
  toast,
18
18
  } from '@kernhq/ui'
19
19
  import { createQuery, useQueryClient } from '@tanstack/svelte-query'
20
+ import { untrack } from 'svelte'
20
21
  import { getQuireApi } from '../api-instance.js'
21
22
  import CommentsPanel from '../components/CommentsPanel.svelte'
22
23
  import ConfirmDialog from '../components/ConfirmDialog.svelte'
@@ -148,19 +149,69 @@ $effect(() => {
148
149
  dirty = false
149
150
  })
150
151
 
152
+ /**
153
+ * The title saves as you type, not only when you leave the field.
154
+ *
155
+ * It used to save on `blur` alone, and nothing else — so typing a name and then going somewhere
156
+ * without blurring first (a keyboard shortcut, ⌘K, closing the tab, any programmatic navigation)
157
+ * threw the name away and left the page called "Untitled" for ever. Measured against the live
158
+ * stack: fifteen seconds after typing, `mod_quire.pages.title` was still `''`; it only ever became
159
+ * the typed value on blur.
160
+ *
161
+ * A page's *body* has never had this problem, because it is a Y.Doc the collab service persists on
162
+ * its own schedule. The title is not — it is a plain column behind `pages.update` — so the schedule
163
+ * has to be written here. `docs/adr/0006` says the title should live in the Y.Doc beside the body
164
+ * for exactly this reason, and because two people renaming at once currently clobber each other;
165
+ * that is a larger change and this is not a substitute for it.
166
+ */
167
+ const TITLE_SAVE_AFTER_MS = 700
168
+ let titleTimer: ReturnType<typeof setTimeout> | null = null
169
+ /* Set in the same tick as the call, because `isPending` arrives a render late and two saves would
170
+ race to write the same column in an order neither of them chose. */
171
+ let savingTitle = false
172
+
173
+ function queueTitleSave() {
174
+ if (titleTimer) clearTimeout(titleTimer)
175
+ titleTimer = setTimeout(() => {
176
+ titleTimer = null
177
+ void saveTitle()
178
+ }, TITLE_SAVE_AFTER_MS)
179
+ }
180
+
151
181
  async function saveTitle() {
152
- if (!doc || !dirty) return
182
+ if (titleTimer) {
183
+ clearTimeout(titleTimer)
184
+ titleTimer = null
185
+ }
186
+ if (!doc || !dirty || savingTitle) return
153
187
  const next = title.trim()
154
188
  if (next === doc.title) {
155
189
  dirty = false
156
190
  return
157
191
  }
158
- await api.pages.update({ workspaceId, pageId, title: next })
159
- dirty = false
160
- await client.invalidateQueries({ queryKey: quireKeys.page(workspaceId, pageId) })
161
- await client.invalidateQueries({ queryKey: quireKeys.tree(workspaceId, doc.spaceId) })
192
+ savingTitle = true
193
+ try {
194
+ await api.pages.update({ workspaceId, pageId, title: next })
195
+ dirty = false
196
+ await client.invalidateQueries({ queryKey: quireKeys.page(workspaceId, pageId) })
197
+ await client.invalidateQueries({ queryKey: quireKeys.tree(workspaceId, doc.spaceId) })
198
+ } finally {
199
+ savingTitle = false
200
+ }
162
201
  }
163
202
 
203
+ /* Leaving the page mid-word must not lose the word. */
204
+ $effect(() => {
205
+ void pageId
206
+ return () => {
207
+ if (titleTimer) {
208
+ clearTimeout(titleTimer)
209
+ titleTimer = null
210
+ }
211
+ if (untrack(() => dirty)) void saveTitle()
212
+ }
213
+ })
214
+
164
215
  async function archive(archived: boolean) {
165
216
  if (!doc) return
166
217
  await api.pages.archive({ workspaceId, pageId, archived })
@@ -402,6 +453,7 @@ async function undoTrash(workspace: string, id: string, spaceId: string, title:
402
453
  oninput={(e) => {
403
454
  title = (e.currentTarget as HTMLInputElement).value
404
455
  dirty = true
456
+ queueTitleSave()
405
457
  }}
406
458
  onblur={saveTitle}
407
459
  onkeydown={(e) => {