From 26c926c0210c38f56e4af35646b6d27e7b78a87d Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 30 Oct 2026 16:21:26 -0500 Subject: [PATCH 1/5] feat(mobile): adopt core 2.80.0, the template folder, subtasks on the board and Open as Gallery Core core-2.80.0-core.hc5a4589d040cd0da, built by CI from ZenNotes/zennotes 31520814 (the v2.80.0 tag), replaces h7896a01910a2ea0f (47d26827); core:adopt checked SHA-256, SHA-512, packed versions and clean-source provenance. The lock moves only the three @zennotes entries: npm 11 also flipped the build tools' dev flags (esbuild, rolldown, lightningcss), which would put them under the production audit, so those lines are kept as they were and npm ci accepts the result. New for the phone, from core: Kanban swimlanes in the board's View menu; a task's indented subtasks inside its card, with a 2/3 progress bar and boxes to tick; WIP limits; empty - [ ] placeholders kept out of Tasks, the Calendar sheet and the widgets' data, with a quiet "3 hidden"; and a template folder (Settings, Templates) whose notes show in the template picker. The on-device vault mirrors desktop vault.ts for the template folder, as it does for the Tasks folder lists: a settings save from this device keeps a desktop user's templates.folder through shared-domain's normalizeTemplateFolderSettings, exactly as desktop main does (a usable path cleaned, only the folder kept, an unusable one dropped with no stub left; the Android shell does the same); renaming or moving the folder, or one it sits in, carries the setting along (rewriteTemplateFolderForRename); the task scans take their folder lists from tasksFolderListsForSettings, so a template's checkboxes never become tasks, even inside a Task folder; and text search drops template notes before ranking, so the result limit is spent on real notes. A self-hosted vault gets the same from server 2.80, which core asks about itself (supportsTemplateFolder). Subtasks need nothing here: the scans already parse with shared-domain's parseTasksFromBody and parseTaskFile, which now set parentId. A test runs the core's contract fixture (bridge-contract/fixtures/task-subtasks.json) through both scans. The drawer's folder sheet gains Open as Gallery, the desktop folder menu's row, through core 2.80's openBrowseFolderGallery; the drawer closes onto the folder's cards. A database keeps its own grid, as on desktop. Core 2.80 now leaves a phone's keyboard down after a callout pick, Remove callout and the database Columns menu opening (hostOpensNotesToRead), the three places read-open.ts guards since 1.34.0. The contract test checks core's new gates; read-open.ts keeps its guards as a second line, unchanged and still the Android shell's file byte for byte, until both shells drop them together. --- README.md | 4 +- package-lock.json | 30 +-- package.json | 6 +- src/bridge/template-folder.test.ts | 197 ++++++++++++++++++ src/bridge/vault-fs.ts | 37 +++- src/ui-mobile/MobileDrawer.tsx | 20 +- src/ui-mobile/gallery-phone.test.ts | 12 ++ src/ui-mobile/phone-focus-guards.test.ts | 14 +- vendor/zennotes/manifest.json | 32 +-- ...pp-core-2.80.0-core.hc5a4589d040cd0da.tgz} | Bin 5423515 -> 5476290 bytes ...ct-2.79.0-boundaries.h28492494a5dfa837.tgz | Bin 46492 -> 0 bytes ...ct-2.80.0-boundaries.hc07ef9e2bef31a64.tgz | Bin 0 -> 54542 bytes ...in-2.79.0-boundaries.h28492494a5dfa837.tgz | Bin 387103 -> 0 bytes ...in-2.80.0-boundaries.hc07ef9e2bef31a64.tgz | Bin 0 -> 412714 bytes 14 files changed, 303 insertions(+), 49 deletions(-) create mode 100644 src/bridge/template-folder.test.ts rename vendor/zennotes/{zennotes-app-core-2.79.0-core.h7896a01910a2ea0f.tgz => zennotes-app-core-2.80.0-core.hc5a4589d040cd0da.tgz} (52%) delete mode 100644 vendor/zennotes/zennotes-bridge-contract-2.79.0-boundaries.h28492494a5dfa837.tgz create mode 100644 vendor/zennotes/zennotes-bridge-contract-2.80.0-boundaries.hc07ef9e2bef31a64.tgz delete mode 100644 vendor/zennotes/zennotes-shared-domain-2.79.0-boundaries.h28492494a5dfa837.tgz create mode 100644 vendor/zennotes/zennotes-shared-domain-2.80.0-boundaries.hc07ef9e2bef31a64.tgz diff --git a/README.md b/README.md index 186816c..38585e3 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ archives are vendored under `vendor/zennotes/` with their source identity and checksums (`manifest.json`), and `package-lock.json` pins the complete install. A clean checkout installs them with `npm ci`, without a source clone or sibling repository. The vendored set is the core release -[core-2.79.0-core.h7896a01910a2ea0f](https://github.com/ZenNotes/zennotes/releases/tag/core-2.79.0-core.h7896a01910a2ea0f) -(desktop commit `47d26827`, clean tree). +[core-2.80.0-core.hc5a4589d040cd0da](https://github.com/ZenNotes/zennotes/releases/tag/core-2.80.0-core.hc5a4589d040cd0da) +(desktop commit `31520814`, clean tree). `npm run boundaries:check` verifies the pins, installed versions, singleton React/CodeMirror peers, and public export usage. `npm run core:adopt -- ` diff --git a/package-lock.json b/package-lock.json index 6fbab0e..38355ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,9 +32,9 @@ "@lezer/highlight": "^1.2.5", "@replit/codemirror-vim": "^6.4.0", "@xyflow/react": "^12.12.0", - "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.79.0-core.h7896a01910a2ea0f.tgz", - "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.79.0-boundaries.h28492494a5dfa837.tgz", - "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.79.0-boundaries.h28492494a5dfa837.tgz", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.80.0-core.hc5a4589d040cd0da.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.80.0-boundaries.hc07ef9e2bef31a64.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.80.0-boundaries.hc07ef9e2bef31a64.tgz", "codemirror": "^6.0.1", "dompurify": "^3.4.17", "function-plot": "^1.26.0", @@ -3666,9 +3666,9 @@ } }, "node_modules/@zennotes/app-core": { - "version": "2.79.0-core.h7896a01910a2ea0f", - "resolved": "file:vendor/zennotes/zennotes-app-core-2.79.0-core.h7896a01910a2ea0f.tgz", - "integrity": "sha512-NUULX0/XabETj9gziIIglT465in8uWMfJU1FQDe7ilbTSeAKF4XtcVC5Zx/+8y1v3q1Xg+P868JE5U7tc35xGg==", + "version": "2.80.0-core.hc5a4589d040cd0da", + "resolved": "file:vendor/zennotes/zennotes-app-core-2.80.0-core.hc5a4589d040cd0da.tgz", + "integrity": "sha512-pfj1PIqD3RaQOLEvtAP6DagOOqZdnWiBxSH+fckx+STFMTS859gQaTlf2arGaCaRypy0bWMLm+H3LYpGmEDUEw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.18.3", @@ -3698,8 +3698,8 @@ "@replit/codemirror-vim": "^6.3.0", "@terrastruct/d2": "0.1.33", "@xyflow/react": "^12.11.2", - "@zennotes/bridge-contract": "2.79.0-boundaries.h28492494a5dfa837", - "@zennotes/shared-domain": "2.79.0-boundaries.h28492494a5dfa837", + "@zennotes/bridge-contract": "2.80.0-boundaries.hc07ef9e2bef31a64", + "@zennotes/shared-domain": "2.80.0-boundaries.hc07ef9e2bef31a64", "dompurify": "^3.3.4", "function-plot": "^1.25.3", "gray-matter": "^4.0.3", @@ -3743,19 +3743,19 @@ } }, "node_modules/@zennotes/bridge-contract": { - "version": "2.79.0-boundaries.h28492494a5dfa837", - "resolved": "file:vendor/zennotes/zennotes-bridge-contract-2.79.0-boundaries.h28492494a5dfa837.tgz", - "integrity": "sha512-1VGbTL3V0714Ppt8KBxzHYbYPBGbXXEZgDnN2Qk0xwNTCfpSEPVnUfvslnVB/qsPxFop3bI8/xBwkE9qIILnGw==", + "version": "2.80.0-boundaries.hc07ef9e2bef31a64", + "resolved": "file:vendor/zennotes/zennotes-bridge-contract-2.80.0-boundaries.hc07ef9e2bef31a64.tgz", + "integrity": "sha512-Ia+UG7f1rU0ar6+lVdrNTrWpM03udlG6ehai9EN8R5yM3U05HZkFSaAA9shxMg9TrDvt9103FAep/U5XH6/mAg==", "license": "MIT" }, "node_modules/@zennotes/shared-domain": { - "version": "2.79.0-boundaries.h28492494a5dfa837", - "resolved": "file:vendor/zennotes/zennotes-shared-domain-2.79.0-boundaries.h28492494a5dfa837.tgz", - "integrity": "sha512-ZrSPk9MILWPB3Uo1N/8+jL8Im+GhZ2aFufmYY+6LTtTKxSjcjUaMq1gSWoTBWqcl1A1fzsSp/nSOwN/2j1ecmg==", + "version": "2.80.0-boundaries.hc07ef9e2bef31a64", + "resolved": "file:vendor/zennotes/zennotes-shared-domain-2.80.0-boundaries.hc07ef9e2bef31a64.tgz", + "integrity": "sha512-8qj/op680ka4qF8sIqGhZZfTd+ADjhneivst+T7dmS3HOjZaHQQskzjLRt1zSNngSXWqz9mxG03UCItYfDL2gA==", "license": "MIT", "dependencies": { "@scure/bip39": "2.4.0", - "@zennotes/bridge-contract": "2.79.0-boundaries.h28492494a5dfa837", + "@zennotes/bridge-contract": "2.80.0-boundaries.hc07ef9e2bef31a64", "hash-wasm": "4.12.0", "lz-string": "^1.5.0" } diff --git a/package.json b/package.json index 40ae67c..55bc3b1 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,9 @@ "@lezer/highlight": "^1.2.5", "@replit/codemirror-vim": "^6.4.0", "@xyflow/react": "^12.12.0", - "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.79.0-core.h7896a01910a2ea0f.tgz", - "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.79.0-boundaries.h28492494a5dfa837.tgz", - "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.79.0-boundaries.h28492494a5dfa837.tgz", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.80.0-core.hc5a4589d040cd0da.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.80.0-boundaries.hc07ef9e2bef31a64.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.80.0-boundaries.hc07ef9e2bef31a64.tgz", "codemirror": "^6.0.1", "dompurify": "^3.4.17", "function-plot": "^1.26.0", diff --git a/src/bridge/template-folder.test.ts b/src/bridge/template-folder.test.ts new file mode 100644 index 0000000..bf82acf --- /dev/null +++ b/src/bridge/template-folder.test.ts @@ -0,0 +1,197 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { test } from 'node:test' +import { loadMobileModule } from '../../tooling/load-mobile-module.ts' + +// Core 2.80.0's template folder (ZenNotes/zennotes#1024) lives in vault.json +// as `templates.folder`. app-core reads it for every host (the picker, the +// Settings row, the tag surfaces), but the on-device vault writes vault.json +// and scans tasks and text itself, so it keeps the setting through a save, +// follows a folder rename, and leaves the folder out of Tasks and search, as +// desktop main and the Go server do. Its task scan also hands the Kanban board +// each subtask's parent (#1028), held to the core's contract fixture. + +Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: { getItem: () => null } }) +const { MobileVault } = await loadMobileModule(['./src/bridge/vault-fs']) + +const SETTINGS = '.zennotes/vault.json' + +class MemoryFs { + files = new Map() + directories = new Set() + private isDirectory(path: string): boolean { + return path === '' || this.directories.has(path) || [...this.files.keys()].some(key => key.startsWith(`${path}/`)) + } + async statVerified(path: string) { + return this.files.has(path) ? 'file' : this.isDirectory(path) ? 'directory' : null + } + async statOrNull(path: string) { + const type = await this.statVerified(path) + return type ? { type } : null + } + async readdir(path: string) { + const prefix = path ? `${path}/` : '' + const names = new Map() + for (const key of [...this.files.keys(), ...this.directories]) { + if (!key.startsWith(prefix) || key === path) continue + const [name, ...rest] = key.slice(prefix.length).split('/') + if (!name) continue + names.set(name, rest.length > 0 || !this.files.has(key) ? 'directory' : 'file') + } + return [...names].map(([name, type]) => ({ name, type })) + } + async exists(path: string) { return await this.statVerified(path) !== null } + async mkdir(path: string) { this.directories.add(path) } + async readText(path: string) { if (!this.files.has(path)) throw new Error('ENOENT'); return this.files.get(path)! } + async readTextOrNull(path: string) { return this.files.get(path) ?? null } + async writeText(path: string, body: string) { this.files.set(path, body) } + async deleteFile(path: string) { this.files.delete(path) } + async rename(from: string, to: string) { + assert.ok(await this.exists(from)); assert.equal(await this.exists(to), false) + for (const [key, body] of [...this.files]) if (key === from || key.startsWith(`${from}/`)) { + this.files.set(to + key.slice(from.length), body); this.files.delete(key) + } + for (const key of [...this.directories]) if (key === from || key.startsWith(`${from}/`)) { + this.directories.add(to + key.slice(from.length)); this.directories.delete(key) + } + } +} + +function fixture(settings: Record) { + const fs = new MemoryFs(), vault = new MobileVault('Fixture') + fs.files.set(SETTINGS, JSON.stringify({ primaryNotesLocation: 'inbox', ...settings })) + fs.files.set('inbox/Templates/Meeting.md', '# Meeting\n\n- [ ] agenda item\nquarterly review\n') + fs.files.set('inbox/Templates/Work/Standup.md', '# Standup\n\n- [ ] yesterday\n') + fs.files.set('inbox/Templates Old/Kept.md', '# Kept\n\n- [ ] a real task\nquarterly review\n') + fs.files.set('inbox/Plan.md', '# Plan\n\n- [ ] ship it\nquarterly review\n') + Object.assign(vault, { + fs, + invalidateMeta: () => {}, + // The scanners walk listNotes(); answer from the memory files so a rename + // shows up in the next scan. + listNotes: async () => [...fs.files.keys()] + .filter(path => path.endsWith('.md')) + .map(path => ({ + path, + title: path.split('/').pop()!.replace(/\.md$/, ''), + folder: path.startsWith('trash/') ? 'trash' : 'inbox' + })) + }) + return { fs, vault } +} + +const written = (fs: MemoryFs) => JSON.parse(fs.files.get(SETTINGS)!) + +const scanned = async (vault: any): Promise => + [...new Set((await vault.scanTasks()).map((task: { sourcePath: string }) => task.sourcePath))].sort() + +const rescanned = async (vault: any, fs: MemoryFs): Promise => { + const out: string[] = [] + for (const path of [...fs.files.keys()].filter(path => path.endsWith('.md')).sort()) { + if ((await vault.scanTasksForPath(path)).length > 0) out.push(path) + } + return out +} + +test('a save from this device keeps a desktop user\'s template folder', async () => { + const { fs, vault } = fixture({ templates: { folder: 'inbox/Templates' } }) + const settings = await vault.getVaultSettings() + assert.deepEqual(settings.templates, { folder: 'inbox/Templates' }) + await vault.setVaultSettings({ ...settings, favorites: ['inbox/Plan.md'] }) + assert.deepEqual(written(fs).templates, { folder: 'inbox/Templates' }) + assert.deepEqual(written(fs).favorites, ['inbox/Plan.md']) +}) + +test('the folder is normalized as shared-domain normalizes it: cleaned, folder only, an unusable one dropped', async () => { + const cases: [unknown, unknown][] = [ + [{ folder: ' inbox//Templates/ ' }, { folder: 'inbox/Templates' }], + [{ folder: 'inbox\\Templates' }, { folder: 'inbox/Templates' }], + [{ folder: 'inbox/.hidden' }, undefined], + [{ folder: '../outside' }, undefined], + [{ folder: '' }, undefined], + [{ folder: 7 }, undefined], + ['Templates', undefined], + [['Templates'], undefined], + // Only the folder is kept, as desktop main and the Go server keep it. + [{ folder: 'inbox/Templates', later: true }, { folder: 'inbox/Templates' }], + [{ folder: '..', later: true }, undefined] + ] + for (const [raw, expected] of cases) { + const { fs, vault } = fixture({ templates: raw }) + const settings = await vault.getVaultSettings() + assert.deepEqual(settings.templates, expected, JSON.stringify(raw)) + await vault.setVaultSettings(settings) + // No empty stub in vault.json when nothing usable is left. + assert.deepEqual(written(fs).templates, expected, JSON.stringify(raw)) + assert.equal('templates' in written(fs), expected !== undefined, JSON.stringify(raw)) + } +}) + +test('the template folder stays out of Tasks, even inside a Task folder, and its neighbour stays in', async () => { + const { fs, vault } = fixture({ templates: { folder: 'inbox/Templates' } }) + const expected = ['inbox/Plan.md', 'inbox/Templates Old/Kept.md'] + assert.deepEqual(await scanned(vault), expected) + // A single-note rescan agrees, or an edit would bring a template's tasks back. + assert.deepEqual(await rescanned(vault, fs), expected) + + const inside = fixture({ templates: { folder: 'inbox/Templates' }, tasks: { includedFolders: ['inbox'] } }) + assert.deepEqual(await scanned(inside.vault), expected) + assert.deepEqual(await rescanned(inside.vault, inside.fs), expected) +}) + +test('a vault without a template folder keeps every note\'s tasks', async () => { + const { vault } = fixture({}) + assert.deepEqual(await scanned(vault), [ + 'inbox/Plan.md', 'inbox/Templates Old/Kept.md', 'inbox/Templates/Meeting.md', 'inbox/Templates/Work/Standup.md' + ]) +}) + +test('renaming the folder, or one it sits in, carries the setting along', async () => { + const { fs, vault } = fixture({ templates: { folder: 'inbox/Templates/Work' } }) + await vault.renameFolder('inbox', 'Templates/Work', 'Templates/Office') + assert.deepEqual(written(fs).templates, { folder: 'inbox/Templates/Office' }) + await vault.renameFolder('inbox', 'Templates', 'Library/Templates') + assert.deepEqual(written(fs).templates, { folder: 'inbox/Library/Templates/Office' }) + assert.deepEqual(await scanned(vault), [ + 'inbox/Library/Templates/Meeting.md', 'inbox/Plan.md', 'inbox/Templates Old/Kept.md' + ]) + // A folder that is neither the template folder nor holds it leaves it alone. + await vault.renameFolder('inbox', 'Templates Old', 'Archive Old') + assert.deepEqual(written(fs).templates, { folder: 'inbox/Library/Templates/Office' }) +}) + +test('text search leaves template notes out before ranking', async () => { + const { vault } = fixture({ templates: { folder: 'inbox/Templates' } }) + const paths = (await vault.searchVaultText('quarterly review')).map((match: { path: string }) => match.path).sort() + assert.deepEqual(paths, ['inbox/Plan.md', 'inbox/Templates Old/Kept.md']) + const none = fixture({}) + const all = (await none.vault.searchVaultText('quarterly review')).map((match: { path: string }) => match.path).sort() + assert.deepEqual(all, ['inbox/Plan.md', 'inbox/Templates Old/Kept.md', 'inbox/Templates/Meeting.md']) +}) + +interface SubtaskCase { + id: string + note: { path: string; title: string; folder: string } + body: string + expectedTasks: { id: string; parentId?: string }[] +} + +test('each scanned task names its parent the way the core contract fixture says (#1028)', async () => { + const fixtureUrl = new URL('../../node_modules/@zennotes/bridge-contract/fixtures/task-subtasks.json', import.meta.url) + const { cases } = JSON.parse(readFileSync(fixtureUrl, 'utf8')) as { cases: SubtaskCase[] } + assert.ok(cases.length > 0) + for (const testCase of cases) { + const fs = new MemoryFs(), vault = new MobileVault('Fixture') + fs.files.set(SETTINGS, JSON.stringify({ primaryNotesLocation: 'inbox' })) + fs.files.set(testCase.note.path, testCase.body) + Object.assign(vault, { + fs, + invalidateMeta: () => {}, + listNotes: async () => [{ path: testCase.note.path, title: testCase.note.title, folder: testCase.note.folder }] + }) + const links = (tasks: { id: string; parentId?: string }[]) => + tasks.map(task => ('parentId' in task ? { id: task.id, parentId: task.parentId } : { id: task.id })) + assert.deepEqual(links(await vault.scanTasksForPath(testCase.note.path)), testCase.expectedTasks, testCase.id) + assert.deepEqual(links(await vault.scanTasks()), testCase.expectedTasks, testCase.id) + } +}) diff --git a/src/bridge/vault-fs.ts b/src/bridge/vault-fs.ts index 0225de3..78eb8b7 100644 --- a/src/bridge/vault-fs.ts +++ b/src/bridge/vault-fs.ts @@ -32,8 +32,15 @@ import { isPathExcludedFromTasks, normalizeTasksExcludedFolders, normalizeTasksIncludedFolders, - rewriteTasksFoldersForRename + rewriteTasksFoldersForRename, + tasksFolderListsForSettings } from '@zennotes/shared-domain/tasks-excluded-folders' +import { + isPathInTemplateFolder, + normalizeTemplateFolderSettings, + resolveTemplateFolder, + rewriteTemplateFolderForRename +} from '@zennotes/shared-domain/template-folder' import { pastedImageFilename } from '@zennotes/shared-domain/pasted-image' import { randomUUID } from './uuid' import { bytesToBase64 } from './base64' @@ -380,6 +387,14 @@ export class MobileVault { const tasks = normalizeTasksSettings(obj.tasks) if (tasks) out.tasks = tasks else delete out.tasks + // A desktop user's template folder (#1024) rides through a save from this + // device, normalized exactly as desktop main normalizes it: a usable + // folder cleaned (`Templates/` reads `Templates`), only the folder kept, + // and no stub left when there is none. The pass-through above would carry + // it raw. + const templates = normalizeTemplateFolderSettings(obj.templates) + if (templates) out.templates = templates + else delete out.templates return out } @@ -1074,7 +1089,10 @@ export class MobileVault { // The Tasks folder lists name on-disk paths, so they follow the folder // too, as on desktop: a renamed Task folder (#962) left behind would // match nothing and empty Tasks. - tasks: rewriteTasksFoldersForRename(settings.tasks, oldRel, newRel) + tasks: rewriteTasksFoldersForRename(settings.tasks, oldRel, newRel), + // So does the template folder (#1024), or its templates would turn back + // into notes and their checkboxes into tasks. + templates: rewriteTemplateFolderForRename(settings.templates, oldRel, newRel) }) } catch (error) { try { @@ -1231,8 +1249,12 @@ export class MobileVault { } await this.collectSearchCandidates(topRel, folder, isPrimaryRoot, hidden, candidates) } + // Template notes (#1024) are placeholders nobody is searching for. They + // go before ranking, as on desktop, so the limit is spent on real notes. + const templateFolder = resolveTemplateFolder(settings) const ranked: { score: number; c: SearchCandidate }[] = [] for (const c of candidates) { + if (isPathInTemplateFolder(c.path, templateFolder)) continue const bodyScore = scoreMatch(trimmed, c.lineText) if (bodyScore <= 0) continue const titleScore = scoreMatch(trimmed, c.title) * 0.18 @@ -1308,13 +1330,12 @@ export class MobileVault { /** The vault's Tasks folder lists from vault.json: the folders left out of * Tasks (#458) and the Task folders it is narrowed to (#962), empty - * meaning the whole vault. */ + * meaning the whole vault, with the template folder (#1024) left out too, + * even inside a Task folder: a template's checkboxes are placeholders for + * the note it will make. The shared helper desktop main and the Go server + * apply, so every scanner agrees. */ private async tasksFolderLists(): Promise<{ excluded: string[]; included: string[] }> { - const tasks = (await this.getVaultSettings()).tasks - return { - excluded: normalizeTasksExcludedFolders(tasks?.excludedFolders), - included: normalizeTasksIncludedFolders(tasks?.includedFolders) - } + return tasksFolderListsForSettings(await this.getVaultSettings()) } async scanTasks(): Promise { diff --git a/src/ui-mobile/MobileDrawer.tsx b/src/ui-mobile/MobileDrawer.tsx index 90f2e47..15d7d83 100644 --- a/src/ui-mobile/MobileDrawer.tsx +++ b/src/ui-mobile/MobileDrawer.tsx @@ -11,7 +11,7 @@ import ReactDOM from 'react-dom/client' import { getShellSnapshot, useShellSnapshot, setNoteSortOrder, type NoteSortOrder } from '@zennotes/app-core/shell' import { getBrowseSnapshot, useBrowseSnapshot, getBrowseDirectory, requestCreateBrowseFolder, requestCreateBrowseNote, requestRenameBrowseFolder, requestRenameBrowseDatabase, requestMoveBrowseDirectory, - requestDeleteBrowseDirectory } from '@zennotes/app-core/browse' + requestDeleteBrowseDirectory, openBrowseFolderGallery } from '@zennotes/app-core/browse' import { useWorkspaceSnapshot, openLocalVault, pickLocalVault, refreshRemoteProfiles, connectRemoteWorkspace, connectRemoteProfile, changeRemoteVaultPath, deleteRemoteProfile } from '@zennotes/app-core/workspace' import { openNote, openAppPage } from '@zennotes/app-core/navigation' @@ -99,6 +99,7 @@ const D = { more: 'M6 12h.01M12 12h.01M18 12h.01', pencil: 'M17 3a2.85 2.85 0 114 4L7.5 20.5 2 22l1.5-5.5L17 3z', pin: 'M12 17v5M9 3h6l-1 7 3 2v3H7v-3l3-2-1-7z', + gallery: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z', // The editor toolbar's "Attach file" glyph. file: 'M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l8.57-8.57A4 4 0 0118 8.84l-8.59 8.57a2 2 0 01-2.83-2.83l8.49-8.48' } @@ -1152,6 +1153,13 @@ function MobileDrawerBody(props: { setFolderMenu(null) void requestMoveBrowseDirectory(host, subpath).catch(reportActionError) } + // The desktop folder menu's Open as Gallery (core 2.80's + // openBrowseFolderGallery): the drawer closes onto the folder's cards. + const openFolderGallery = (subpath: string): void => { + const host = folderMenu?.host ?? captureMobileWorkspace() + setFolderMenu(null) + go(() => openBrowseFolderGallery(host, subpath).catch(reportActionError)) + } const newFolderHere = (): void => { void requestCreateBrowseFolder(captureMobileWorkspace(), path).catch(reportActionError) } @@ -1433,6 +1441,16 @@ function MobileDrawerBody(props: { {pinnedFolders.includes(folderMenu.subpath) ? 'Unpin' : 'Pin'} )} + {folderMenu.kind === 'folder' && ( + + )}