fix: refresh image asset scope after attachment writes
This commit is contained in:
@@ -312,7 +312,7 @@ The folder tree hides only the dedicated `type/` directory, since note types alr
|
||||
|
||||
A `vault_health_check` command detects stray files in non-protected subfolders and filename-title mismatches. On vault load, a migration banner offers to flatten stray files to the root via `flatten_vault`.
|
||||
|
||||
Command-layer path access is fenced to the active vault before file operations reach the vault backend. `src-tauri/src/commands/vault/boundary.rs` canonicalizes the configured/requested vault root, rejects `..` escapes and absolute paths outside that root, and validates writable targets through the nearest existing ancestor so note reads, saves, deletes, view-file edits, and folder mutations cannot step outside the active vault.
|
||||
Command-layer path access is fenced to the active vault before file operations reach the vault backend. `src-tauri/src/commands/vault/boundary.rs` canonicalizes the configured/requested vault root, rejects `..` escapes and absolute paths outside that root, and validates writable targets through the nearest existing ancestor so note reads, saves, deletes, view-file edits, folder mutations, and image attachment writes cannot step outside the active vault. Image attachment commands refresh the runtime asset scope after saving so files created under a previously missing `attachments/` directory can render immediately.
|
||||
|
||||
### Vault Caching
|
||||
|
||||
|
||||
@@ -595,7 +595,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
|
||||
| `cache.rs` | Git-based incremental vault caching (`scan_vault_cached`), git helpers |
|
||||
| `filename_rules.rs` | Cross-platform validation for note filenames, folder names, and custom view filenames |
|
||||
| `rename.rs` | `rename_note` / `rename_note_filename` / `move_note_to_folder` — stage crash-safe file moves, update `title` frontmatter when needed, recover unfinished rename transactions, and report backlink rewrite failures |
|
||||
| `image.rs` | `save_image` — saves base64-encoded attachments with sanitized filenames |
|
||||
| `image.rs` | `save_image` / `copy_image_to_vault` — save editor image attachments with sanitized filenames |
|
||||
| `migration.rs` | `flatten_vault`, `vault_health_check`, `migrate_is_a_to_type` |
|
||||
| `config_seed.rs` | Maintains vault AI guidance (`AGENTS.md` + `CLAUDE.md` shim), migrates legacy `config/agents.md`, and repairs missing root type scaffolding such as `type.md` and `note.md` |
|
||||
| `getting_started.rs` | Clones and normalizes the public Getting Started starter vault |
|
||||
@@ -713,8 +713,8 @@ The desktop MCP WebSocket bridge is intentionally local-only. `mcp-server/ws-bri
|
||||
| `save_vault_config` | Save per-vault UI config |
|
||||
| `get_default_vault_path` | Get default vault path |
|
||||
| `get_build_number` | Get app build number |
|
||||
| `save_image` | Save base64 image to vault |
|
||||
| `copy_image_to_vault` | Copy image file to vault |
|
||||
| `save_image` | Save base64 image to `attachments/` and refresh the active vault asset scope |
|
||||
| `copy_image_to_vault` | Copy image file to `attachments/` and refresh the active vault asset scope |
|
||||
| `update_menu_state` | Update native menu checkmarks and enabled/disabled state for selection-dependent actions |
|
||||
| `trigger_menu_command` | Emit a native menu command ID for deterministic shortcut QA |
|
||||
| `update_current_window_min_size` | Update the active Tauri window's minimum size and optionally grow it to fit restored panes |
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { test, expect, type Page } from '@playwright/test'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import {
|
||||
createFixtureVaultCopy,
|
||||
openFixtureVault,
|
||||
removeFixtureVaultCopy,
|
||||
} from '../tests/helpers/fixtureVault'
|
||||
|
||||
// Minimal valid PNG: 1x1 red pixel
|
||||
const TEST_PNG_BASE64 =
|
||||
@@ -11,16 +16,28 @@ function createTestPng(filepath: string) {
|
||||
fs.writeFileSync(filepath, Buffer.from(TEST_PNG_BASE64, 'base64'))
|
||||
}
|
||||
|
||||
test('image upload via file picker displays image with data URL', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(1000)
|
||||
let tempVaultDir: string
|
||||
|
||||
// Open a note
|
||||
await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 })
|
||||
await page.waitForTimeout(500)
|
||||
async function openImageTestNote(page: Page) {
|
||||
await page.locator('[data-testid="note-list-container"]').getByText('Alpha Project', { exact: true }).click()
|
||||
|
||||
const editor = page.locator('.bn-editor')
|
||||
await expect(editor).toBeVisible({ timeout: 10000 })
|
||||
return editor
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testInfo.setTimeout(60_000)
|
||||
tempVaultDir = createFixtureVaultCopy()
|
||||
await openFixtureVault(page, tempVaultDir)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
removeFixtureVaultCopy(tempVaultDir)
|
||||
})
|
||||
|
||||
test('image upload via file picker displays image with data URL', async ({ page }) => {
|
||||
const editor = await openImageTestNote(page)
|
||||
await editor.click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
@@ -63,16 +80,37 @@ test('image upload via file picker displays image with data URL', async ({ page
|
||||
if (fs.existsSync(testImagePath)) fs.unlinkSync(testImagePath)
|
||||
})
|
||||
|
||||
test('image paste into editor inserts image block', async ({ page }) => {
|
||||
const editor = await openImageTestNote(page)
|
||||
await editor.click()
|
||||
|
||||
await page.evaluate((base64) => {
|
||||
const editorElement = document.querySelector('.bn-editor')
|
||||
if (!editorElement) throw new Error('Editor not found')
|
||||
|
||||
const binary = atob(base64)
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
||||
|
||||
const file = new File([bytes], 'pasted-image.png', { type: 'image/png' })
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.items.add(file)
|
||||
editorElement.dispatchEvent(new ClipboardEvent('paste', {
|
||||
clipboardData,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}))
|
||||
}, TEST_PNG_BASE64)
|
||||
|
||||
const images = page.locator('.bn-editor img')
|
||||
await expect(images.first()).toBeVisible({ timeout: 5000 })
|
||||
|
||||
const src = await images.first().getAttribute('src')
|
||||
expect(src).toMatch(/^data:/)
|
||||
})
|
||||
|
||||
test('editor has uploadFile configured (no error on image block insert)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Click first note
|
||||
await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 })
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const editor = page.locator('.bn-editor')
|
||||
await expect(editor).toBeVisible({ timeout: 10000 })
|
||||
const editor = await openImageTestNote(page)
|
||||
|
||||
// Capture console errors
|
||||
const errors: string[] = []
|
||||
|
||||
@@ -40,6 +40,31 @@ fn with_requested_root_path<T>(
|
||||
with_requested_root(raw_vault_path.as_ref(), action)
|
||||
}
|
||||
|
||||
fn sync_image_asset_scope(
|
||||
app_handle: &tauri::AppHandle,
|
||||
requested_root: &str,
|
||||
) -> Result<(), String> {
|
||||
#[cfg(desktop)]
|
||||
crate::sync_vault_asset_scope(app_handle, Path::new(requested_root))?;
|
||||
#[cfg(not(desktop))]
|
||||
let _ = requested_root;
|
||||
#[cfg(not(desktop))]
|
||||
let _ = app_handle;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn with_image_asset_scope(
|
||||
app_handle: &tauri::AppHandle,
|
||||
vault_path: &Path,
|
||||
action: impl FnOnce(&str) -> Result<String, String>,
|
||||
) -> Result<String, String> {
|
||||
with_requested_root_path(vault_path, |requested_root| {
|
||||
let saved_path = action(requested_root)?;
|
||||
sync_image_asset_scope(app_handle, requested_root)?;
|
||||
Ok(saved_path)
|
||||
})
|
||||
}
|
||||
|
||||
fn with_writable_note_path<T>(
|
||||
path: PathBuf,
|
||||
vault_path: Option<PathBuf>,
|
||||
@@ -148,15 +173,24 @@ pub fn sync_note_title(path: PathBuf, vault_path: Option<PathBuf>) -> Result<boo
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_image(vault_path: PathBuf, filename: String, data: String) -> Result<String, String> {
|
||||
with_requested_root_path(vault_path.as_path(), |requested_root| {
|
||||
pub fn save_image(
|
||||
app_handle: tauri::AppHandle,
|
||||
vault_path: PathBuf,
|
||||
filename: String,
|
||||
data: String,
|
||||
) -> Result<String, String> {
|
||||
with_image_asset_scope(&app_handle, vault_path.as_path(), |requested_root| {
|
||||
vault::save_image(requested_root, &filename, &data)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn copy_image_to_vault(vault_path: PathBuf, source_path: PathBuf) -> Result<String, String> {
|
||||
with_requested_root_path(vault_path.as_path(), |requested_root| {
|
||||
pub fn copy_image_to_vault(
|
||||
app_handle: tauri::AppHandle,
|
||||
vault_path: PathBuf,
|
||||
source_path: PathBuf,
|
||||
) -> Result<String, String> {
|
||||
with_image_asset_scope(&app_handle, vault_path.as_path(), |requested_root| {
|
||||
vault::copy_image_to_vault(requested_root, source_path.to_string_lossy().as_ref())
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user