Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.80.0-core.hc5a4589d040cd0da](https://github.com/ZenNotes/zennotes/releases/tag/core-2.80.0-core.hc5a4589d040cd0da)
(desktop commit `31520814`, clean tree).
[core-2.81.0-core.h4e6a38a3361b4748](https://github.com/ZenNotes/zennotes/releases/tag/core-2.81.0-core.h4e6a38a3361b4748)
(desktop commit `bfab9fdc`, clean tree).

`npm run boundaries:check` verifies the pins, installed versions, singleton
React/CodeMirror peers, and public export usage. `npm run core:adopt -- <core-tag>`
Expand Down
24 changes: 12 additions & 12 deletions ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -624,12 +624,12 @@
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ZenWidgets/ZenWidgets.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = WYY7PK57DM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ZenWidgets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.35.0;
MARKETING_VERSION = 1.36.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ZenWidgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand Down Expand Up @@ -753,12 +753,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = WYY7PK57DM;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.35.0;
MARKETING_VERSION = 1.36.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -775,12 +775,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = WYY7PK57DM;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.35.0;
MARKETING_VERSION = 1.36.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
Expand All @@ -795,12 +795,12 @@
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = WYY7PK57DM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.35.0;
MARKETING_VERSION = 1.36.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand All @@ -817,12 +817,12 @@
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ZenWidgets/ZenWidgets.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = WYY7PK57DM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ZenWidgets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.35.0;
MARKETING_VERSION = 1.36.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ZenWidgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand Down Expand Up @@ -855,12 +855,12 @@
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = WYY7PK57DM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.35.0;
MARKETING_VERSION = 1.36.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand Down
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.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",
"@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.81.0-core.h4e6a38a3361b4748.tgz",
"@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.81.0-boundaries.h697bb3192aa5970b.tgz",
"@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.81.0-boundaries.h697bb3192aa5970b.tgz",
"codemirror": "^6.0.1",
"dompurify": "^3.4.17",
"function-plot": "^1.26.0",
Expand Down
42 changes: 42 additions & 0 deletions src/bridge/database-open.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { describe, it } from 'node:test'
import { missingDatabaseAsNull } from './database-open.ts'

const doc = { relPath: 'inbox/Board.base/data.csv' } as never

describe('Opening a database whose folder is gone', () => {
it('answers null for a missing database, so core forgets it and closes its tab', async () => {
const open = missingDatabaseAsNull(async (rel) => {
throw new Error(`Database not found: ${rel}`)
})
assert.equal(await open('inbox/Projects/Untitled Kanban.base/data.csv'), null)
})

it('passes a database through, and every other failure still throws', async () => {
assert.equal(await missingDatabaseAsNull(async () => doc)('x'), doc)
const unreadable = missingDatabaseAsNull(async () => {
throw new Error('EACCES: permission denied')
})
await assert.rejects(unreadable('x'), /permission denied/)
})

it('is what the bridge hands core', () => {
const bridge = readFileSync(new URL('./mobile-bridge.ts', import.meta.url), 'utf8')
assert.match(bridge, /openDatabase: openDatabaseOrThrow,/)
assert.match(bridge, /const openDatabase = missingDatabaseAsNull\(openDatabaseOrThrow\)/)
})

it('reads the contract core and the shared ops keep', () => {
const pkg = (path: string): string =>
readFileSync(new URL(`../../node_modules/@zennotes/${path}`, import.meta.url), 'utf8')
// The shared ops throw for an absent data.csv ...
assert.match(pkg('shared-domain/dist/database-ops.js'), /if \(csvText === null\)\n\s+throw new Error\(`Database not found: \$\{rel\}`\);/)
// ... core forgets a database the bridge answers null for, and toasts a
// throw once, recording it so the grid does not ask again (core 2.81.0).
const store = pkg('app-core/dist/store.js')
assert.match(store, /if \(!doc\) \{[\s\S]{0,240}await get\(\)\.forgetDatabase\(csvPath\);/)
assert.match(store, /const message = humanIpcError\(err, 'Could not open database'\);[\s\S]{0,240}databasesLoadError[\s\S]{0,240}\.addToast\(message, 'error'\);/)
assert.match(pkg('bridge-contract/dist/bridge.d.ts'), /openDatabase\(relPath: string\): Promise<DatabaseDoc \| null>;/)
})
})
26 changes: 26 additions & 0 deletions src/bridge/database-open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { DatabaseDoc } from '@zennotes/bridge-contract/databases'

/**
* A database's tab can outlive its folder: deleted in a file manager, by sync
* from another device, or by another app sharing the vault. The bridge
* contract answers null for a missing database, as desktop's main process
* does, and core then forgets the database and closes its tab.
*
* The shared database ops throw "Database not found" for it instead, and
* core's grid toasted that error and asked again on its next render: one
* relaunch with such a tab open stacked 780 "Database not found" toasts over
* the app. Only that error becomes null; anything else (a file that cannot
* be read, a schema that cannot be parsed) still throws.
*/
export function missingDatabaseAsNull(
open: (relPath: string) => Promise<DatabaseDoc>
): (relPath: string) => Promise<DatabaseDoc | null> {
return async (relPath) => {
try {
return await open(relPath)
} catch (err) {
if (err instanceof Error && err.message.startsWith('Database not found')) return null
throw err
}
}
}
5 changes: 4 additions & 1 deletion src/bridge/mobile-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
VaultTextSearchMatch
} from '@zennotes/shared-domain/ipc'
import { createDatabaseOps } from '@zennotes/shared-domain/database-ops'
import { missingDatabaseAsNull } from './database-open'
import { normalizeVaultDisplayName, resolveVaultName } from '@zennotes/shared-domain/vault-display-name'
import type {
CustomCodeLanguage,
Expand Down Expand Up @@ -725,14 +726,16 @@ const dbOps = createDatabaseOps({
})

const {
openDatabase,
openDatabase: openDatabaseOrThrow,
writeDatabaseRows,
writeDatabaseSchema,
createDatabase,
createRecordPage,
renameDatabase,
listDatabases
} = dbOps
// A tab whose database is gone answers null (database-open.ts).
const openDatabase = missingDatabaseAsNull(openDatabaseOrThrow)

// --------------------------------------------------------------------
// Asset URL resolution (WebView-loadable file URLs)
Expand Down
8 changes: 8 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { installDeepLinks } from './ui-mobile/deep-links'
import { mountMobileShell } from './ui-mobile/MobileShell'
import { installHomeGuard } from './ui-mobile/nav'
import { refreshVault, wireICloudLiveRefresh } from './ui-mobile/refresh'
import { installSoftReturn } from './ui-mobile/soft-return'
import { installFieldKeyboardReveal } from './ui-mobile/field-keyboard-reveal'
import { isPhoneDevice, watchPhoneClass } from './viewport'
import { waitAtBoot } from './boot-wait'
import './ui-mobile/mobile.css'
Expand All @@ -51,6 +53,12 @@ function wireKeyboard(): void {
void Keyboard.setResizeMode({
mode: isPhoneDevice() ? KeyboardResize.Native : KeyboardResize.None
}).catch(() => {})
// Native resize hides the keyboard from CodeMirror's own check, so Return
// at a sentence start would reach the editor as Shift+Return.
if (isPhoneDevice()) installSoftReturn()
// It hides the keyboard from WebKit too, which then leaves a focused field
// outside the editor under it.
if (isPhoneDevice()) installFieldKeyboardReveal()
// Publish the layout decision to CSS as .zn-phone and keep it in step
// with Split View / Stage Manager resizes.
watchPhoneClass()
Expand Down
23 changes: 22 additions & 1 deletion src/ui-mobile/MobileDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, openBrowseFolderGallery } from '@zennotes/app-core/browse'
requestDeleteBrowseDirectory, openBrowseFolderGallery, createBrowseKanban } 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'
Expand Down Expand Up @@ -100,6 +100,8 @@ const D = {
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 New sheet's New Kanban glyph (MobileShell ICONS.tabs).
kanban: 'M4 6h16M4 6v12h16V6M9 6v12',
// 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'
}
Expand Down Expand Up @@ -1160,6 +1162,15 @@ function MobileDrawerBody(props: {
setFolderMenu(null)
go(() => openBrowseFolderGallery(host, subpath).catch(reportActionError))
}
// The desktop folder menu's New Kanban (core 2.81's createBrowseKanban): a
// board seeded with To do, Doing and Done lands in this folder and opens on
// its Board. The New sheet's row puts one where new databases go instead.
// Core refuses a folder inside a database, which the drawer never lists.
const newKanbanIn = (subpath: string): void => {
const host = folderMenu?.host ?? captureMobileWorkspace()
setFolderMenu(null)
go(() => createBrowseKanban(host, subpath).catch(reportActionError))
}
const newFolderHere = (): void => {
void requestCreateBrowseFolder(captureMobileWorkspace(), path).catch(reportActionError)
}
Expand Down Expand Up @@ -1451,6 +1462,16 @@ function MobileDrawerBody(props: {
Open as Gallery
</button>
)}
{folderMenu.kind === 'folder' && (
<button
type="button"
className="zn-mobile-sheet-row"
onClick={() => newKanbanIn(folderMenu.subpath)}
>
<Icon d={D.kanban} />
New Kanban
</button>
)}
<button
type="button"
className="zn-mobile-sheet-row"
Expand Down
Loading
Loading