mirror of
https://github.com/grub-basket/SP.git
synced 2026-07-22 07:46:27 +00:00
0.99.19: multiplayer authorship + task due-date reminders + full settings search
This commit is contained in:
parent
4add22ecfc
commit
07638ea7fe
11 changed files with 351 additions and 63 deletions
100
main.js
100
main.js
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "stashpad",
|
||||
"name": "Stashpad",
|
||||
"version": "0.99.14",
|
||||
"version": "0.99.19",
|
||||
"minAppVersion": "1.7.0",
|
||||
"description": "Chat-style nested-notes view: rapid capture, outliner navigation, in-place editing.",
|
||||
"author": "Human",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "stashpad-obsidian",
|
||||
"version": "0.99.14",
|
||||
"version": "0.99.19",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
|
|||
28
release-notes/0.99.19.md
Normal file
28
release-notes/0.99.19.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# 0.99.19 — Multiplayer authorship, task reminders, full settings search
|
||||
|
||||
## Task due-date reminders
|
||||
|
||||
- Setting a due date on a task now actually reminds you. Because a plugin can't
|
||||
run while the app is closed, reminders fire **when Obsidian next launches**
|
||||
(for anything that came due while it was closed) and on a periodic check while
|
||||
it's open. Each is a persistent notification with an **Open** button, recorded
|
||||
in the notification history under a new **Reminder** category (and respecting
|
||||
its mute toggle). A reminder won't re-fire for the same task + date once shown.
|
||||
|
||||
## Multiplayer authorship (shared vaults)
|
||||
|
||||
- **Assigning works across the whole vault now.** The "Assign to" / due-date
|
||||
picker lists every author found anywhere in the vault — not just the ones this
|
||||
device's registry happens to know — so a coworker who's only worked in *other*
|
||||
folders shows up with their real identity, and assigning them no longer creates
|
||||
a duplicate.
|
||||
- **New folders auto-populate** with every known author, so you can assign anyone
|
||||
immediately without waiting for them to contribute there first.
|
||||
- **New command: "Sync authors across all folders."** Backfills existing folders
|
||||
so every known author exists everywhere — handy after adding a collaborator.
|
||||
|
||||
## Settings search
|
||||
|
||||
- Every Stashpad setting is now findable in Obsidian's settings search, including
|
||||
the **Authorship**, **Templates** (color aliases / note templates), and **JD
|
||||
Index** pages — previously only their page names matched.
|
||||
18
scripts/backup-dev-folder
Executable file
18
scripts/backup-dev-folder
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/sh
|
||||
# Back up a Claude Dev Vault folder to a timestamped zip BEFORE running tests
|
||||
# that might touch it (especially the real "Stashpad" folder, or vault-wide
|
||||
# commands). Stored OUTSIDE the vault so it isn't indexed/imported.
|
||||
# Usage: scripts/backup-dev-folder <FolderName> (e.g. Stashpad)
|
||||
# Paths are relative to this script (scripts/ inside the dev repo): the dev vault
|
||||
# is a sibling of the dev repo, under the parent project dir.
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$DIR/../.." && pwd)" # parent of the dev repo (the project dir)
|
||||
VAULT="$ROOT/Claude Dev Vault"
|
||||
DEST="$ROOT/Claude Dev Vault Backups"
|
||||
F="$1"
|
||||
[ -n "$F" ] || { echo "usage: backup-dev-folder <FolderName>" >&2; exit 1; }
|
||||
[ -d "$VAULT/$F" ] || { echo "no such folder: $VAULT/$F" >&2; exit 1; }
|
||||
mkdir -p "$DEST"
|
||||
TS=$(date "+%Y-%m-%d-%H%M%S")
|
||||
OUT="$DEST/$F-$TS.zip"
|
||||
( cd "$VAULT" && zip -rq "$OUT" "$F" ) && echo "backed up '$F' -> $OUT"
|
||||
|
|
@ -100,7 +100,7 @@ case "$1" in
|
|||
try { window.__claudeTestBanner.hide(); } catch (e) {}
|
||||
}
|
||||
window.__claudeTestBanner = null;
|
||||
new Notice('✅ Safe to use Stashpad again — Claude has finished testing.', 6000);
|
||||
new Notice('✅ Safe to use Stashpad again — Claude has finished testing.', 0);
|
||||
return 'cleared';
|
||||
})()" >/dev/null 2>&1
|
||||
echo "guard: banner cleared, all-clear shown"
|
||||
|
|
|
|||
146
src/main.ts
146
src/main.ts
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { DEFAULT_STOPWORDS, bodyToSlug, buildFilename, parseIdFromFilename } from "./slug-service";
|
||||
import { getActiveView, onActiveViewChange } from "./active-view";
|
||||
import { importStashZip, STASH_EXT, splitFrontmatter } from "./stash-package";
|
||||
import { formatDateTime } from "./format";
|
||||
import { resolveStashBytes, isEncryptedStash } from "./stash-crypto";
|
||||
import { StashpadLog } from "./log";
|
||||
import { ROOT_ID } from "./types";
|
||||
|
|
@ -291,6 +292,9 @@ export default class StashpadPlugin extends Plugin {
|
|||
// 0.77.7: seed the local user's author page into the new folder so
|
||||
// their links resolve everywhere from the start.
|
||||
try { await this.seedLocalAuthorStub(cleaned); } catch {}
|
||||
// 0.99.17 (#2): also seed every KNOWN author (coworkers from other folders)
|
||||
// so a new folder auto-populates and you can assign anyone immediately.
|
||||
try { await this.seedKnownAuthorsInFolder(cleaned); } catch {}
|
||||
}
|
||||
|
||||
/** Tally per-note colors found in EVERY markdown file under `folder`.
|
||||
|
|
@ -710,6 +714,11 @@ export default class StashpadPlugin extends Plugin {
|
|||
// Vault is fully indexed now — safe to reconcile locked placeholders
|
||||
// (drop entries whose blob is truly gone, add cross-device blobs).
|
||||
void this.reconcileLockedRegistry();
|
||||
// 0.99.19: fire reminders for tasks that came due while Obsidian was
|
||||
// closed (delay so the metadata cache is populated), then re-check on an
|
||||
// interval so tasks coming due while it's open also surface.
|
||||
window.setTimeout(() => void this.checkDueReminders(), 6000);
|
||||
this.registerInterval(window.setInterval(() => void this.checkDueReminders(), 5 * 60 * 1000));
|
||||
window.setTimeout(() => { void this.seedLocalAuthorStubsEverywhere(); }, 4000);
|
||||
// 0.79.12: register each Stashpad folder's _archive in Obsidian's
|
||||
// "Excluded files" so native search / quick switcher / graph / link
|
||||
|
|
@ -1408,6 +1417,11 @@ export default class StashpadPlugin extends Plugin {
|
|||
// 0.58.0: rebootstrap as a command palette entry — mirrors the
|
||||
// "Rebootstrap now" button in settings. Useful when troubleshooting
|
||||
// / migrating without opening Settings.
|
||||
this.addCommand({
|
||||
id: "stashpad-sync-authors",
|
||||
name: "Sync authors across all folders (multiplayer)",
|
||||
callback: () => void this.syncAuthorsAcrossFolders(),
|
||||
});
|
||||
this.addCommand({
|
||||
id: "stashpad-rebootstrap-all",
|
||||
name: "Rebootstrap all Stashpad folders (backfill metadata + rename stale titles)",
|
||||
|
|
@ -3620,29 +3634,150 @@ export default class StashpadPlugin extends Plugin {
|
|||
* Used when assigning a task to someone so the assignee wikilink
|
||||
* resolves. No-op if a stub for this id already exists in the dir
|
||||
* (under any name). Also registers the author. */
|
||||
async ensureAuthorStubFor(folder: string, id: string, name: string): Promise<void> {
|
||||
if (!id || !name) return;
|
||||
async ensureAuthorStubFor(folder: string, id: string, name: string): Promise<boolean> {
|
||||
if (!id || !name) return false;
|
||||
this.authorRegistry.record({ id, name });
|
||||
const dir = `${folder.replace(/\/+$/, "")}/_authors`;
|
||||
const exists = this.app.vault.getMarkdownFiles().some(
|
||||
(f) => f.path.startsWith(dir + "/") && this.parseAuthorFilePath(f.path)?.id === id,
|
||||
);
|
||||
if (exists) return;
|
||||
if (exists) return false;
|
||||
const rec = this.authorRegistry.get(id);
|
||||
const safe = this.authorNameToSafe(name);
|
||||
const path = `${dir}/${safe}-${id}.md`;
|
||||
try {
|
||||
await this.ensureFolderPath(dir);
|
||||
if (await this.app.vault.adapter.exists(path)) return;
|
||||
if (await this.app.vault.adapter.exists(path)) return false;
|
||||
await this.app.vault.create(path, this.buildAuthorStub(
|
||||
{ id, name, role: rec?.role, department: rec?.department },
|
||||
new Date().toISOString(),
|
||||
));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn("[Stashpad] ensureAuthorStubFor failed", path, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 0.99.17 (#2): seed EVERY known author (vault-wide) into `folder`'s
|
||||
* `_authors/`, not just the local user — so a new folder auto-populates with
|
||||
* coworkers and assignment works without waiting for them to contribute. Each
|
||||
* stub reuses the author's real id. Returns how many stubs were created. */
|
||||
async seedKnownAuthorsInFolder(folder: string): Promise<number> {
|
||||
let created = 0;
|
||||
for (const a of this.collectKnownAuthors()) {
|
||||
if (await this.ensureAuthorStubFor(folder, a.id, a.name)) created++;
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
/** 0.99.19: Task due-date reminders. Obsidian plugins can't fire while the app
|
||||
* is closed, so this runs at LAUNCH (onLayoutReady) and on an interval while
|
||||
* running: it finds tasks whose `due` has passed and that haven't been
|
||||
* reminded yet (tracked by `<id>@<dueRaw>` in settings.notifiedDueKeys, so the
|
||||
* same task+due never re-fires — a changed due date re-keys and reminds again),
|
||||
* shows a PERSISTENT notification under the "reminder" category (so it lands in
|
||||
* the history log + respects mute), then records it. */
|
||||
async checkDueReminders(): Promise<void> {
|
||||
const now = Date.now();
|
||||
const notified = new Set(this.settings.notifiedDueKeys ?? []);
|
||||
const due: Array<{ id: string; folder: string; file: TFile; dueMs: number; key: string }> = [];
|
||||
for (const f of this.app.vault.getMarkdownFiles()) {
|
||||
if (f.path.includes("/_authors/")) continue;
|
||||
const fm = this.app.metadataCache.getFileCache(f)?.frontmatter as { id?: unknown; due?: unknown } | undefined;
|
||||
if (!fm || fm.due == null) continue;
|
||||
const id = typeof fm.id === "string" ? fm.id : "";
|
||||
if (!id) continue;
|
||||
const dueRaw = String(fm.due);
|
||||
const dueMs = typeof fm.due === "number" ? fm.due : Date.parse(dueRaw);
|
||||
if (!Number.isFinite(dueMs) || dueMs > now) continue; // not due yet
|
||||
const key = `${id}@${dueRaw}`;
|
||||
if (notified.has(key)) continue;
|
||||
due.push({ id, folder: (f.parent?.path ?? "").replace(/\/+$/, ""), file: f, dueMs, key });
|
||||
}
|
||||
if (due.length === 0) return;
|
||||
// Record up front so the interval / a fast re-entry can't double-fire.
|
||||
this.settings.notifiedDueKeys = [...(this.settings.notifiedDueKeys ?? []), ...due.map((d) => d.key)].slice(-2000);
|
||||
await this.saveSettings();
|
||||
const titleOf = async (file: TFile): Promise<string> => {
|
||||
try {
|
||||
const body = splitFrontmatter(await this.app.vault.cachedRead(file)).body;
|
||||
const line = body.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
|
||||
if (line) return line.replace(/^[#>\-*\s]+/, "").slice(0, 60);
|
||||
} catch { /* fall through to filename */ }
|
||||
return file.basename.replace(/-[a-z0-9]{4,12}$/, "").replace(/-/g, " ");
|
||||
};
|
||||
if (due.length <= 3) {
|
||||
for (const d of due) {
|
||||
const title = await titleOf(d.file);
|
||||
this.notifications.show({
|
||||
message: `⏰ Task due: “${title}” (${formatDateTime(d.dueMs, this.settings)})`,
|
||||
kind: "warning", category: "reminder", duration: 0, folder: d.folder, affectedIds: [d.id],
|
||||
actions: [{ label: "Open", onClick: () => void this.revealNoteByRef(d.folder, d.id) }],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.notifications.show({
|
||||
message: `⏰ ${due.length} tasks are due — open the Tasks panel to review.`,
|
||||
kind: "warning", category: "reminder", duration: 0, folder: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 0.99.17 (#3): the "centralized sync" — rebuild the registry from the whole
|
||||
* vault, then ensure every known author has a stub in every Stashpad folder.
|
||||
* Backfills existing folders (new folders are handled at creation). */
|
||||
async syncAuthorsAcrossFolders(): Promise<void> {
|
||||
await this.rebuildAuthorRegistry(); // learn every author from the vault first
|
||||
const authors = this.collectKnownAuthors();
|
||||
const folders = this.discoverStashpadFolders();
|
||||
if (!authors.length || !folders.length) { new Notice("No authors or Stashpad folders to sync."); return; }
|
||||
const prog = folders.length * authors.length > 8 ? new Notice("", 0) : null;
|
||||
let created = 0;
|
||||
for (const folder of folders) {
|
||||
prog?.setMessage(`Syncing authors → ${folder.split("/").pop()}…`);
|
||||
for (const a of authors) {
|
||||
if (await this.ensureAuthorStubFor(folder, a.id, a.name)) created++;
|
||||
}
|
||||
}
|
||||
prog?.hide();
|
||||
this.notifications.show({
|
||||
message: `Synced authors across ${folders.length} folder${folders.length === 1 ? "" : "s"} — ${created} new stub${created === 1 ? "" : "s"} (${authors.length} author${authors.length === 1 ? "" : "s"} known).`,
|
||||
kind: "success", category: "system", folder: "",
|
||||
});
|
||||
}
|
||||
|
||||
/** 0.99.16: VAULT-WIDE list of known authors for the assignee pickers — the
|
||||
* union of the LOCAL registry (per-config, so in a shared vault it mostly
|
||||
* knows just this user) AND a scan of every folder's `_authors/` stub files
|
||||
* (id from the filename, display name from the stub's aliases/name). This is
|
||||
* what surfaces COWORKERS who exist in shared folders but aren't in this
|
||||
* device's registry — the reason only the local user showed up before.
|
||||
* Deduped by id (registry name wins); the local user is listed first. Also
|
||||
* warms the registry with anything new it finds (idempotent after the first). */
|
||||
collectKnownAuthors(): Array<{ id: string; name: string }> {
|
||||
const byId = new Map<string, string>();
|
||||
// The local user is always "known" (from settings), even before they have a
|
||||
// stub or any authored notes — and listed first.
|
||||
const myId = (this.settings.authorId ?? "").trim();
|
||||
const myName = (this.settings.authorName ?? "").trim();
|
||||
if (myId && myName) byId.set(myId, myName);
|
||||
for (const a of this.authorRegistry.all()) if (!byId.has(a.id)) byId.set(a.id, a.name);
|
||||
for (const f of this.app.vault.getMarkdownFiles()) {
|
||||
if (!f.path.includes("/_authors/")) continue;
|
||||
const parsed = this.parseAuthorFilePath(f.path);
|
||||
if (!parsed) continue;
|
||||
const fm = this.app.metadataCache.getFileCache(f)?.frontmatter as { aliases?: unknown; name?: unknown } | undefined;
|
||||
const aliasName = Array.isArray(fm?.aliases)
|
||||
? ((fm!.aliases as unknown[]).find((x) => typeof x === "string") as string | undefined ?? "")
|
||||
: (typeof fm?.aliases === "string" ? fm.aliases : "");
|
||||
const name = (aliasName || (typeof fm?.name === "string" ? fm.name : "") || parsed.name).trim();
|
||||
if (!byId.has(parsed.id)) byId.set(parsed.id, name);
|
||||
this.authorRegistry.record({ id: parsed.id, name }); // warm the registry (no-op if unchanged)
|
||||
}
|
||||
return [...byId.entries()].map(([id, name]) => ({ id, name }));
|
||||
}
|
||||
|
||||
/** 0.77.2: rebuild the author registry from scratch by scanning the
|
||||
* vault. The authoritative inputs are (a) the `_authors` stub files
|
||||
* (id from filename, display name from `aliases`/`name`/H1, plus role/
|
||||
|
|
@ -4141,6 +4276,9 @@ export default class StashpadPlugin extends Plugin {
|
|||
notificationHistoryLimit: (typeof data?.notificationHistoryLimit === "number" && Number.isFinite(data.notificationHistoryLimit))
|
||||
? data.notificationHistoryLimit
|
||||
: 5000,
|
||||
notifiedDueKeys: Array.isArray(data?.notifiedDueKeys)
|
||||
? data.notifiedDueKeys.filter((x: unknown): x is string => typeof x === "string").slice(-2000)
|
||||
: [],
|
||||
drafts: normalizeDrafts(data?.drafts),
|
||||
lastSubmitted: data?.lastSubmitted && typeof data.lastSubmitted === "object" ? data.lastSubmitted : {},
|
||||
// Migrate: when slugStopWords has never been set on this install
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export type NotificationCategory =
|
|||
| "attachment"
|
||||
| "color" | "reorder"
|
||||
| "multiplayer"
|
||||
| "reminder"
|
||||
| "system";
|
||||
|
||||
/** Human-readable labels for each category, used by the settings UI
|
||||
|
|
@ -45,6 +46,7 @@ export const CATEGORY_LABELS: Record<NotificationCategory, { label: string; desc
|
|||
color: { label: "Color", desc: "Per-note color changes." },
|
||||
reorder: { label: "Reorder", desc: "Drag-reorder and keyboard moveUp/Down/Top/Bottom." },
|
||||
multiplayer: { label: "Multiplayer", desc: "Cross-author activity (someone else touched your notes or vice versa)." },
|
||||
reminder: { label: "Reminder", desc: "Task due-date reminders — surfaced when a task comes due (fired at launch / periodically)." },
|
||||
system: { label: "System", desc: "Plumbing toasts: backfill progress, integrity warnings, errors not tied to a verb." },
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -406,6 +406,9 @@ export interface StashpadSettings {
|
|||
* Default 5000. Persisted alongside the live history in
|
||||
* `<pluginDir>/notifications.json`. */
|
||||
notificationHistoryLimit: number;
|
||||
/** Keys (`<id>@<dueRaw>`) of task due-reminders already fired, so they don't
|
||||
* re-fire on every launch. Bounded; pruned when it grows. */
|
||||
notifiedDueKeys: string[];
|
||||
/** 0.71.0 / 0.71.2: JD Index Builder. Two flavors:
|
||||
* - "Preview" → writes a single Index.md inside the designated
|
||||
* Stashpad folder, showing the would-be hierarchy + non-matches.
|
||||
|
|
@ -503,6 +506,7 @@ export const DEFAULT_SETTINGS: StashpadSettings = {
|
|||
attachmentsOnlyNotes: {},
|
||||
mutedNotificationCategories: [],
|
||||
notificationHistoryLimit: 5000,
|
||||
notifiedDueKeys: [],
|
||||
autoNavOnMoveIn: false,
|
||||
autoNavOnMoveOut: false,
|
||||
autoExpandCursorRow: false,
|
||||
|
|
@ -657,10 +661,12 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
case "diagnostics": return this.diagnosticsItems();
|
||||
case "general": return this.generalItems();
|
||||
case "encryption": return this.encryptionItems();
|
||||
// authorship/templates/jdindex still render via the fresh-at-open `page:`
|
||||
// path (searchable by page name). They're per-folder editors that don't
|
||||
// decompose into clean per-setting entries; render-at-display via the page
|
||||
// renderer keeps them correct. Returning null falls back to that path.
|
||||
// 0.99.15: authorship/templates/jdindex decomposed too — static fields as
|
||||
// per-setting items, the per-folder editors as sectionDefs (rendered fresh
|
||||
// at display) — so individual settings are searchable, not just page names.
|
||||
case "authorship": return this.authorshipItems();
|
||||
case "templates": return this.templatesItems();
|
||||
case "jdindex": return this.jdIndexItems();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1449,6 +1455,89 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
* unique links). The id never changes once set, so already-stamped
|
||||
* notes keep referring to the right person even if they rename
|
||||
* themselves later. */
|
||||
/** 0.99.15: Authorship tab decomposed for native settings search — static
|
||||
* fields as per-setting renderDefs; the dynamic "folders worked in" + "known
|
||||
* authors" lists as sectionDefs (rendered fresh at display). */
|
||||
private authorshipItems(): SettingDefinitionItem[] {
|
||||
const items: SettingDefinitionItem[] = [];
|
||||
items.push(this.renderDef("Author name",
|
||||
"Your display name. Used in the note footer + as the author/contributor link target. Leave blank to opt out (notes won't be stamped).",
|
||||
(s) => s.addText((t) => t.setValue(this.plugin.settings.authorName).onChange(async (v) => {
|
||||
this.plugin.settings.authorName = v.trim();
|
||||
if (this.plugin.settings.authorName && !this.plugin.settings.authorId) this.plugin.settings.authorId = newId();
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.syncAuthorFilesToName();
|
||||
})), ["author", "name", "identity", "stamp"]));
|
||||
items.push(this.renderDef("Author id (auto-assigned)",
|
||||
"Stable id appended to your name on links so coworkers with the same name don't collide. Generated once and shouldn't change. To reset it, clear and retype your author name.",
|
||||
(s) => s.addText((t) => t.setValue(this.plugin.settings.authorId).setDisabled(true)), ["author", "id"]));
|
||||
items.push(this.renderDef("Title / role",
|
||||
"Optional. Shown on your author page (e.g. \"Engineer\", \"PM\", \"Designer\").",
|
||||
(s) => s.addText((t) => t.setValue(this.plugin.settings.authorRole).onChange(async (v) => {
|
||||
this.plugin.settings.authorRole = v.trim(); await this.plugin.saveSettings(); await this.plugin.syncAuthorFilesToName();
|
||||
})), ["role", "title", "job"]));
|
||||
items.push(this.renderDef("Department / team",
|
||||
"Optional. Shown on your author page (e.g. \"Engineering\", \"Growth\").",
|
||||
(s) => s.addText((t) => t.setValue(this.plugin.settings.authorDepartment).onChange(async (v) => {
|
||||
this.plugin.settings.authorDepartment = v.trim(); await this.plugin.saveSettings(); await this.plugin.syncAuthorFilesToName();
|
||||
})), ["department", "team"]));
|
||||
const footerToggle = (name: string, get: () => boolean, put: (v: boolean) => void, aliases: string[]): SettingDefinitionItem =>
|
||||
this.renderDef(name, "", (s) => s.addToggle((t) => t.setValue(get()).onChange(async (v) => { put(v); await this.plugin.saveSettings(); })), aliases);
|
||||
items.push(footerToggle("Show author in note footer", () => this.plugin.settings.showAuthor, (v) => { this.plugin.settings.showAuthor = v; }, ["author", "footer", "show"]));
|
||||
items.push(footerToggle("Show contributors in note footer", () => this.plugin.settings.showContributors, (v) => { this.plugin.settings.showContributors = v; }, ["contributors", "footer", "show"]));
|
||||
items.push(footerToggle("Show last edit time in note footer", () => this.plugin.settings.showLastEdit, (v) => { this.plugin.settings.showLastEdit = v; }, ["last edit", "modified", "footer", "time"]));
|
||||
items.push(this.sectionDef("Folders you've worked in",
|
||||
"Folders where you've authored or contributed notes. Click one to open it.",
|
||||
(host) => this.renderAuthoredFolders(host),
|
||||
["folders", "authored", "contributed", "worked"]));
|
||||
items.push(this.sectionDef("Known authors",
|
||||
"Everyone the plugin has seen, with role/department + rename history; rebuild/restore the registry.",
|
||||
(host) => this.renderKnownAuthorsSection(host),
|
||||
["authors", "registry", "rename", "known", "rebuild"]));
|
||||
return items;
|
||||
}
|
||||
|
||||
/** The "folders you've worked in" list, extracted so the authorship sectionDef
|
||||
* can render it fresh at display time. */
|
||||
private renderAuthoredFolders(parent: HTMLElement): void {
|
||||
const folders = this.plugin.collectAuthoredFolders();
|
||||
if (folders.length === 0) { parent.createEl("p", { cls: "setting-item-description", text: "No authored or contributed folders yet." }); return; }
|
||||
const list = parent.createDiv({ cls: "stashpad-authored-folders-list" });
|
||||
for (const f of folders) {
|
||||
const row = list.createDiv({ cls: "stashpad-authored-folder-row" });
|
||||
const a = row.createEl("a", { cls: "stashpad-authored-folder-link", text: f.folder });
|
||||
a.onclick = (e) => { e.preventDefault(); void this.plugin.activateViewForFolder(f.folder); };
|
||||
const counts: string[] = [];
|
||||
if (f.authored > 0) counts.push(`authored ${f.authored}`);
|
||||
if (f.contributed > 0) counts.push(`contributed to ${f.contributed}`);
|
||||
row.createSpan({ cls: "stashpad-authored-folder-counts", text: ` · ${counts.join(", ")}` });
|
||||
}
|
||||
}
|
||||
|
||||
/** 0.99.15: Templates tab — the two per-folder editors as searchable sections. */
|
||||
private templatesItems(): SettingDefinitionItem[] {
|
||||
return [
|
||||
this.sectionDef("Color aliases",
|
||||
"Give your note colors friendly names, per Stashpad folder.",
|
||||
(host) => this.renderColorAliasesSection(host),
|
||||
["color", "colour", "alias", "name", "swatch", "palette", "label"]),
|
||||
this.sectionDef("Note templates",
|
||||
"Per-Stashpad note templates — content stamped into new notes.",
|
||||
(host) => this.renderNoteTemplatesSection(host),
|
||||
["template", "note", "default", "boilerplate", "snippet"]),
|
||||
];
|
||||
}
|
||||
|
||||
/** 0.99.15: JD Index tab as a searchable section (scope/preview/build inside). */
|
||||
private jdIndexItems(): SettingDefinitionItem[] {
|
||||
return [
|
||||
this.sectionDef("JD Index (Johnny Decimal)",
|
||||
"Build a Johnny-Decimal-style index from dotted-prefix note titles — set the scope, preview, then build.",
|
||||
(host) => this.renderJdIndexSection(host),
|
||||
["jd", "johnny", "decimal", "index", "scope", "build", "preview", "hierarchy", "folder"]),
|
||||
];
|
||||
}
|
||||
|
||||
private renderAuthorshipSection(parent: HTMLElement): void {
|
||||
parent.createEl("h3", { text: "Authorship" });
|
||||
parent.createEl("p", {
|
||||
|
|
|
|||
|
|
@ -7889,7 +7889,7 @@ export class StashpadView extends ItemView {
|
|||
const current = curFm && (typeof curFm.due === "string" || typeof curFm.due === "number") ? String(curFm.due) : null;
|
||||
// 0.78.1: offer known authors (registry, newest-first) for assignment,
|
||||
// and pre-fill any assignees already on the first target.
|
||||
const knownAuthors = this.plugin.authorRegistry.all().map((a) => ({ id: a.id, name: a.name }));
|
||||
const knownAuthors = this.plugin.collectKnownAuthors();
|
||||
const currentAssignees = parseAssignees(curFm ?? {});
|
||||
new DueDatePickerModal(this.app, current, (result) => {
|
||||
void this.applyDue(targets, result.iso, result.assignees);
|
||||
|
|
@ -7984,7 +7984,7 @@ export class StashpadView extends ItemView {
|
|||
if (targets.length === 0) { new Notice("Nothing to assign."); return; }
|
||||
const first = targets[0];
|
||||
const curFm = first.file ? this.app.metadataCache.getFileCache(first.file)?.frontmatter as any : null;
|
||||
const knownAuthors = this.plugin.authorRegistry.all().map((a) => ({ id: a.id, name: a.name }));
|
||||
const knownAuthors = this.plugin.collectKnownAuthors();
|
||||
const currentAssignees = parseAssignees(curFm ?? {});
|
||||
new AssignModal(this.app, { knownAuthors, currentAssignees }, (assignees) => {
|
||||
void this.applyAssignees(targets, assignees);
|
||||
|
|
|
|||
13
styles.css
13
styles.css
|
|
@ -4077,6 +4077,19 @@
|
|||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
/* 0.99.18: explicit selection + focus highlight so the auto-selected HH/MM text
|
||||
reads clearly in BOTH the search "when" popover AND the due-date modal — the
|
||||
modal context was swallowing the default text-selection highlight. Same class
|
||||
in both pickers, so this fixes both. */
|
||||
.stashpad-when-time-field::selection {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
.stashpad-when-time-field:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 2px var(--interactive-accent);
|
||||
}
|
||||
.stashpad-when-time-colon { font-size: 1.15em; color: var(--text-muted); }
|
||||
.stashpad-when-time-period {
|
||||
display: inline-flex;
|
||||
|
|
|
|||
Loading…
Reference in a new issue