feat: bulk edit metadata across folders and selected notes - #124
Conversation
Pure, Obsidian-free logic deciding what to write for one note given a conflict policy (skip/overwrite/merge), with stable-serialization equality so merge dedupes across types and every policy converges to a no-op on re-run. Covered by 22 unit tests.
Replace the blind per-file folder writer with a policy-aware bulk flow, reachable from the folder context menu and a new file-explorer multi-selection (files-menu) item. - Each note's read/decide/write runs atomically inside app.fileManager.processFrontMatter (the controller's frontmatter primitive), so decisions use live frontmatter, not the lazy cache. - Conflict policy (skip/merge/overwrite) is chosen once; destructive overwrite is gated behind a blast-radius confirmation (closes the spirit of #20). A cancel-safe option modal resolves null on Escape. - Adds honor EditMode multi-value wrapping for parity with single-note adds; merge keeps every note list-shaped and dedupes idempotently. - One summary Notice; per-note failures are isolated and logged. Closes #64, #20.
Live-vault coverage for folder add + idempotent re-run, merge into lists without duplication, idempotent overwrite, multi-selection dedup, and the files-menu item wiring - each asserting zero runtime errors.
- Folder/multi-select menu items now appear when markdown lives only in subfolders, matching the recursive collector (was a direct-child-only gate). Regression-tested live. - Frame the overwrite confirmation against the authoritative selection size instead of a cache-derived conflict count, so the stated blast radius can never understate what the live apply overwrites. - Abort the flow on an empty value rather than writing a blank property across the batch. - Report per-note failures once via console.warn instead of log.logWarning (which routes to logError, spamming Notices and registering as runtime errors).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1aebe430c1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // across the whole batch, matching the prior folder command's guard. | ||
| if (!rawValue) return; | ||
|
|
||
| const conflicts = this.countExisting(files, key); |
There was a problem hiding this comment.
Detect conflicts from live frontmatter
When the selected notes were just created or externally modified and Obsidian's metadata cache has not caught up, countExisting() can return 0 even though applyToFile() later sees the key via processFrontMatter; in that case the interactive flow never offers Merge or Overwrite and silently applies the default skip policy, leaving those existing values unchanged. The conflict scan should use the same live frontmatter source as the write path, or otherwise wait for/cache-validate metadata before deciding whether to show the policy modal.
Useful? React with 👍 / 👎.
# [1.9.0](1.8.4...1.9.0) (2026-07-01) ### Bug Fixes * address review findings from PRs [#119](https://github.com/chhoumann/MetaEdit/issues/119)-[#128](https://github.com/chhoumann/MetaEdit/issues/128) ([adf0b57](adf0b57)) * **api:** preserve reserved keys when cloning values ([#162](#162)) ([cff6076](cff6076)) * **automators:** count only [x]/[X] tasks complete; null-guard Kanban lane display ([#145](#145)) ([21da53a](21da53a)) * **bulk:** reject __proto__/constructor as a bulk property key ([#148](#148)) ([f03eacc](f03eacc)) * **bulk:** serialize bulk frontmatter writes through the controller write queue ([#147](#147)) ([3eb8236](3eb8236)) * **controller:** guard __proto__/constructor in all frontmatter writes ([#159](#159)) ([9d99fae](9d99fae)) * **controller:** make inline-field writes fence-aware ([5f8e713](5f8e713)) * **core,api:** safe command no-op, block-list deletion, presence-based property lookups ([#143](#143)) ([1702d08](1702d08)) * edit YAML lists (incl. tags) as native lists instead of collapsing them to a string ([#94](#94)) ([#128](#128)) ([79f6ee0](79f6ee0)), closes [#51](#51) [#36](#36) * harden frontmatter metadata writes ([add2e90](add2e90)) * Kanban helper syncs only the card's leading link, not trailing date/reference links ([#126](#126)) ([52aec1e](52aec1e)) * **kanban:** don't write to an ambiguous same-named note ([#158](#158)) ([7f9655b](7f9655b)) * **kanban:** single notice for a card missing the board property ([9b1c88a](9b1c88a)) * parse inline fields behind list/quote markers and brackets ([#122](#122)) ([a62ef21](a62ef21)), closes [#119](#119) [#18](#18) [#121](#121) [#78](#78) [#84](#84) * **parser:** preserve [[wikilinks]] in multi-value inline edits ([c331adb](c331adb)) * **settings:** serialize Auto Property choice persistence to prevent lost updates ([#154](#154)) ([2066b3e](2066b3e)) * **settings:** settings tab no longer clobbers concurrently-added Auto Property choices ([5759534](5759534)) * **settings:** validate auto-properties before queueing and make rollbacks compare-and-restore ([#156](#156)) ([8cbacdc](8cbacdc)), closes [#154](#154) * stop inline-field updates appending a stray bracket ([#127](#127)) ([d2617f5](d2617f5)), closes [#67](#67) [#121](#121) * **suggester:** center row action icons flush-right ([#166](#166)) ([c00795e](c00795e)) * **suggester:** use native icons + tooltips for row actions ([70903cc](70903cc)) * **suggester:** well-formed duplicate Notice, robust modal close, cleaner name suggestions ([#144](#144)) ([915dd6b](915dd6b)), closes [#143](#143) * **tags:** correct and clarify tag editing ([#142](#142)) ([b837899](b837899)), closes [#49](#49) * tolerate malformed YAML frontmatter when parsing note metadata ([#132](#132)) ([b835f97](b835f97)), closes [#130](#130) ### Features * adopt modern Obsidian APIs and raise baseline ([5131664](5131664)) * **api:** append Dataview field instances without replacing ([42c78a8](42c78a8)), closes [#91](#91) * **api:** expand public integration surface ([#120](#120)) ([bbf0a7a](bbf0a7a)) * assist property-value entry with autocomplete and a native date picker ([#125](#125)) ([cd86235](cd86235)), closes [#61](#61) [#74](#74) [#61](#61) [#61](#61) * **auto-properties:** description, multi-select, and learn-as-you-go values ([#123](#123)) ([4b24296](4b24296)), closes [#59](#59) [#40](#40) [#59](#59) [#40](#40) [#59](#59) [#40](#40) [#43](#43) [#30](#30) * **auto-properties:** paste a list to split into choices ([#139](#139)) ([0245f7a](0245f7a)) * bulk edit metadata across folders and selected notes ([#124](#124)) ([c8d91e2](c8d91e2)), closes [#64](#64) [#20](#20) * **create:** fluid, type-aware native YAML property creation ([#170](#170)) ([59b4c5d](59b4c5d)) * **editor:** edit properties with Obsidian's native widgets; raise minAppVersion to 1.12.7 ([#168](#168)) ([73b37e0](73b37e0)) * hide file tags from the Edit Meta menu ([#46](#46), [#90](#90)) ([#131](#131)) ([2c07937](2c07937)), closes [#tag](https://github.com/chhoumann/MetaEdit/issues/tag) [#tags](https://github.com/chhoumann/MetaEdit/issues/tags) * support nested YAML path editing ([e9fce98](e9fce98))
|
🎉 This PR is included in version 1.9.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Adds a coherent bulk metadata edit capability: add or update a single YAML
frontmatter property across many notes at once, safely and idempotently.
Reachable two ways:
files-menuentry - the literal ask of Add feature to add a property to multiple notes. #64).The flow: prompt for a property name and value, then - only when some notes
already define the property - choose a conflict policy once:
A single summary Notice reports the outcome (
added / merged / overwritten / skipped / unchanged / failed). Per-note failures are isolated and logged; onebad note never aborts the batch.
Closes #64. Closes #20.
Why these issues, and what about #32?
property when bulk-applying in folders; merge into arrays; confirm before
overwriting") are the same capability from two angles, delivered together here.
On today's master don't duplicate properties inside folders #20's literal duplicate is already prevented by the
controller's
hasOwnPropertyguard; what was missing - and is added here - isthe actual bulk capability: a multi-select entry point, a conflict policy
(merge into lists / confirmed overwrite), idempotency, and a single summary.
intentionally scoped out. It is a content-harvest operation (scan note
body lines, collect matches into a property) - a fundamentally different
mental model from "apply property = value across a set of notes," and
single-note-oriented in the issue. Folding it in would muddy a clean
capability. The bulk engine is operation-shaped so Search for line with keyword, and add to property #32 can be added later as
its own feature. Recommend keeping Search for line with keyword, and add to property #32 open as a separate follow-up.
Design
(
src/bulk/bulkMetadata.ts): re-running any policy is a no-op (proven by unittests and live re-run E2E). Merge dedupes by stable serialization and every
policy converges.
single
app.fileManager.processFrontMattercallback - the same frontmatterprimitive the controller write path is built on - not hand-rolled YAML text
edits. Decisions use the note's live frontmatter, never the lazy metadata
cache.
wrapping, so YAML shape is consistent whether you add one note or many.
BulkOptionModalresolvesnullon Escape; textprompts catch the prompt's cancel rejection and abort cleanly.
Adversarial review
Design and implementation each went through opposing-model adversarial review
(refute-first, multiple lenses). Findings folded in: cancel-safe modal; atomic
read-decide-write against live frontmatter; EditMode parity; recursive menu
reachability (items now appear when markdown lives only in subfolders);
overwrite confirmation framed against the authoritative selection size (never a
cache count that could understate the blast radius); empty value aborts; failures
reported once via
console.warn(not per-note error Notices). Verified live thatprocessFrontMatterperforms no write on a no-op, so skip/unchanged re-runsdon't churn files or fire automators.
Collision boundary
Touches only
src/main.ts,src/Modals/LinkMenu.ts, new files undersrc/bulk/,src/Types/obsidian-augmentations.d.ts, andstyles.css. It editsnone of:
metaController.ts,parser.ts,Settings/, the suggester modals, orMetaEditApi.ts.Validation
Obsidian 1.12.7, isolated worktree E2E vault. Branch rebased on current
master (integrates #120 / #122).
pnpm run lint-> 0 errors (only pre-existing warnings; 2 adjacent ones fixed).pnpm run build-> clean.pnpm run test-> 96 unit tests pass (22 new for the decision engine).pnpm run test:e2e-> 16 live tests pass (11 pre-existing + 5 new), each asserting zero runtime errors.frontmatter, scalar conflict, array, empty, subfolder note, non-md file) and
verified collection (recursion + md filter + dedup), every policy, idempotent
re-runs, EditMode wrapping, recursive menu reachability, the multi-select menu
wiring, the full interactive flow (prompts -> option modal -> summary), and
cancel-safety (Escape aborts with no writes, no hang). Final frontmatter
verified clean (correct fences, list formatting, merged
[x, y, z]with noduplicates).
Known limitations / deliberate choices
Ctrl+Z (same as the previous folder command); that's why Overwrite is confirmed.
5and"5"are treated as the samevalue (prevents visually-identical duplicates). Pre-existing duplicates inside
a list are left as-is rather than silently rewritten.
(the same source as the existing
getFilesWithProperty); the apply uses livefrontmatter and the summary reports the true counts. The destructive overwrite
confirmation is framed against the authoritative selection size, so it can
never understate the blast radius.
processFrontMatter(atomic,key-scoped) sequentially; they are not cross-serialized with the controller's
private per-file queue, which lives in a file outside this change's boundary.
Risk is low because writes are sequential and merge into existing frontmatter
by key.
the final summary).