desktop M10.3: frontend data-source adapter seam (repo interface + rest.ts)
Extract a typed repository interface (adapters/repo.ts) from the scattered store/view -> api.* calls, backed by adapters/rest.ts (verbatim HTTP mapping) and selected through adapters/index.ts. Every store and the notes-facing views now depend on `repo`, never the HTTP client directly -- the seam the offline local source (M10.5, over Tauri invoke) plugs into next. Behavior-preserving for web: rest.ts maps each semantic method to the exact endpoint the code called before; query-string and multipart building moved out of the stores/views into rest.ts (the one place that knows the URL shape). Client-side logic (reconcile/sort/optimistic reorder/toasts) stays in the stores. GraphView + admin SettingsView keep direct api calls -- out of the offline-core scope (M10.5 is board/editor/capture/search/filter/labels/ checklists/reminders). Task 1992. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
// The data source the whole app uses. Import `repo` from here — never reach for
|
||||||
|
// `api/client` in stores/views.
|
||||||
|
//
|
||||||
|
// Today it always resolves to the REST source. M10.5 adds the offline desktop
|
||||||
|
// source (`local.ts`, over Tauri `invoke`) and flips this to a runtime choice:
|
||||||
|
//
|
||||||
|
// import { isDesktop } from "../desktop/bridge";
|
||||||
|
// import { local } from "./local";
|
||||||
|
// export const repo: Repo = isDesktop() && !hasConfiguredServer() ? local : rest;
|
||||||
|
//
|
||||||
|
// so a fresh desktop launch runs fully offline, and a server-connected one keeps
|
||||||
|
// using REST. Web is unaffected.
|
||||||
|
|
||||||
|
import { rest } from "./rest";
|
||||||
|
import type { Repo } from "./repo";
|
||||||
|
|
||||||
|
export const repo: Repo = rest;
|
||||||
|
|
||||||
|
export type { Repo } from "./repo";
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// The data-source seam. Stores/views talk to this typed repository interface
|
||||||
|
// instead of reaching for the HTTP client directly, so the SAME UI can run
|
||||||
|
// against the REST backend (web + a server-connected desktop) or a fully-local
|
||||||
|
// on-device source (offline desktop). `rest.ts` implements it over `api/client`;
|
||||||
|
// `local.ts` (M10.5) implements it over Tauri `invoke`; `index.ts` picks one.
|
||||||
|
//
|
||||||
|
// Keep this interface a thin, semantic mirror of the current calls: every method
|
||||||
|
// maps 1:1 to a backend operation and returns the same shape the stores already
|
||||||
|
// consume. Client-side logic (list reconciliation, optimistic updates, toasts)
|
||||||
|
// stays in the stores — the repo is data access only.
|
||||||
|
|
||||||
|
import type { NoteColor } from "../notes/colors";
|
||||||
|
import type { Note, NoteFacets, NoteView, NoteKind, NoteRevision } from "../stores/notes";
|
||||||
|
import type { Label } from "../stores/labels";
|
||||||
|
import type { SavedFilter } from "../stores/savedFilters";
|
||||||
|
import type { Device } from "../stores/devices";
|
||||||
|
import type { TitleEntry } from "../stores/titles";
|
||||||
|
import type { User } from "../stores/session";
|
||||||
|
import type { PublicConfig } from "../stores/config";
|
||||||
|
|
||||||
|
// ---- notes payload shapes ----------------------------------------------------
|
||||||
|
|
||||||
|
// The full board query the GET /api/notes endpoint accepts. `rest.ts` renders it
|
||||||
|
// to a query string (the one place that knows the URL shape); a local source
|
||||||
|
// reads the same fields structurally.
|
||||||
|
export interface NoteListQuery {
|
||||||
|
view: NoteView;
|
||||||
|
labelId?: string | null;
|
||||||
|
facets?: NoteFacets;
|
||||||
|
// The timeline view sorts by creation instead of the board's pinned/position order.
|
||||||
|
sort?: "created";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoteCreateInput {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
color: NoteColor;
|
||||||
|
kind?: NoteKind;
|
||||||
|
items?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mutable subset of a note (PATCH /api/notes/:id).
|
||||||
|
export type NoteChanges = Partial<
|
||||||
|
Pick<Note, "title" | "body" | "color" | "kind" | "pinned" | "archived" | "remind_at" | "recurrence">
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface ChecklistItemChanges {
|
||||||
|
text?: string;
|
||||||
|
checked?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Backlink {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportResult {
|
||||||
|
source: string;
|
||||||
|
imported: number;
|
||||||
|
skipped: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceToken {
|
||||||
|
token: string;
|
||||||
|
device: Device;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- per-domain repositories -------------------------------------------------
|
||||||
|
|
||||||
|
export interface ConfigRepo {
|
||||||
|
get(): Promise<PublicConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthRepo {
|
||||||
|
me(): Promise<User>;
|
||||||
|
login(email: string, password: string): Promise<User>;
|
||||||
|
register(email: string, password: string, displayName: string): Promise<User>;
|
||||||
|
logout(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DevicesRepo {
|
||||||
|
list(): Promise<Device[]>;
|
||||||
|
create(name: string): Promise<DeviceToken>;
|
||||||
|
remove(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelsRepo {
|
||||||
|
list(): Promise<Label[]>;
|
||||||
|
create(name: string): Promise<Label>;
|
||||||
|
rename(id: string, name: string): Promise<Label>;
|
||||||
|
setColor(id: string, color: string): Promise<Label>;
|
||||||
|
remove(id: string): Promise<void>;
|
||||||
|
merge(sourceId: string, into: string): Promise<Label>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotesRepo {
|
||||||
|
list(query: NoteListQuery): Promise<Note[]>;
|
||||||
|
get(id: string): Promise<Note>;
|
||||||
|
create(input: NoteCreateInput): Promise<Note>;
|
||||||
|
createTitled(title: string): Promise<Note>;
|
||||||
|
update(id: string, changes: NoteChanges): Promise<Note>;
|
||||||
|
completeReminder(id: string): Promise<Note>;
|
||||||
|
snoozeReminder(id: string, minutes: number): Promise<Note>;
|
||||||
|
setLabels(id: string, labelIds: string[]): Promise<Note>;
|
||||||
|
addItem(id: string, text: string): Promise<Note>;
|
||||||
|
updateItem(id: string, itemId: string, changes: ChecklistItemChanges): Promise<Note>;
|
||||||
|
deleteItem(id: string, itemId: string): Promise<Note>;
|
||||||
|
uploadAttachment(id: string, file: File): Promise<Note>;
|
||||||
|
deleteAttachment(id: string, attId: string): Promise<Note>;
|
||||||
|
unfurl(id: string, url: string): Promise<Note>;
|
||||||
|
deletePreview(id: string, previewId: string): Promise<Note>;
|
||||||
|
import(file: File): Promise<ImportResult>;
|
||||||
|
reorder(orderedIds: string[]): Promise<void>;
|
||||||
|
trash(id: string): Promise<Note>;
|
||||||
|
restore(id: string): Promise<Note>;
|
||||||
|
deleteForever(id: string): Promise<void>;
|
||||||
|
revisions(id: string): Promise<NoteRevision[]>;
|
||||||
|
restoreRevision(id: string, revId: string): Promise<Note>;
|
||||||
|
reminders(): Promise<Note[]>;
|
||||||
|
titles(): Promise<TitleEntry[]>;
|
||||||
|
search(q: string): Promise<Note[]>;
|
||||||
|
backlinks(id: string): Promise<Backlink[]>;
|
||||||
|
linkSearch(q: string): Promise<TitleEntry[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedFiltersRepo {
|
||||||
|
list(): Promise<SavedFilter[]>;
|
||||||
|
create(name: string, params: NoteFacets): Promise<SavedFilter>;
|
||||||
|
remove(id: string): Promise<void>;
|
||||||
|
rename(id: string, name: string): Promise<SavedFilter>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Repo {
|
||||||
|
config: ConfigRepo;
|
||||||
|
auth: AuthRepo;
|
||||||
|
devices: DevicesRepo;
|
||||||
|
labels: LabelsRepo;
|
||||||
|
notes: NotesRepo;
|
||||||
|
savedFilters: SavedFiltersRepo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
// REST implementation of the repository seam: maps each semantic operation to the
|
||||||
|
// existing HTTP endpoint via `api/client`. This is the ONLY place that knows about
|
||||||
|
// URL paths, query strings, and multipart bodies. Behaviour here must stay a
|
||||||
|
// verbatim mirror of the calls the stores/views made before the seam existed — the
|
||||||
|
// web app is unchanged; the offline `local.ts` source (M10.5) is the alternative.
|
||||||
|
|
||||||
|
import { api } from "../api/client";
|
||||||
|
import type { Note, NoteRevision } from "../stores/notes";
|
||||||
|
import type { Label } from "../stores/labels";
|
||||||
|
import type { SavedFilter } from "../stores/savedFilters";
|
||||||
|
import type { Device } from "../stores/devices";
|
||||||
|
import type { TitleEntry } from "../stores/titles";
|
||||||
|
import type { User } from "../stores/session";
|
||||||
|
import type { PublicConfig } from "../stores/config";
|
||||||
|
import type {
|
||||||
|
Backlink,
|
||||||
|
DeviceToken,
|
||||||
|
ImportResult,
|
||||||
|
NoteChanges,
|
||||||
|
NoteCreateInput,
|
||||||
|
NoteListQuery,
|
||||||
|
ChecklistItemChanges,
|
||||||
|
Repo,
|
||||||
|
} from "./repo";
|
||||||
|
|
||||||
|
// Render a board query to the GET /api/notes query string. Mirrors the param
|
||||||
|
// building that used to live in notes.store.load()/TimelineView (order-preserving;
|
||||||
|
// query-param order is irrelevant to the server, but kept close for review).
|
||||||
|
function notesQuery(q: NoteListQuery): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("filter", q.view);
|
||||||
|
if (q.labelId) params.append("label", q.labelId);
|
||||||
|
for (const id of q.facets?.label ?? []) if (id) params.append("label", id);
|
||||||
|
if (q.facets?.q) params.set("q", q.facets.q);
|
||||||
|
if (q.facets?.color) params.set("color", q.facets.color);
|
||||||
|
if (q.facets?.kind) params.set("kind", q.facets.kind);
|
||||||
|
if (q.facets?.has_reminder) params.set("has_reminder", "true");
|
||||||
|
if (q.facets?.has_attachment) params.set("has_attachment", "true");
|
||||||
|
if (q.facets?.created_after) params.set("created_after", q.facets.created_after);
|
||||||
|
if (q.facets?.created_before) params.set("created_before", q.facets.created_before);
|
||||||
|
if (q.sort) params.set("sort", q.sort);
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileForm(file: File): FormData {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file", file);
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rest: Repo = {
|
||||||
|
config: {
|
||||||
|
get: () => api.get<PublicConfig>("/api/config"),
|
||||||
|
},
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
me: () => api.get<User>("/api/auth/me"),
|
||||||
|
login: (email, password) => api.post<User>("/api/auth/login", { email, password }),
|
||||||
|
register: (email, password, displayName) =>
|
||||||
|
api.post<User>("/api/auth/register", { email, password, display_name: displayName }),
|
||||||
|
logout: () => api.post<void>("/api/auth/logout"),
|
||||||
|
},
|
||||||
|
|
||||||
|
devices: {
|
||||||
|
list: async () => (await api.get<{ devices: Device[] }>("/api/auth/devices")).devices,
|
||||||
|
create: (name) => api.post<DeviceToken>("/api/auth/devices", { name }),
|
||||||
|
remove: (id) => api.del<void>(`/api/auth/devices/${id}`),
|
||||||
|
},
|
||||||
|
|
||||||
|
labels: {
|
||||||
|
list: async () => (await api.get<{ labels: Label[] }>("/api/labels")).labels,
|
||||||
|
create: (name) => api.post<Label>("/api/labels", { name }),
|
||||||
|
rename: (id, name) => api.patch<Label>(`/api/labels/${id}`, { name }),
|
||||||
|
setColor: (id, color) => api.patch<Label>(`/api/labels/${id}`, { color }),
|
||||||
|
remove: (id) => api.del<void>(`/api/labels/${id}`),
|
||||||
|
merge: (sourceId, into) => api.post<Label>(`/api/labels/${sourceId}/merge`, { into }),
|
||||||
|
},
|
||||||
|
|
||||||
|
notes: {
|
||||||
|
list: async (query) => (await api.get<{ notes: Note[] }>(`/api/notes?${notesQuery(query)}`)).notes,
|
||||||
|
get: (id) => api.get<Note>(`/api/notes/${id}`),
|
||||||
|
create: (input: NoteCreateInput) => api.post<Note>("/api/notes", input),
|
||||||
|
createTitled: (title) => api.post<Note>("/api/notes", { title, body: "" }),
|
||||||
|
update: (id, changes: NoteChanges) => api.patch<Note>(`/api/notes/${id}`, changes),
|
||||||
|
completeReminder: (id) => api.post<Note>(`/api/notes/${id}/reminder/complete`),
|
||||||
|
snoozeReminder: (id, minutes) => api.post<Note>(`/api/notes/${id}/reminder/snooze`, { minutes }),
|
||||||
|
setLabels: (id, labelIds) => api.put<Note>(`/api/notes/${id}/labels`, { label_ids: labelIds }),
|
||||||
|
addItem: (id, text) => api.post<Note>(`/api/notes/${id}/items`, { text }),
|
||||||
|
updateItem: (id, itemId, changes: ChecklistItemChanges) =>
|
||||||
|
api.patch<Note>(`/api/notes/${id}/items/${itemId}`, changes),
|
||||||
|
deleteItem: (id, itemId) => api.del<Note>(`/api/notes/${id}/items/${itemId}`),
|
||||||
|
uploadAttachment: (id, file) => api.postForm<Note>(`/api/notes/${id}/attachments`, fileForm(file)),
|
||||||
|
deleteAttachment: (id, attId) => api.del<Note>(`/api/notes/${id}/attachments/${attId}`),
|
||||||
|
unfurl: (id, url) => api.post<Note>(`/api/notes/${id}/unfurl`, { url }),
|
||||||
|
deletePreview: (id, previewId) => api.del<Note>(`/api/notes/${id}/previews/${previewId}`),
|
||||||
|
import: (file) => api.postForm<ImportResult>("/api/notes/import", fileForm(file)),
|
||||||
|
reorder: (orderedIds) => api.post<void>("/api/notes/reorder", { ids: orderedIds }),
|
||||||
|
trash: (id) => api.post<Note>(`/api/notes/${id}/trash`),
|
||||||
|
restore: (id) => api.post<Note>(`/api/notes/${id}/restore`),
|
||||||
|
deleteForever: (id) => api.del<void>(`/api/notes/${id}`),
|
||||||
|
revisions: async (id) => (await api.get<{ revisions: NoteRevision[] }>(`/api/notes/${id}/revisions`)).revisions,
|
||||||
|
restoreRevision: (id, revId) => api.post<Note>(`/api/notes/${id}/revisions/${revId}/restore`),
|
||||||
|
reminders: async () => (await api.get<{ notes: Note[] }>("/api/notes/reminders")).notes,
|
||||||
|
titles: async () => (await api.get<{ titles: TitleEntry[] }>("/api/notes/titles")).titles,
|
||||||
|
search: async (q) => (await api.get<{ notes: Note[] }>(`/api/notes/search?q=${encodeURIComponent(q)}`)).notes,
|
||||||
|
backlinks: async (id) => (await api.get<{ backlinks: Backlink[] }>(`/api/notes/${id}/backlinks`)).backlinks,
|
||||||
|
linkSearch: async (q) =>
|
||||||
|
(await api.get<{ results: TitleEntry[] }>(`/api/notes/link-search?q=${encodeURIComponent(q)}`)).results,
|
||||||
|
},
|
||||||
|
|
||||||
|
savedFilters: {
|
||||||
|
list: async () => (await api.get<{ filters: SavedFilter[] }>("/api/saved-filters")).filters,
|
||||||
|
create: (name, params) => api.post<SavedFilter>("/api/saved-filters", { name, params }),
|
||||||
|
remove: (id) => api.del<void>(`/api/saved-filters/${id}`),
|
||||||
|
rename: (id, name) => api.patch<SavedFilter>(`/api/saved-filters/${id}`, { name }),
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
import { useNotesStore } from "../stores/notes";
|
import { useNotesStore } from "../stores/notes";
|
||||||
import { useConfigStore } from "../stores/config";
|
import { useConfigStore } from "../stores/config";
|
||||||
import { useTitlesStore, type TitleEntry } from "../stores/titles";
|
import { useTitlesStore, type TitleEntry } from "../stores/titles";
|
||||||
@@ -206,8 +206,7 @@ async function loadBacklinks(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await api.get<{ backlinks: { id: string; title: string }[] }>(`/api/notes/${noteId.value}/backlinks`);
|
backlinks.value = await repo.notes.backlinks(noteId.value);
|
||||||
backlinks.value = res.backlinks;
|
|
||||||
} catch {
|
} catch {
|
||||||
backlinks.value = [];
|
backlinks.value = [];
|
||||||
}
|
}
|
||||||
@@ -264,8 +263,8 @@ function refreshLinkMatches() {
|
|||||||
const q = linkQuery.value.trim();
|
const q = linkQuery.value.trim();
|
||||||
linkTimer = setTimeout(async () => {
|
linkTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get<{ results: TitleEntry[] }>(`/api/notes/link-search?q=${encodeURIComponent(q)}`);
|
const results = await repo.notes.linkSearch(q);
|
||||||
linkMatches.value = res.results.filter((r) => r.id !== noteId.value).slice(0, 8);
|
linkMatches.value = results.filter((r) => r.id !== noteId.value).slice(0, 8);
|
||||||
} catch {
|
} catch {
|
||||||
linkMatches.value = [];
|
linkMatches.value = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
|
|
||||||
interface PublicConfig {
|
export interface PublicConfig {
|
||||||
site_name: string;
|
site_name: string;
|
||||||
allow_registration: boolean;
|
allow_registration: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -20,7 +20,7 @@ export const useConfigStore = defineStore("config", () => {
|
|||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
if (loaded.value) return;
|
if (loaded.value) return;
|
||||||
try {
|
try {
|
||||||
const cfg = await api.get<PublicConfig>("/api/config");
|
const cfg = await repo.config.get();
|
||||||
siteName.value = cfg.site_name;
|
siteName.value = cfg.site_name;
|
||||||
allowRegistration.value = cfg.allow_registration;
|
allowRegistration.value = cfg.allow_registration;
|
||||||
version.value = cfg.version;
|
version.value = cfg.version;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
|
|
||||||
// A linked native client (Tauri/Android) that holds a device bearer token.
|
// A linked native client (Tauri/Android) that holds a device bearer token.
|
||||||
export interface Device {
|
export interface Device {
|
||||||
@@ -17,7 +17,7 @@ export const useDevicesStore = defineStore("devices", () => {
|
|||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
items.value = (await api.get<{ devices: Device[] }>("/api/auth/devices")).devices;
|
items.value = await repo.devices.list();
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -26,13 +26,13 @@ export const useDevicesStore = defineStore("devices", () => {
|
|||||||
// Issues a token for the current user; the plaintext token is returned ONCE
|
// Issues a token for the current user; the plaintext token is returned ONCE
|
||||||
// (never retrievable again) for the caller to display + copy.
|
// (never retrievable again) for the caller to display + copy.
|
||||||
async function create(name: string): Promise<string> {
|
async function create(name: string): Promise<string> {
|
||||||
const res = await api.post<{ token: string; device: Device }>("/api/auth/devices", { name });
|
const res = await repo.devices.create(name);
|
||||||
items.value.unshift(res.device);
|
items.value.unshift(res.device);
|
||||||
return res.token;
|
return res.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revoke(id: string): Promise<void> {
|
async function revoke(id: string): Promise<void> {
|
||||||
await api.del(`/api/auth/devices/${id}`);
|
await repo.devices.remove(id);
|
||||||
items.value = items.value.filter((d) => d.id !== id);
|
items.value = items.value.filter((d) => d.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
|
|
||||||
export interface Label {
|
export interface Label {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,13 +19,12 @@ export const useLabelsStore = defineStore("labels", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
const res = await api.get<{ labels: Label[] }>("/api/labels");
|
items.value = await repo.labels.list();
|
||||||
items.value = res.labels;
|
|
||||||
loaded.value = true;
|
loaded.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(name: string): Promise<Label> {
|
async function create(name: string): Promise<Label> {
|
||||||
const label = await api.post<Label>("/api/labels", { name });
|
const label = await repo.labels.create(name);
|
||||||
if (!items.value.some((lb) => lb.id === label.id)) {
|
if (!items.value.some((lb) => lb.id === label.id)) {
|
||||||
items.value.push(label);
|
items.value.push(label);
|
||||||
sort();
|
sort();
|
||||||
@@ -34,7 +33,7 @@ export const useLabelsStore = defineStore("labels", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function rename(id: string, name: string): Promise<void> {
|
async function rename(id: string, name: string): Promise<void> {
|
||||||
const updated = await api.patch<Label>(`/api/labels/${id}`, { name });
|
const updated = await repo.labels.rename(id, name);
|
||||||
const idx = items.value.findIndex((lb) => lb.id === id);
|
const idx = items.value.findIndex((lb) => lb.id === id);
|
||||||
// The single-label PATCH doesn't recompute the count — keep the one we have.
|
// The single-label PATCH doesn't recompute the count — keep the one we have.
|
||||||
if (idx >= 0) items.value[idx] = { ...updated, count: items.value[idx].count };
|
if (idx >= 0) items.value[idx] = { ...updated, count: items.value[idx].count };
|
||||||
@@ -42,20 +41,20 @@ export const useLabelsStore = defineStore("labels", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setColor(id: string, color: string): Promise<void> {
|
async function setColor(id: string, color: string): Promise<void> {
|
||||||
const updated = await api.patch<Label>(`/api/labels/${id}`, { color });
|
const updated = await repo.labels.setColor(id, color);
|
||||||
const idx = items.value.findIndex((lb) => lb.id === id);
|
const idx = items.value.findIndex((lb) => lb.id === id);
|
||||||
if (idx >= 0) items.value[idx] = { ...updated, count: items.value[idx].count };
|
if (idx >= 0) items.value[idx] = { ...updated, count: items.value[idx].count };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(id: string): Promise<void> {
|
async function remove(id: string): Promise<void> {
|
||||||
await api.del(`/api/labels/${id}`);
|
await repo.labels.remove(id);
|
||||||
items.value = items.value.filter((lb) => lb.id !== id);
|
items.value = items.value.filter((lb) => lb.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge `sourceId` into `targetId`: the server moves the source's notes onto the
|
// Merge `sourceId` into `targetId`: the server moves the source's notes onto the
|
||||||
// target and deletes the source; the response carries the target's new count.
|
// target and deletes the source; the response carries the target's new count.
|
||||||
async function mergeInto(sourceId: string, targetId: string): Promise<void> {
|
async function mergeInto(sourceId: string, targetId: string): Promise<void> {
|
||||||
const target = await api.post<Label>(`/api/labels/${sourceId}/merge`, { into: targetId });
|
const target = await repo.labels.merge(sourceId, targetId);
|
||||||
items.value = items.value.filter((lb) => lb.id !== sourceId);
|
items.value = items.value.filter((lb) => lb.id !== sourceId);
|
||||||
const idx = items.value.findIndex((lb) => lb.id === targetId);
|
const idx = items.value.findIndex((lb) => lb.id === targetId);
|
||||||
if (idx >= 0) items.value[idx] = target;
|
if (idx >= 0) items.value[idx] = target;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
import { useUiStore } from "./ui";
|
import { useUiStore } from "./ui";
|
||||||
import type { NoteColor } from "../notes/colors";
|
import type { NoteColor } from "../notes/colors";
|
||||||
|
|
||||||
@@ -127,19 +127,7 @@ export const useNotesStore = defineStore("notes", () => {
|
|||||||
activeFacets.value = facets;
|
activeFacets.value = facets;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
items.value = await repo.notes.list({ view: v, labelId, facets });
|
||||||
params.set("filter", v);
|
|
||||||
if (labelId) params.append("label", labelId);
|
|
||||||
for (const id of facets.label ?? []) if (id) params.append("label", id);
|
|
||||||
if (facets.q) params.set("q", facets.q);
|
|
||||||
if (facets.color) params.set("color", facets.color);
|
|
||||||
if (facets.kind) params.set("kind", facets.kind);
|
|
||||||
if (facets.has_reminder) params.set("has_reminder", "true");
|
|
||||||
if (facets.has_attachment) params.set("has_attachment", "true");
|
|
||||||
if (facets.created_after) params.set("created_after", facets.created_after);
|
|
||||||
if (facets.created_before) params.set("created_before", facets.created_before);
|
|
||||||
const res = await api.get<{ notes: Note[] }>(`/api/notes?${params.toString()}`);
|
|
||||||
items.value = res.notes;
|
|
||||||
sortItems();
|
sortItems();
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -153,7 +141,7 @@ export const useNotesStore = defineStore("notes", () => {
|
|||||||
kind?: NoteKind;
|
kind?: NoteKind;
|
||||||
items?: string[];
|
items?: string[];
|
||||||
}): Promise<Note> {
|
}): Promise<Note> {
|
||||||
const note = await api.post<Note>("/api/notes", input);
|
const note = await repo.notes.create(input);
|
||||||
reconcile(note);
|
reconcile(note);
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
@@ -164,7 +152,7 @@ export const useNotesStore = defineStore("notes", () => {
|
|||||||
Pick<Note, "title" | "body" | "color" | "kind" | "pinned" | "archived" | "remind_at" | "recurrence">
|
Pick<Note, "title" | "body" | "color" | "kind" | "pinned" | "archived" | "remind_at" | "recurrence">
|
||||||
>,
|
>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
reconcile(await api.patch<Note>(`/api/notes/${id}`, changes));
|
reconcile(await repo.notes.update(id, changes));
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPinned = (id: string, pinned: boolean) => mutate(id, { pinned });
|
const setPinned = (id: string, pinned: boolean) => mutate(id, { pinned });
|
||||||
@@ -180,50 +168,46 @@ export const useNotesStore = defineStore("notes", () => {
|
|||||||
const saveEdit = (id: string, changes: { title: string; body: string; color: NoteColor }) => mutate(id, changes);
|
const saveEdit = (id: string, changes: { title: string; body: string; color: NoteColor }) => mutate(id, changes);
|
||||||
|
|
||||||
async function completeReminder(id: string): Promise<void> {
|
async function completeReminder(id: string): Promise<void> {
|
||||||
reconcile(await api.post<Note>(`/api/notes/${id}/reminder/complete`));
|
reconcile(await repo.notes.completeReminder(id));
|
||||||
}
|
}
|
||||||
async function snoozeReminder(id: string, minutes: number): Promise<void> {
|
async function snoozeReminder(id: string, minutes: number): Promise<void> {
|
||||||
reconcile(await api.post<Note>(`/api/notes/${id}/reminder/snooze`, { minutes }));
|
reconcile(await repo.notes.snoozeReminder(id, minutes));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setLabels(id: string, labelIds: string[]): Promise<void> {
|
async function setLabels(id: string, labelIds: string[]): Promise<void> {
|
||||||
reconcile(await api.put<Note>(`/api/notes/${id}/labels`, { label_ids: labelIds }));
|
reconcile(await repo.notes.setLabels(id, labelIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addItem(id: string, text: string): Promise<void> {
|
async function addItem(id: string, text: string): Promise<void> {
|
||||||
reconcile(await api.post<Note>(`/api/notes/${id}/items`, { text }));
|
reconcile(await repo.notes.addItem(id, text));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateItem(id: string, itemId: string, changes: { text?: string; checked?: boolean }): Promise<void> {
|
async function updateItem(id: string, itemId: string, changes: { text?: string; checked?: boolean }): Promise<void> {
|
||||||
reconcile(await api.patch<Note>(`/api/notes/${id}/items/${itemId}`, changes));
|
reconcile(await repo.notes.updateItem(id, itemId, changes));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteItem(id: string, itemId: string): Promise<void> {
|
async function deleteItem(id: string, itemId: string): Promise<void> {
|
||||||
reconcile(await api.del<Note>(`/api/notes/${id}/items/${itemId}`));
|
reconcile(await repo.notes.deleteItem(id, itemId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadAttachment(id: string, file: File): Promise<void> {
|
async function uploadAttachment(id: string, file: File): Promise<void> {
|
||||||
const form = new FormData();
|
reconcile(await repo.notes.uploadAttachment(id, file));
|
||||||
form.append("file", file);
|
|
||||||
reconcile(await api.postForm<Note>(`/api/notes/${id}/attachments`, form));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAttachment(id: string, attId: string): Promise<void> {
|
async function deleteAttachment(id: string, attId: string): Promise<void> {
|
||||||
reconcile(await api.del<Note>(`/api/notes/${id}/attachments/${attId}`));
|
reconcile(await repo.notes.deleteAttachment(id, attId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unfurl(id: string, url: string): Promise<void> {
|
async function unfurl(id: string, url: string): Promise<void> {
|
||||||
reconcile(await api.post<Note>(`/api/notes/${id}/unfurl`, { url }));
|
reconcile(await repo.notes.unfurl(id, url));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deletePreview(id: string, previewId: string): Promise<void> {
|
async function deletePreview(id: string, previewId: string): Promise<void> {
|
||||||
reconcile(await api.del<Note>(`/api/notes/${id}/previews/${previewId}`));
|
reconcile(await repo.notes.deletePreview(id, previewId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importNotes(file: File): Promise<{ source: string; imported: number; skipped: number }> {
|
async function importNotes(file: File): Promise<{ source: string; imported: number; skipped: number }> {
|
||||||
const form = new FormData();
|
const data = await repo.notes.import(file);
|
||||||
form.append("file", file);
|
|
||||||
const data = await api.postForm<{ source: string; imported: number; skipped: number }>("/api/notes/import", form);
|
|
||||||
// Refresh the current lens so imported notes appear (labels reloaded by caller).
|
// Refresh the current lens so imported notes appear (labels reloaded by caller).
|
||||||
await load(view.value, activeLabel.value, activeFacets.value);
|
await load(view.value, activeLabel.value, activeFacets.value);
|
||||||
return data;
|
return data;
|
||||||
@@ -231,14 +215,14 @@ export const useNotesStore = defineStore("notes", () => {
|
|||||||
|
|
||||||
async function fetchOne(id: string): Promise<Note | null> {
|
async function fetchOne(id: string): Promise<Note | null> {
|
||||||
try {
|
try {
|
||||||
return await api.get<Note>(`/api/notes/${id}`);
|
return await repo.notes.get(id);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTitled(title: string): Promise<Note> {
|
async function createTitled(title: string): Promise<Note> {
|
||||||
const created = await api.post<Note>("/api/notes", { title, body: "" });
|
const created = await repo.notes.createTitled(title);
|
||||||
reconcile(created);
|
reconcile(created);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
@@ -252,31 +236,30 @@ export const useNotesStore = defineStore("notes", () => {
|
|||||||
if (n) n.position = total - index;
|
if (n) n.position = total - index;
|
||||||
});
|
});
|
||||||
sortItems();
|
sortItems();
|
||||||
await api.post("/api/notes/reorder", { ids: orderedIds });
|
await repo.notes.reorder(orderedIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function trash(id: string): Promise<void> {
|
async function trash(id: string): Promise<void> {
|
||||||
reconcile(await api.post<Note>(`/api/notes/${id}/trash`));
|
reconcile(await repo.notes.trash(id));
|
||||||
useUiStore().showToast("Note moved to trash", { label: "Undo", run: () => void restore(id) });
|
useUiStore().showToast("Note moved to trash", { label: "Undo", run: () => void restore(id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restore(id: string): Promise<void> {
|
async function restore(id: string): Promise<void> {
|
||||||
reconcile(await api.post<Note>(`/api/notes/${id}/restore`));
|
reconcile(await repo.notes.restore(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteForever(id: string): Promise<void> {
|
async function deleteForever(id: string): Promise<void> {
|
||||||
await api.del(`/api/notes/${id}`);
|
await repo.notes.deleteForever(id);
|
||||||
const idx = items.value.findIndex((n) => n.id === id);
|
const idx = items.value.findIndex((n) => n.id === id);
|
||||||
if (idx >= 0) items.value.splice(idx, 1);
|
if (idx >= 0) items.value.splice(idx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRevisions(id: string): Promise<NoteRevision[]> {
|
async function fetchRevisions(id: string): Promise<NoteRevision[]> {
|
||||||
const res = await api.get<{ revisions: NoteRevision[] }>(`/api/notes/${id}/revisions`);
|
return await repo.notes.revisions(id);
|
||||||
return res.revisions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreRevision(id: string, revId: string): Promise<Note> {
|
async function restoreRevision(id: string, revId: string): Promise<Note> {
|
||||||
const note = await api.post<Note>(`/api/notes/${id}/revisions/${revId}/restore`);
|
const note = await repo.notes.restoreRevision(id, revId);
|
||||||
reconcile(note);
|
reconcile(note);
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
import router from "../router";
|
import router from "../router";
|
||||||
import { useUiStore } from "./ui";
|
import { useUiStore } from "./ui";
|
||||||
import type { Note } from "./notes";
|
import type { Note } from "./notes";
|
||||||
@@ -23,7 +23,7 @@ export const useReminderStore = defineStore("reminders", () => {
|
|||||||
// Single owner of the reminders endpoint — the RemindersView reads its list through
|
// Single owner of the reminders endpoint — the RemindersView reads its list through
|
||||||
// this too, so the URL + response shape live in one place.
|
// this too, so the URL + response shape live in one place.
|
||||||
async function fetchReminders(): Promise<Note[]> {
|
async function fetchReminders(): Promise<Note[]> {
|
||||||
return (await api.get<{ notes: Note[] }>("/api/notes/reminders")).notes;
|
return repo.notes.reminders();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function check(): Promise<void> {
|
async function check(): Promise<void> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
import type { NoteFacets } from "./notes";
|
import type { NoteFacets } from "./notes";
|
||||||
|
|
||||||
// A named, saved facet combination (a 'view'/lens). `params` mirrors NoteFacets.
|
// A named, saved facet combination (a 'view'/lens). `params` mirrors NoteFacets.
|
||||||
@@ -17,7 +17,7 @@ export const useSavedFiltersStore = defineStore("savedFilters", () => {
|
|||||||
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
items.value = (await api.get<{ filters: SavedFilter[] }>("/api/saved-filters")).filters;
|
items.value = await repo.savedFilters.list();
|
||||||
} catch {
|
} catch {
|
||||||
// leave whatever we have
|
// leave whatever we have
|
||||||
} finally {
|
} finally {
|
||||||
@@ -26,18 +26,18 @@ export const useSavedFiltersStore = defineStore("savedFilters", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function create(name: string, params: NoteFacets): Promise<SavedFilter> {
|
async function create(name: string, params: NoteFacets): Promise<SavedFilter> {
|
||||||
const sf = await api.post<SavedFilter>("/api/saved-filters", { name, params });
|
const sf = await repo.savedFilters.create(name, params);
|
||||||
items.value.push(sf);
|
items.value.push(sf);
|
||||||
return sf;
|
return sf;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(id: string): Promise<void> {
|
async function remove(id: string): Promise<void> {
|
||||||
await api.del(`/api/saved-filters/${id}`);
|
await repo.savedFilters.remove(id);
|
||||||
items.value = items.value.filter((f) => f.id !== id);
|
items.value = items.value.filter((f) => f.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rename(id: string, name: string): Promise<void> {
|
async function rename(id: string, name: string): Promise<void> {
|
||||||
const sf = await api.patch<SavedFilter>(`/api/saved-filters/${id}`, { name });
|
const sf = await repo.savedFilters.rename(id, name);
|
||||||
const idx = items.value.findIndex((f) => f.id === id);
|
const idx = items.value.findIndex((f) => f.id === id);
|
||||||
if (idx >= 0) items.value[idx] = sf;
|
if (idx >= 0) items.value[idx] = sf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,7 +17,7 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
|
|
||||||
async function fetchMe(): Promise<void> {
|
async function fetchMe(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
user.value = await api.get<User>("/api/auth/me");
|
user.value = await repo.auth.me();
|
||||||
} catch {
|
} catch {
|
||||||
user.value = null;
|
user.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -26,19 +26,15 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login(email: string, password: string): Promise<void> {
|
async function login(email: string, password: string): Promise<void> {
|
||||||
user.value = await api.post<User>("/api/auth/login", { email, password });
|
user.value = await repo.auth.login(email, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function register(email: string, password: string, displayName: string): Promise<void> {
|
async function register(email: string, password: string, displayName: string): Promise<void> {
|
||||||
user.value = await api.post<User>("/api/auth/register", {
|
user.value = await repo.auth.register(email, password, displayName);
|
||||||
email,
|
|
||||||
password,
|
|
||||||
display_name: displayName,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout(): Promise<void> {
|
async function logout(): Promise<void> {
|
||||||
await api.post("/api/auth/logout");
|
await repo.auth.logout();
|
||||||
user.value = null;
|
user.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
|
|
||||||
export interface TitleEntry {
|
export interface TitleEntry {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -14,8 +14,7 @@ export const useTitlesStore = defineStore("titles", () => {
|
|||||||
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
if (loaded.value) return;
|
if (loaded.value) return;
|
||||||
const res = await api.get<{ titles: TitleEntry[] }>("/api/notes/titles");
|
items.value = await repo.notes.titles();
|
||||||
items.value = res.titles;
|
|
||||||
loaded.value = true;
|
loaded.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch } from "vue";
|
import { computed, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
import { type Note } from "../stores/notes";
|
|
||||||
import { useNoteList } from "../composables/useNoteList";
|
import { useNoteList } from "../composables/useNoteList";
|
||||||
import { useNoteEditor } from "../composables/useNoteEditor";
|
import { useNoteEditor } from "../composables/useNoteEditor";
|
||||||
import AsyncState from "../components/AsyncState.vue";
|
import AsyncState from "../components/AsyncState.vue";
|
||||||
@@ -17,7 +16,7 @@ const noMatchSubtitle = computed(() => `Nothing found for "${query.value}".`);
|
|||||||
const { items: results, loading, error, load: run } = useNoteList(async () => {
|
const { items: results, loading, error, load: run } = useNoteList(async () => {
|
||||||
const q = query.value.trim();
|
const q = query.value.trim();
|
||||||
if (!q) return [];
|
if (!q) return [];
|
||||||
return (await api.get<{ notes: Note[] }>(`/api/notes/search?q=${encodeURIComponent(q)}`)).notes;
|
return repo.notes.search(q);
|
||||||
}, "Search failed.");
|
}, "Search failed.");
|
||||||
|
|
||||||
const { editing, open: openEditor, close: closeEditor, navigate: onNavigate } = useNoteEditor({
|
const { editing, open: openEditor, close: closeEditor, navigate: onNavigate } = useNoteEditor({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { api } from "../api/client";
|
import { repo } from "../adapters";
|
||||||
|
import type { NoteListQuery } from "../adapters/repo";
|
||||||
import { type Note } from "../stores/notes";
|
import { type Note } from "../stores/notes";
|
||||||
import { addLocalDays, parseLocalDate } from "../notes/datetime";
|
import { addLocalDays, parseLocalDate } from "../notes/datetime";
|
||||||
import { useNoteList } from "../composables/useNoteList";
|
import { useNoteList } from "../composables/useNoteList";
|
||||||
@@ -19,18 +20,22 @@ const fromDate = ref("");
|
|||||||
const toDate = ref("");
|
const toDate = ref("");
|
||||||
const hasRange = computed(() => !!fromDate.value || !!toDate.value);
|
const hasRange = computed(() => !!fromDate.value || !!toDate.value);
|
||||||
|
|
||||||
function buildQuery(): string {
|
function buildQuery(): NoteListQuery {
|
||||||
const params = new URLSearchParams({ filter: "active", sort: "created" });
|
|
||||||
const from = parseLocalDate(fromDate.value);
|
const from = parseLocalDate(fromDate.value);
|
||||||
if (from) params.set("created_after", from.toISOString());
|
|
||||||
const to = parseLocalDate(toDate.value);
|
const to = parseLocalDate(toDate.value);
|
||||||
// Half-open upper bound: start of the day AFTER `to`, so the whole `to` day is included.
|
return {
|
||||||
if (to) params.set("created_before", addLocalDays(to, 1).toISOString());
|
view: "active",
|
||||||
return params.toString();
|
sort: "created",
|
||||||
|
facets: {
|
||||||
|
created_after: from ? from.toISOString() : undefined,
|
||||||
|
// Half-open upper bound: start of the day AFTER `to`, so the whole `to` day is included.
|
||||||
|
created_before: to ? addLocalDays(to, 1).toISOString() : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { items, loading, error, load } = useNoteList(
|
const { items, loading, error, load } = useNoteList(
|
||||||
async () => (await api.get<{ notes: Note[] }>(`/api/notes?${buildQuery()}`)).notes,
|
async () => repo.notes.list(buildQuery()),
|
||||||
"Couldn't load the timeline.",
|
"Couldn't load the timeline.",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user