diff --git a/bun.lock b/bun.lock index 5e452c9474..878c7c28c1 100644 --- a/bun.lock +++ b/bun.lock @@ -113,15 +113,15 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@f063676", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@f063676", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-dT2Uri9+T8j9SzVoEHxyQ4qnvX9Qen7NANLJPNMCdBP1Sh1iOsSkSqoBX5jkMceRMPtOicrzBN2ZjOModeDxWQ=="], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], - "@appwrite.io/pink-icons-svelte": ["@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", { "peerDependencies": { "svelte": "^4.0.0" } }], + "@appwrite.io/pink-icons-svelte": ["@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", { "peerDependencies": { "svelte": "^4.0.0" } }, "sha512-2HYl/CC2OlfZIR7LzbLXuSPBn0iNkjbnxpaeGCkZ7UNZ/hFeSeeWjDJqTBMdZ8+X95uuZqHx62XPTiE/svuSXQ=="], "@appwrite.io/pink-legacy": ["@appwrite.io/pink-legacy@1.0.3", "", { "dependencies": { "@appwrite.io/pink-icons": "1.0.0", "the-new-css-reset": "^1.11.2" } }, "sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ=="], - "@appwrite.io/pink-svelte": ["@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8dcaa17", { "dependencies": { "@appwrite.io/pink-icons-svelte": "2.0.0-RC.1", "@floating-ui/dom": "^1.6.13", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@tanstack/svelte-virtual": "^3.13.10", "ansicolor": "^2.0.3", "d3": "^7.9.0", "fuse.js": "^7.1.0", "pretty-bytes": "^6.1.1", "shiki": "^1.18.0", "svelte-motion": "^0.12.2", "svelte-sonner": "^0.3.28" }, "peerDependencies": { "svelte": "^4.0.0" } }], + "@appwrite.io/pink-svelte": ["@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8dcaa17", { "dependencies": { "@appwrite.io/pink-icons-svelte": "2.0.0-RC.1", "@floating-ui/dom": "^1.6.13", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@tanstack/svelte-virtual": "^3.13.10", "ansicolor": "^2.0.3", "d3": "^7.9.0", "fuse.js": "^7.1.0", "pretty-bytes": "^6.1.1", "shiki": "^1.18.0", "svelte-motion": "^0.12.2", "svelte-sonner": "^0.3.28" }, "peerDependencies": { "svelte": "^4.0.0" } }, "sha512-rw3zXN7/cUciCnhj0FR8M0H5Db+LYYMaKtPxvOAIMxNTBmStzU8kTw6grqIvdtFu9vybIsjKtIwm9QLHpNDBjA=="], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], diff --git a/src/lib/components/AttributeNote.svelte b/src/lib/components/AttributeNote.svelte new file mode 100644 index 0000000000..3fc0d6a9b7 --- /dev/null +++ b/src/lib/components/AttributeNote.svelte @@ -0,0 +1,375 @@ + + +
+ {#if editing} +
+ +
+ Ctrl+Enter / Cmd+Enter to save · Esc to cancel +
+ {#if note} + + {/if} + + +
+
+
+ {:else if note} + + {:else} + + {/if} +
+ + diff --git a/src/lib/stores/attributeNotes.ts b/src/lib/stores/attributeNotes.ts new file mode 100644 index 0000000000..f0468b9688 --- /dev/null +++ b/src/lib/stores/attributeNotes.ts @@ -0,0 +1,138 @@ +/** + * attributeNotes.ts + * + * Store for persisting developer notes on collection attributes/columns. + * Notes are stored in localStorage because the Appwrite API does not currently + * expose a `notes` field on attribute objects. This is a console-only feature. + * + * Storage key format: appwrite_attribute_notes + * Data shape: Record + * where key = `${databaseId}/${collectionId}/${attributeKey}` + * and value = the note text + * + * Issue: https://github.com/appwrite/appwrite/issues/11945 + */ + +import { browser } from '$app/environment'; +import { writable } from 'svelte/store'; + +const STORAGE_KEY = 'appwrite_attribute_notes'; + +type NotesMap = Record; + +/** + * Build the compound key used to look up a note. + */ +export function buildNoteKey( + databaseId: string, + collectionId: string, + attributeKey: string +): string { + return `${databaseId}/${collectionId}/${attributeKey}`; +} + +/** + * Load notes map from localStorage, returning an empty object on any error. + */ +function isNotesMap(value: unknown): value is NotesMap { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function loadFromStorage(): NotesMap { + if (!browser) return {}; + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return {}; + const parsed: unknown = JSON.parse(raw); + return isNotesMap(parsed) ? parsed : {}; + } catch { + return {}; + } +} + +/** + * Persist notes map to localStorage. + */ +function saveToStorage(notes: NotesMap): void { + if (!browser) return; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(notes)); + } catch { + // Silently ignore storage errors (e.g. private browsing quota) + } +} + +function createAttributeNotesStore() { + const { subscribe, set, update } = writable(loadFromStorage()); + + return { + subscribe, + + /** + * Get the note for a specific attribute. + */ + getNote(databaseId: string, collectionId: string, attributeKey: string): string { + const notes = loadFromStorage(); + return notes[buildNoteKey(databaseId, collectionId, attributeKey)] ?? ''; + }, + + /** + * Save or clear a note for a specific attribute. + */ + setNote( + databaseId: string, + collectionId: string, + attributeKey: string, + note: string + ): void { + update((notes) => { + const key = buildNoteKey(databaseId, collectionId, attributeKey); + const updated = { ...notes }; + + const trimmedNote = note.trim(); + + if (trimmedNote) { + updated[key] = trimmedNote; + } else { + delete updated[key]; + } + saveToStorage(updated); + return updated; + }); + }, + + /** + * Delete a note for a specific attribute (e.g. when attribute is deleted). + */ + deleteNote(databaseId: string, collectionId: string, attributeKey: string): void { + update((notes) => { + const key = buildNoteKey(databaseId, collectionId, attributeKey); + const updated = { ...notes }; + delete updated[key]; + saveToStorage(updated); + return updated; + }); + }, + + /** + * Delete all notes for a collection (e.g. when collection is deleted). + */ + deleteCollectionNotes(databaseId: string, collectionId: string): void { + update((notes) => { + const prefix = `${databaseId}/${collectionId}/`; + const updated = Object.fromEntries( + Object.entries(notes).filter(([k]) => !k.startsWith(prefix)) + ); + saveToStorage(updated); + return updated; + }); + }, + + /** Re-sync from localStorage (useful after external changes). */ + reload(): void { + set(loadFromStorage()); + } + }; +} + +export const attributeNotes = createAttributeNotesStore(); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte index 5b4075bc8a..2ea535bc54 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte @@ -1,4 +1,5 @@