web: shared note actions, reminder chips, page header, formatters and settings seam
From the audit (#5179, web half). - NoteActions: share, pin, archive and trash (restore and delete forever when trashed), as both the card and the editor offer them. The editor's history toggle goes in its slot, and it closes on `acted`. - ReminderActions: the Done / 1h / 1d chips on the card and in the editor, which are now the same chips. - PageHeader: the back-to-board header that Settings, Sync and Linked devices each wrote out, now with a `back` icon from the shared set. - notes/datetime: formatShortDateTime (was formatReminder and the editor's revLabel) and formatDateTime (the two `fmt` copies). - notes/colors: labelDotClasses (the sidebar's and the tag manager's labelDot). - Drawer links use exact-active-class instead of route.name ternaries. - BoardView binds its three grids from one gridBinds object. - Settings goes through repo.settings (rest, plus a local adapter that answers "needs a server") and shows load failures through AsyncState. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,15 +4,16 @@ import { useNotesStore } from "../stores/notes";
|
||||
import { errorMessage } from "../api/errors";
|
||||
import { rendersInline } from "../notes/attachments";
|
||||
import Icon from "./Icon.vue";
|
||||
import NoteActions from "./NoteActions.vue";
|
||||
import ReminderActions from "./ReminderActions.vue";
|
||||
import LabelPicker from "./LabelPicker.vue";
|
||||
import LinkPreview from "./LinkPreview.vue";
|
||||
import { fromLocalInput, toLocalInput } from "../notes/datetime";
|
||||
import { formatShortDateTime, fromLocalInput, toLocalInput } from "../notes/datetime";
|
||||
import { takeMorphOrigin } from "../composables/useEditorMorph";
|
||||
import { prefersReducedMotion } from "../composables/useReducedMotion";
|
||||
import type { Note, NoteLabel, NoteRevision } from "../stores/notes";
|
||||
import { labelChipClasses } from "../notes/colors";
|
||||
import { canEditText, isOwnNote } from "../notes/sharing";
|
||||
import { useUiStore } from "../stores/ui";
|
||||
import {
|
||||
afterEnter,
|
||||
type EditorBlock,
|
||||
@@ -119,7 +120,6 @@ const bodyPlaceholder = "Take a note…";
|
||||
// Sharing (#5174). Someone else's note: at `edit` its text and checklist are theirs to
|
||||
// change and nothing else is; at `view` it is read-only. The server enforces both —
|
||||
// this only stops the editor offering what would be refused.
|
||||
const ui = useUiStore();
|
||||
const own = computed(() => isOwnNote(liveNote.value));
|
||||
const editable = computed(() => canEditText(liveNote.value));
|
||||
// On the desktop too: linked, it shares through the server; unlinked, the dialog
|
||||
@@ -427,12 +427,6 @@ async function onRecurrenceChange(e: Event) {
|
||||
if (!id) return;
|
||||
await notes.setRecurrence(id, (e.target as HTMLSelectElement).value || null);
|
||||
}
|
||||
async function completeReminder() {
|
||||
if (noteId.value) await notes.completeReminder(noteId.value);
|
||||
}
|
||||
async function snoozeReminder(minutes: number) {
|
||||
if (noteId.value) await notes.snoozeReminder(noteId.value, minutes);
|
||||
}
|
||||
|
||||
// ---- labels ----
|
||||
async function onLabelsChange(next: NoteLabel[]) {
|
||||
@@ -516,12 +510,6 @@ async function onPaste(e: ClipboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- edit-mode lifecycle actions (pin/archive/trash/restore/delete) ----
|
||||
async function act(fn: () => Promise<void>) {
|
||||
await fn();
|
||||
await finish();
|
||||
}
|
||||
|
||||
// ---- version history (modal edit only) ----
|
||||
const showHistory = ref(false);
|
||||
const revisions = ref<NoteRevision[]>([]);
|
||||
@@ -549,10 +537,6 @@ async function restoreRevisionAt(revId: string) {
|
||||
baseline.value = { body: updated.body };
|
||||
void loadRevisions(); // the pre-restore state became a new revision
|
||||
}
|
||||
function revLabel(iso: string | null): string {
|
||||
if (!iso) return "";
|
||||
return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
||||
}
|
||||
function revPreview(rev: NoteRevision): string {
|
||||
const s = rev.body.trim().replace(/\s+/g, " ");
|
||||
if (!s) return "(empty)";
|
||||
@@ -770,19 +754,7 @@ function revPreview(rev: NoteRevision): string {
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-neutral-300 px-2 py-1 text-neutral-600 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
@click="completeReminder"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
<button type="button" class="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200" @click="snoozeReminder(60)">
|
||||
Snooze 1h
|
||||
</button>
|
||||
<button type="button" class="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200" @click="snoozeReminder(1440)">
|
||||
1d
|
||||
</button>
|
||||
<ReminderActions :note-id="liveNote.id" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -797,7 +769,7 @@ function revPreview(rev: NoteRevision): string {
|
||||
</p>
|
||||
<ul v-else class="flex max-h-40 flex-col gap-0.5 overflow-y-auto">
|
||||
<li v-for="rev in revisions" :key="rev.id" class="flex items-center gap-2 rounded-md px-1 py-1 text-xs">
|
||||
<span class="shrink-0 tabular-nums text-neutral-400">{{ revLabel(rev.created_at) }}</span>
|
||||
<span class="shrink-0 tabular-nums text-neutral-400">{{ formatShortDateTime(rev.created_at) }}</span>
|
||||
<span class="min-w-0 flex-1 truncate text-neutral-600 dark:text-neutral-300">{{ revPreview(rev) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -842,71 +814,20 @@ function revPreview(rev: NoteRevision): string {
|
||||
:model-value="labelList"
|
||||
@update:model-value="onLabelsChange"
|
||||
/>
|
||||
<button
|
||||
v-if="canShare && !liveNote.trashed"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="liveNote.shared ? 'text-brand-700 dark:text-brand' : ''"
|
||||
title="Share"
|
||||
aria-label="Share"
|
||||
@click="ui.openShare(liveNote.id)"
|
||||
>
|
||||
<Icon name="users" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!isCreate && own"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="showHistory ? 'text-brand-700 dark:text-brand' : ''"
|
||||
title="Version history"
|
||||
aria-label="Version history"
|
||||
:aria-pressed="showHistory"
|
||||
@click="toggleHistory"
|
||||
>
|
||||
<Icon name="history" />
|
||||
</button>
|
||||
<!-- Pin and archive are everyone's own on a shared note (#5176); trash is the owner's. -->
|
||||
<template v-if="!isCreate && !liveNote.trashed">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="liveNote.pinned ? 'text-brand-700 dark:text-brand' : ''"
|
||||
:title="liveNote.pinned ? 'Unpin' : 'Pin'"
|
||||
@click="act(() => notes.setPinned(liveNote.id, !liveNote.pinned))"
|
||||
>
|
||||
<Icon name="pin" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:title="liveNote.archived ? 'Unarchive' : 'Archive'"
|
||||
@click="act(() => notes.setArchived(liveNote.id, !liveNote.archived))"
|
||||
>
|
||||
<Icon name="archive" />
|
||||
</button>
|
||||
<NoteActions v-if="!isCreate" :note="liveNote" :own="own" :can-share="canShare" @acted="finish">
|
||||
<button
|
||||
v-if="own"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Move to trash"
|
||||
@click="act(() => notes.trash(liveNote.id))"
|
||||
:class="showHistory ? 'text-brand-700 dark:text-brand' : ''"
|
||||
title="Version history"
|
||||
aria-label="Version history"
|
||||
:aria-pressed="showHistory"
|
||||
@click="toggleHistory"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
<Icon name="history" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="!isCreate && liveNote.trashed">
|
||||
<button type="button" class="icon-btn" title="Restore" @click="act(() => notes.restore(liveNote.id))">
|
||||
<Icon name="restore" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Delete forever"
|
||||
@click="act(() => notes.deleteForever(liveNote.id))"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
</NoteActions>
|
||||
<button
|
||||
type="button"
|
||||
:class="
|
||||
|
||||
Reference in New Issue
Block a user