Actions: Batch's Override Export sends Export for Screens files to the destination folder

With Override Export on, action.batch gave each document.export,
document.exportSelection and document.exportForScreens step a path in
the destination folder. document.exportForScreens reads folder and has
no path parameter, so its files still went to the step's own folder,
or came back as data when the step named none. assets.export was not
overridden at all.

Override Export now gives a document.exportForScreens or assets.export
step the folder destinationFolder/<stem>, in place of the step's own
folder or an export preset's. Each source file gets its own folder,
named after the file without its extension, and each data set gets one
named <document>_<data set>. The export names the files in that folder
as it does outside Batch. Two data sets, or two source files whose
names without the extension differ, no longer write over each other's
files when their artboards or assets have the same names. Other export
steps are unchanged.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-17 15:28:34 -07:00
parent e6aec0664c
commit 74f5f6e58f
4 changed files with 99 additions and 10 deletions
+93 -8
View File
@@ -248,7 +248,9 @@ fn write_result(s: &mut Session, path: &str, ext: &str, as_copy: bool) -> Result
}
/// Point Save / Export steps at `dest_folder/{stem}.{ext}` when overrides are on: `ext` is the
/// extension of the step's `format` ([`format_extension`]), else `default_ext`.
/// extension of the step's `format` ([`format_extension`]), else `default_ext`. Export for Screens
/// and Export Assets steps get `dest_folder/{stem}` as their `folder`, one folder for each source
/// file or data set, and name the files in it themselves.
#[cfg(not(target_arch = "wasm32"))]
fn rewrite_paths(
steps: &[Value],
@@ -267,18 +269,22 @@ fn rewrite_paths(
let id = c.get("command").and_then(Value::as_str).unwrap_or("");
let mut c = c.clone();
let rewrite = (override_save && matches!(id, "document.save" | "file.save" | "file.saveAs"))
|| (override_export && matches!(id, "document.export" | "document.exportForScreens" | "document.exportSelection"));
|| (override_export && matches!(id, "document.export" | "document.exportForScreens" | "document.exportSelection" | "assets.export"));
if !rewrite {
return c;
}
let params = c.get("params").cloned().unwrap_or(json!({}));
let mut params = params.as_object().cloned().unwrap_or_default();
let ext = match params.get("format").and_then(Value::as_str).filter(|f| !f.is_empty()) {
Some(f) => format_extension(f),
None => default_ext.to_string(),
};
let file = std::path::Path::new(folder).join(format!("{stem}.{ext}"));
params.insert("path".into(), json!(file.to_string_lossy()));
if matches!(id, "document.exportForScreens" | "assets.export") {
params.insert("folder".into(), json!(std::path::Path::new(folder).join(stem).to_string_lossy()));
} else {
let ext = match params.get("format").and_then(Value::as_str).filter(|f| !f.is_empty()) {
Some(f) => format_extension(f),
None => default_ext.to_string(),
};
let file = std::path::Path::new(folder).join(format!("{stem}.{ext}"));
params.insert("path".into(), json!(file.to_string_lossy()));
}
if let Some(o) = c.as_object_mut() {
o.insert("params".into(), Value::Object(params));
}
@@ -470,6 +476,85 @@ mod tests {
assert!(file_names(&dest).is_empty(), "{r}");
}
/// Override Export writes an Export for Screens step's files into a folder for each source file
/// inside the destination folder, under the names the export gives them, whether the step names
/// a folder of its own, takes one from an export preset or names none. With Override Export off,
/// the files go to the step's folder.
#[test]
fn batch_override_export_sends_export_for_screens_files_to_the_destination_folder() {
let src = tmp("screens-src");
write_svg(&src, "a.svg", "#f00");
write_svg(&src, "b.svg", "#0f0");
let own = tmp("screens-own");
let svg = json!([{"format": "svg"}]);
let preset = json!({"name": "Own Folder", "settings": {"formats": svg, "folder": own.to_string_lossy()}});
let steps = [json!({"folder": own.to_string_lossy(), "formats": svg}), json!({"exportPreset": "Own Folder"}), json!({"formats": svg})];
for (i, params) in steps.into_iter().enumerate() {
let dest = tmp(&format!("screens-out-{i}"));
let mut s = Session::new();
s.execute("document.exportPresets.save", &preset).unwrap();
let r = s
.execute(
"action.batch",
&json!({
"folder": src.to_string_lossy(),
"destinationFolder": dest.to_string_lossy(),
"commands": [{"command": "document.exportForScreens", "params": params}]
}),
)
.unwrap();
assert_eq!(r["failed"], 0, "{params}: {r}");
assert_eq!(file_names(&dest), ["a", "b"], "{params}: {r}");
for stem in ["a", "b"] {
assert_eq!(file_names(&dest.join(stem)), ["Artboard-1.svg"], "{params}: {r}");
}
assert!(file_names(&own).is_empty(), "{params}: {r}");
}
let dest = tmp("screens-out-off");
let mut s = Session::new();
let r = s
.execute(
"action.batch",
&json!({
"folder": src.to_string_lossy(),
"destinationFolder": dest.to_string_lossy(),
"overrideExport": false,
"commands": [{"command": "document.exportForScreens", "params": {"folder": own.to_string_lossy(), "formats": svg}}]
}),
)
.unwrap();
assert_eq!(r["failed"], 0, "{r}");
assert_eq!(file_names(&own), ["Artboard-1.svg"], "{r}");
assert!(file_names(&dest).is_empty(), "{r}");
}
/// Override Export writes an Export Assets step's files into a folder for each data set inside
/// the destination folder, under the assets' names.
#[test]
fn batch_override_export_sends_asset_export_files_to_the_destination_folder() {
let own = tmp("assets-own");
let dest = tmp("assets-out");
let mut s = with_data_sets(&["a", "b"]);
let id = s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap();
s.execute("assets.add", &json!({"ids": [id], "name": "Logo"})).unwrap();
let r = s
.execute(
"action.batch",
&json!({
"source": "datasets",
"destinationFolder": dest.to_string_lossy(),
"commands": [{"command": "assets.export", "params": {"folder": own.to_string_lossy(), "formats": [{"format": "svg"}]}}]
}),
)
.unwrap();
assert_eq!(r["failed"], 0, "{r}");
assert_eq!(file_names(&dest), ["Untitled_a", "Untitled_b"], "{r}");
for stem in ["Untitled_a", "Untitled_b"] {
assert_eq!(file_names(&dest.join(stem)), ["Logo.svg"], "{r}");
}
assert!(file_names(&own).is_empty(), "{r}");
}
/// Override Save names each file with the extension of the format the step names, also over
/// data sets.
#[test]
+1 -1
View File
@@ -187,7 +187,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Batch…",
[],
None,
"{commands: [{command, params}] (the action's steps), source?: folder (default) | datasets, folder? (required for folder: the source folder), includeSubdirs?: false, destination?: none (default: leave each file open) | saveAndClose (save over the source and close) | folder (save or export into destinationFolder and close), destinationFolder? (required when destination is folder; also the folder Override Save/Export write into), overrideOpen?: true (skip document.open / file.open steps: the batch opens each source file), overrideSave?: true (point document.save / file.save / file.saveAs at destinationFolder/{stem}.{ext}), overrideExport?: true (point document.export / document.exportForScreens / document.exportSelection the same way), stopOnError?: true, label?} play the commands on every readable file in folder (and its subfolders when includeSubdirs), or on every data set of the open document (source: datasets; File + Data Set Name for the folder destination and Override Save/Export, with `/`, `\\`, `:`, `<`, `>`, `\"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and `-2`, `-3`… added to a name already used). Caps: 500 files or data sets, 8 folder levels, 256 steps. Batches cannot nest. → {processed: [{path|dataset, result|error}…], count, failed, stopped}",
"{commands: [{command, params}] (the action's steps), source?: folder (default) | datasets, folder? (required for folder: the source folder), includeSubdirs?: false, destination?: none (default: leave each file open) | saveAndClose (save over the source and close) | folder (save or export into destinationFolder and close), destinationFolder? (required when destination is folder; also the folder Override Save/Export write into), overrideOpen?: true (skip document.open / file.open steps: the batch opens each source file), overrideSave?: true (point document.save / file.save / file.saveAs at destinationFolder/{stem}.{ext}), overrideExport?: true (point document.export / document.exportSelection the same way, and give document.exportForScreens / assets.export the folder destinationFolder/{stem}, one for each source file or data set, in which the export names its files as it does outside Batch), stopOnError?: true, label?} play the commands on every readable file in folder (and its subfolders when includeSubdirs), or on every data set of the open document (source: datasets; File + Data Set Name for the folder destination and Override Save/Export, with `/`, `\\`, `:`, `<`, `>`, `\"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and `-2`, `-3`… added to a name already used). Caps: 500 files or data sets, 8 folder levels, 256 steps. Batches cannot nest. → {processed: [{path|dataset, result|error}…], count, failed, stopped}",
always,
action_batch::action_batch
),
+4 -1
View File
@@ -1154,7 +1154,10 @@ already walks Image Trace. Short agent entry points:
Export end the file name of a step with `format` in that `format`, in lower case and without a leading dot, when it
is one of the format's extensions (`.svg` and `SVG` give `.svg`, `.JPEG` gives `.jpeg`), and otherwise in the
format's first extension (`png8` gives `.png`, `template` gives `.vctemplate`). A step without `format` keeps the
source file's extension (`.vectorcraft` over data sets).
source file's extension (`.vectorcraft` over data sets). Override Export gives a `document.exportForScreens` or
`assets.export` step the `folder` `destinationFolder/<stem>`, in place of the step's own folder or an export
preset's: one folder for each source file, named after the file without its extension, or for each data set,
named `<document>_<data set>`. The export names the files in it as it does outside Batch.
- Image Trace: `imageTrace.presets`, `imageTrace.make {id?, preset?, params?, view?}` (or `imageTrace.makeAndExpand`),
`imageTrace.setView`, `imageTrace.expand`, `imageTrace.release`.
- Live Paint: `livePaint.make`, `livePaint.merge`, `livePaint.fill {group?, point}`, `livePaint.strokeEdge`,
+1
View File
@@ -430,6 +430,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Variables: `dataset.select`, `dataset.next` and `dataset.prev` name the area type and type on a path whose text they rewrote and that doesn't fit, in the reply's `overflow` key, as `text.setText` does.
- Variables: Lock Variables (`variable.lock`, in the Variables panel's bottom bar and menu) stops variables from being defined, deleted or renamed until they are unlocked, while objects can still be bound and unbound and data sets still change and apply; the lock is saved with the document.
- Batch…: Override Save and Override Export name each file with the extension of the format the step writes, however the step spells the format (`.svg` and `SVG` give `.svg`, `png8` gives `.png`), in `action.batch`.
- Batch…: Override Export writes the files of an Export for Screens or Export Assets step into a folder for each source file or data set inside the destination folder, under the names the export gives them (`action.batch`).
- Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command.
- Batch… over data sets names each file after the document and the data set (`action.batch`). It writes `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and adds `-2`, `-3`… to a name an earlier data set used.
- Export for Screens, Save for Web (slice images, and files named after the document's title) and the Libraries panel name files with the characters Windows does not allow written as `-` and with a `-` after a Windows device name such as `CON`, on every platform (`document.exportForScreens`, `document.exportForWeb`, `library.create`).