mirror of
https://github.com/grub-basket/SP.git
synced 2026-07-22 07:46:27 +00:00
0.146.0: notification jump buttons for aggregate views + action-log capture for lock/unlock/archive/restore
This commit is contained in:
parent
c65aede818
commit
a289dbe399
9 changed files with 120 additions and 85 deletions
144
main.js
144
main.js
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "stashpad",
|
||||
"name": "Stashpad",
|
||||
"version": "0.145.0",
|
||||
"version": "0.146.0",
|
||||
"minAppVersion": "1.13.0",
|
||||
"description": "A chat-style, nested-notes workspace: rapid capture, outliner navigation, fast search, tasks, and per-folder templates, with one-click Open Knowledge Format (OKF) export for LLMs and agents.",
|
||||
"author": "Human",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "stashpad-obsidian",
|
||||
"version": "0.145.0",
|
||||
"version": "0.146.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
|
|||
20
release-notes/0.146.0.md
Normal file
20
release-notes/0.146.0.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# 0.146.0
|
||||
|
||||
## Notification jump buttons
|
||||
|
||||
- Notifications for actions whose result lands in an **aggregate view** now carry a
|
||||
one-click button to jump straight there:
|
||||
- **Locking** notes (selection or a whole folder) → **"All encrypted"**.
|
||||
- **Archiving** notes (plaintext or encrypted, single or bulk) → **"All archived"**.
|
||||
- **Importing** a `.stash` bundle or folder → **"Go to folder"** (next to Undo).
|
||||
- Deleting to trash, restoring from trash, and cross-Stashpad moves already carried
|
||||
their jump buttons; this fills in the ones that were missing so every "where did it
|
||||
go?" notification can take you there.
|
||||
|
||||
## Action-log capture for the encryption / archive / trash lifecycle
|
||||
|
||||
- The per-folder action log now records **lock**, **unlock**, **archive**, and
|
||||
**restore** events — previously these were only shown as transient notifications and
|
||||
left no durable record. They appear in the log history with readable descriptions.
|
||||
- A default delete that goes to Stashpad's own trash is now marked **"→ Trash"** in the
|
||||
log, distinguishing it from a delete sent to Obsidian's native trash.
|
||||
|
|
@ -552,7 +552,10 @@ export class ImportService {
|
|||
duration: 0,
|
||||
folder,
|
||||
affectedPaths: records.flatMap((r) => this.recordNotePaths(r)),
|
||||
actions: [{ label: "Undo import", onClick: () => void doUndo() }],
|
||||
actions: [
|
||||
...(folder ? [{ label: "Go to folder", onClick: () => void this.plugin.activateViewForFolder(folder) }] : []),
|
||||
{ label: "Undo import", onClick: () => void doUndo() },
|
||||
],
|
||||
});
|
||||
// Also put it on the folder's undo stack so Mod+Z / the undo command
|
||||
// reverses the import. Redo is a no-op (re-importing would need the
|
||||
|
|
|
|||
|
|
@ -3788,7 +3788,7 @@ export default class StashpadPlugin extends Plugin {
|
|||
if (await this.lockNoteSubtree(cleaned, id, prevSibling, { silent: true })) count++;
|
||||
}
|
||||
prog?.hide();
|
||||
if (count > 0) this.notifications.show({ message: `Locked ${count} note${count === 1 ? "" : "s"} in “${cleaned.split("/").pop()}”.`, kind: "success", category: "system", folder: cleaned });
|
||||
if (count > 0) this.notifications.show({ message: `Locked ${count} note${count === 1 ? "" : "s"} in “${cleaned.split("/").pop()}”.`, kind: "success", category: "system", folder: cleaned, actions: [{ label: "All encrypted", onClick: () => void openAggregateView(this, "encrypted") }] });
|
||||
return count;
|
||||
}
|
||||
|
||||
|
|
@ -4098,6 +4098,7 @@ export default class StashpadPlugin extends Plugin {
|
|||
}
|
||||
const r = await restorePlaintextDeleted(this.app, blobPath, existing);
|
||||
this.pendingEncBlobs.delete(blobPath);
|
||||
try { await this.newLog().append({ type: "restore", id: meta?.rootId || ROOT_ID, payload: { to: r.restoredTo, from: "trash", encrypted: false } }); } catch { /* log best-effort */ }
|
||||
if (!opts.silent) this.notifications.show({ message: `Restored ${r.notesWritten} note${r.notesWritten === 1 ? "" : "s"} to “${r.restoredTo.split("/").pop()}”.`, kind: "success", category: "system", folder: r.restoredTo, actions: [{ label: "Go to folder", onClick: () => void this.activateViewForFolder(r.restoredTo) }] });
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -4149,6 +4150,7 @@ export default class StashpadPlugin extends Plugin {
|
|||
}
|
||||
const r = await restoreDeleted(this.app, blobPath, dek, existing);
|
||||
this.pendingEncBlobs.delete(blobPath);
|
||||
try { await this.newLog().append({ type: "restore", id: meta?.rootId || ROOT_ID, payload: { to: r.restoredTo, from: "trash", encrypted: true } }); } catch { /* log best-effort */ }
|
||||
// 0.138.0: restore-from-trash counts as an unlock (user decision) — the
|
||||
// note WAS encrypted and is plaintext again. Sidecar carries the identity.
|
||||
if (meta?.rootId) {
|
||||
|
|
@ -5040,7 +5042,7 @@ export default class StashpadPlugin extends Plugin {
|
|||
for (const r of roots) {
|
||||
if (await this.lockNoteSubtree(cleaned, r.id, null, { silent: true })) count++;
|
||||
}
|
||||
if (count > 0) this.notifications.show({ message: `Archived (encrypted) ${count} note${count === 1 ? "" : "s"} moved into “${cleaned.split("/").pop()}”.`, kind: "success", category: "system", folder: cleaned });
|
||||
if (count > 0) this.notifications.show({ message: `Archived (encrypted) ${count} note${count === 1 ? "" : "s"} moved into “${cleaned.split("/").pop()}”.`, kind: "success", category: "system", folder: cleaned, actions: [{ label: "All archived", onClick: () => void openAggregateView(this, "archived") }] });
|
||||
}
|
||||
|
||||
/** Open a fresh Stashpad tab focused on a specific folder via the
|
||||
|
|
|
|||
|
|
@ -319,7 +319,8 @@ export class LogModal extends Modal {
|
|||
case "delete": {
|
||||
const atts = Array.isArray(p.attachmentsRemoved) ? p.attachmentsRemoved.length : 0;
|
||||
const merged = p.mergedInto ? ` (merged into ${p.mergedInto})` : "";
|
||||
return `Deleted ${ev.id}${merged}${atts ? ` — also removed ${atts} attachment${atts === 1 ? "" : "s"}` : ""}`;
|
||||
const toTrash = p.to === "trash" ? " → Trash" : "";
|
||||
return `Deleted ${ev.id}${toTrash}${merged}${atts ? ` — also removed ${atts} attachment${atts === 1 ? "" : "s"}` : ""}`;
|
||||
}
|
||||
case "missing": return `Missing: ${p.lastPath ?? ev.id}`;
|
||||
case "parent_change": {
|
||||
|
|
@ -359,6 +360,10 @@ export class LogModal extends Modal {
|
|||
case "attachment_remove": return `Removed attachment ${p.path ?? ev.id}`;
|
||||
case "palette_color_add": return `Added palette color ${p.color ?? ev.id}`;
|
||||
case "palette_color_remove": return `Removed palette color ${p.color ?? ev.id}`;
|
||||
case "lock": return `Locked (encrypted) ${ev.id}`;
|
||||
case "unlock": return `Unlocked ${ev.id}`;
|
||||
case "archive": return `Archived ${ev.id}${p.to ? ` → ${p.to}` : ""}${p.encrypted === false ? " (plaintext)" : ""}`;
|
||||
case "restore": return `Restored ${ev.id}${p.to ? ` → ${p.to}` : ""} from trash`;
|
||||
default: return JSON.stringify(p);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,7 +289,10 @@ export type LogEventType =
|
|||
| "palette_color_add" | "palette_color_remove"
|
||||
// 0.136.0: one-time move of legacy dedicated-archive contents into the
|
||||
// folder's own archive/ subfolder (per-folder archive overhaul).
|
||||
| "archive_migration";
|
||||
| "archive_migration"
|
||||
// 0.146.0: encryption / archive / trash lifecycle — previously surfaced only
|
||||
// as notifications; now also recorded in the per-folder action log.
|
||||
| "lock" | "unlock" | "archive" | "restore";
|
||||
|
||||
export interface LogEvent {
|
||||
ts: string;
|
||||
|
|
|
|||
14
src/view.ts
14
src/view.ts
|
|
@ -27,6 +27,7 @@ import { setActiveView, clearActiveView } from "./active-view";
|
|||
import { BreadcrumbLevelsModal, type BreadcrumbLevel, ColorPickerModal, ConfirmDeleteModal, ConfirmModal, DueDatePickerModal, SplitNoteModal } from "./modals";
|
||||
import { ComposerAutocomplete } from "./composer-autocomplete";
|
||||
import { matchBinding, humanCombo } from "./view-keys";
|
||||
import { openAggregateView } from "./aggregate-view";
|
||||
import { AuthorshipTracker } from "./authorship-tracker";
|
||||
import { ViewDnD } from "./view-dnd";
|
||||
import { NoteBodyRenderer } from "./note-body-renderer";
|
||||
|
|
@ -6692,13 +6693,13 @@ export class StashpadView extends ItemView {
|
|||
const prevSibling = idx > 0 ? order[idx - 1] : null;
|
||||
// Silent per-item; one summary toast below (a batch shouldn't spam).
|
||||
const r = await this.plugin.lockNoteSubtree(this.noteFolder, t.id, prevSibling, { silent: true });
|
||||
if (r) locked++;
|
||||
if (r) { locked++; await this.log.append({ type: "lock", id: t.id }); }
|
||||
}
|
||||
if (locked > 0) {
|
||||
this.selection.clear();
|
||||
this.lastSelected = null;
|
||||
this.render();
|
||||
this.plugin.notifications.show({ message: `Locked ${locked} stash${locked === 1 ? "" : "es"}.`, kind: "success", category: "system", folder: this.noteFolder });
|
||||
this.plugin.notifications.show({ message: `Locked ${locked} stash${locked === 1 ? "" : "es"}.`, kind: "success", category: "system", folder: this.noteFolder, actions: [{ label: "All encrypted", onClick: () => void openAggregateView(this.plugin, "encrypted") }] });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6754,8 +6755,9 @@ export class StashpadView extends ItemView {
|
|||
try { await this.app.fileManager.renameFile(f, to); moves.push({ from: fromPath, to }); } catch (e) { console.warn("[Stashpad] plaintext archive move failed", fromPath, e); }
|
||||
}
|
||||
this.selection.clear(); this.lastSelected = null; this.tree.rebuild(src); this.render();
|
||||
if (moves.length > 0) for (const rt of roots0) await this.log.append({ type: "archive", id: rt.id, payload: { to: cleanDest, encrypted: false } });
|
||||
const nm = cleanDest.split("/").pop() || cleanDest;
|
||||
this.plugin.notifications.show({ message: `Moved ${moves.length} note${moves.length === 1 ? "" : "s"} → plaintext archive “${nm}” (de-indexed, not encrypted). Undo to bring ${moves.length === 1 ? "it" : "them"} back.`, kind: "success", category: "system", folder: src });
|
||||
this.plugin.notifications.show({ message: `Moved ${moves.length} note${moves.length === 1 ? "" : "s"} → plaintext archive “${nm}” (de-indexed, not encrypted). Undo to bring ${moves.length === 1 ? "it" : "them"} back.`, kind: "success", category: "system", folder: src, actions: [{ label: "All archived", onClick: () => void openAggregateView(this.plugin, "archived") }] });
|
||||
this.plugin.getUndoStack(src).push({
|
||||
label: `Archive (plaintext, ${roots0.length})`,
|
||||
undo: async () => { for (const m of moves) { const fl = this.app.vault.getAbstractFileByPath(m.to); if (fl instanceof TFile) { try { await this.app.fileManager.renameFile(fl, m.from); } catch (e) { console.warn("[Stashpad] plaintext archive undo failed", m.to, e); } } } this.tree.rebuild(src); this.render(); },
|
||||
|
|
@ -6784,12 +6786,12 @@ export class StashpadView extends ItemView {
|
|||
const idx = order.indexOf(t.id);
|
||||
const prevSibling = idx > 0 ? order[idx - 1] : null;
|
||||
const r = await this.plugin.lockNoteSubtree(src, t.id, prevSibling, { silent: true, blobFolder: dest });
|
||||
if (r) blobs.push(r.blobPath);
|
||||
if (r) { blobs.push(r.blobPath); await this.log.append({ type: "archive", id: t.id, payload: { to: dest, encrypted: true } }); }
|
||||
}
|
||||
if (blobs.length === 0) return;
|
||||
this.selection.clear(); this.lastSelected = null; this.tree.rebuild(src); this.render();
|
||||
const name = dest.split("/").pop() || dest;
|
||||
this.plugin.notifications.show({ message: `Archived ${blobs.length} note${blobs.length === 1 ? "" : "s"} → “${name}”. Undo to bring ${blobs.length === 1 ? "it" : "them"} back.`, kind: "success", category: "system", folder: src });
|
||||
this.plugin.notifications.show({ message: `Archived ${blobs.length} note${blobs.length === 1 ? "" : "s"} → “${name}”. Undo to bring ${blobs.length === 1 ? "it" : "them"} back.`, kind: "success", category: "system", folder: src, actions: [{ label: "All archived", onClick: () => void openAggregateView(this.plugin, "archived") }] });
|
||||
this.plugin.getUndoStack(src).push({
|
||||
label: `Archive (${blobs.length})`,
|
||||
undo: async () => {
|
||||
|
|
@ -6911,7 +6913,7 @@ export class StashpadView extends ItemView {
|
|||
const folder = this.noteFolder;
|
||||
const rootIds = roots.map((r) => r.id);
|
||||
let blobs: string[] = [];
|
||||
for (const id of rootIds) { const b = await this.plugin.plaintextDeleteSubtree(folder, id); if (b) blobs.push(b); }
|
||||
for (const id of rootIds) { const b = await this.plugin.plaintextDeleteSubtree(folder, id); if (b) { blobs.push(b); await this.log.append({ type: "delete", id, payload: { to: "trash", bundle: b } }); } }
|
||||
if (blobs.length === 0) return;
|
||||
this.selection.clear(); this.lastSelected = null; this.tree.rebuild(folder); this.render();
|
||||
this.plugin.notifications.show({ message: `Deleted ${blobs.length} note${blobs.length === 1 ? "" : "s"} → Trash. Undo to bring ${blobs.length === 1 ? "it" : "them"} back.`, kind: "success", category: "system", folder, actions: [{ label: "Open Trash", onClick: () => this.plugin.openEncryptedTrash() }] });
|
||||
|
|
|
|||
Loading…
Reference in a new issue