The Share dialog lists people and groups in one picker and shows a group share as its name and member count. Settings gains a Groups section for the admin: create, rename, delete, and add or remove people. The core client reads the directory's groups and group shares (ShareTarget: a member or a group); the desktop command takes user_id or group_id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
5.4 KiB
TypeScript
110 lines
5.4 KiB
TypeScript
// Offline desktop implementation of the repository seam: maps each operation to a
|
|
// Tauri command backed by the on-device SQLite store (desktop/src-tauri/src/local).
|
|
// Selected by index.ts when running inside the desktop shell with no server.
|
|
//
|
|
// Argument keys are camelCase; Tauri converts them to the Rust commands' snake_case
|
|
// parameters (e.g. labelIds -> label_ids). A few operations have no offline meaning
|
|
// yet (account auth, device linking, URL unfurl) — those reject with a clear message
|
|
// rather than silently failing; the board, editor, attachments, capture, filters,
|
|
// labels, checklists, reminders, import and export all work fully offline.
|
|
|
|
import { invoke } from "../desktop/bridge";
|
|
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 { DeviceToken, Directory, ImportResult, NoteShare, Repo } from "./repo";
|
|
|
|
const NEEDS_SERVER = "That's not available offline — connect a server to use it.";
|
|
|
|
export const local: Repo = {
|
|
config: {
|
|
get: () => invoke<PublicConfig>("config_get"),
|
|
},
|
|
|
|
auth: {
|
|
me: () => invoke<User>("auth_me"),
|
|
login: () => Promise.reject(new Error("You're offline — there's no account to sign in to.")),
|
|
register: () => Promise.reject(new Error("You're offline — accounts are created on a server.")),
|
|
logout: () => Promise.resolve(),
|
|
},
|
|
|
|
devices: {
|
|
list: () => Promise.resolve<Device[]>([]),
|
|
create: () => Promise.reject<DeviceToken>(new Error(NEEDS_SERVER)),
|
|
remove: () => Promise.resolve(),
|
|
},
|
|
|
|
labels: {
|
|
list: () => invoke<Label[]>("labels_list"),
|
|
create: (name) => invoke<Label>("labels_create", { name }),
|
|
rename: (id, name) => invoke<Label>("labels_rename", { id, name }),
|
|
setColor: (id, color) => invoke<Label>("labels_set_color", { id, color }),
|
|
remove: (id) => invoke<void>("labels_remove", { id }),
|
|
merge: (sourceId, into) => invoke<Label>("labels_merge", { sourceId, into }),
|
|
},
|
|
|
|
notes: {
|
|
list: (query) => invoke<Note[]>("notes_list", { query }),
|
|
get: (id) => invoke<Note>("notes_get", { id }),
|
|
create: (input) => invoke<Note>("notes_create", { input }),
|
|
update: (id, changes) => invoke<Note>("notes_update", { id, changes }),
|
|
completeReminder: (id) => invoke<Note>("notes_complete_reminder", { id }),
|
|
snoozeReminder: (id, minutes) => invoke<Note>("notes_snooze_reminder", { id, minutes }),
|
|
setLabels: (id, labelIds) => invoke<Note>("notes_set_labels", { id, labelIds }),
|
|
addItem: (id, text) => invoke<Note>("notes_add_item", { id, text }),
|
|
updateItem: (id, itemId, changes) => invoke<Note>("notes_update_item", { id, itemId, changes }),
|
|
deleteItem: (id, itemId) => invoke<Note>("notes_delete_item", { id, itemId }),
|
|
// Kept on this device and uploaded by the next sync once linked (#5168). The bytes
|
|
// go as the raw IPC body; the name is percent-encoded because a header carries
|
|
// only ASCII.
|
|
uploadAttachment: async (id, file) =>
|
|
invoke<Note>("notes_add_attachment", new Uint8Array(await file.arrayBuffer()), {
|
|
headers: {
|
|
"x-note-id": id,
|
|
"x-filename": encodeURIComponent(file.name),
|
|
"x-mime": file.type || "application/octet-stream",
|
|
},
|
|
}),
|
|
deleteAttachment: (id, attId) => invoke<Note>("notes_delete_attachment", { id, attId }),
|
|
unfurl: () => Promise.reject<Note>(new Error(NEEDS_SERVER)),
|
|
deletePreview: (id, previewId) => invoke<Note>("notes_delete_preview", { id, previewId }),
|
|
// The archive crosses as raw bytes, as an attachment does.
|
|
import: async (file) => invoke<ImportResult>("notes_import", new Uint8Array(await file.arrayBuffer())),
|
|
exportAll: () => invoke<string>("notes_export"),
|
|
reorder: (orderedIds) => invoke<void>("notes_reorder", { orderedIds }),
|
|
trash: (id) => invoke<Note>("notes_trash", { id }),
|
|
restore: (id) => invoke<Note>("notes_restore", { id }),
|
|
deleteForever: (id) => invoke<void>("notes_delete_forever", { id }),
|
|
revisions: (id) => invoke<NoteRevision[]>("notes_revisions", { id }),
|
|
restoreRevision: (id, revId) => invoke<Note>("notes_restore_revision", { id, revId }),
|
|
reminders: () => invoke<Note[]>("notes_reminders"),
|
|
titles: () => invoke<TitleEntry[]>("notes_titles"),
|
|
},
|
|
|
|
savedFilters: {
|
|
list: () => invoke<SavedFilter[]>("saved_filters_list"),
|
|
create: (name, params) => invoke<SavedFilter>("saved_filters_create", { name, params }),
|
|
remove: (id) => invoke<void>("saved_filters_remove", { id }),
|
|
rename: (id, name) => invoke<SavedFilter>("saved_filters_rename", { id, name }),
|
|
},
|
|
|
|
// Sharing is between accounts on a server, so these go to the linked one (#5175).
|
|
// Unlinked, each rejects with the core's explanation, which the dialog shows.
|
|
shares: {
|
|
directory: () => invoke<Directory>("shares_directory"),
|
|
list: (noteId) => invoke<NoteShare[]>("shares_list", { noteId }),
|
|
share: (noteId, target, permission) =>
|
|
invoke<NoteShare[]>("shares_share", {
|
|
noteId,
|
|
userId: "user_id" in target ? target.user_id : null,
|
|
groupId: "group_id" in target ? target.group_id : null,
|
|
permission,
|
|
}),
|
|
unshare: (noteId, shareId) => invoke<NoteShare[]>("shares_unshare", { noteId, shareId }),
|
|
},
|
|
};
|