Actions: Batch Save and Close over data sets follows a redirected Save step

With a destination folder and Override Save on, a recorded Save or Save As
step writes each data set to its own file in that folder and makes that
file the document's own, so Save and Close then saves each data set to its
own file. The batch no longer warns that every data set saves over one
file in that case, and no longer refuses a document that was never saved,
converted or restored, since the recorded step gives it a file first.
This commit is contained in:
dotCooCoo
2026-11-08 23:50:30 +07:00
committed by bflatastic
parent 64cfa8b553
commit 427029def6
4 changed files with 70 additions and 29 deletions
+64 -25
View File
@@ -113,7 +113,8 @@ const ONE_FILE_WARNING: &str = "each data set saves over the same file, which ke
/// Whether an `action.batch` with params `p` plays on data sets of the open document in `s` and
/// writes each data set to the same file. Save and Close saves the open document over its own file
/// after every data set. A step that the batch does not point into `destinationFolder` writes the
/// after every data set, unless a Save step that Override Save points into `destinationFolder`
/// first makes each data set's file the document's own. A step that the batch does not point into `destinationFolder` writes the
/// same file every time when it writes one: a Save, Save As, Save a Copy, Save as Template, Export,
/// Export Selection, PDF, EPS, DXF, Save for Web or Save for Office Documents step that gives a
/// `path`; a Save step without a `path` when the document has a file of its own (saved, and not
@@ -125,38 +126,44 @@ pub fn batch_overwrites_one_file(s: &Session, p: &Value) -> bool {
if p.get("source").and_then(Value::as_str) != Some("datasets") {
return false;
}
if p.get("destination").and_then(Value::as_str) == Some("saveAndClose") {
return true;
}
let folder = p.get("destinationFolder").and_then(Value::as_str).is_some();
let redirected = |key: &str| folder && p.get(key).and_then(Value::as_bool).unwrap_or(true);
let (save, export) = (redirected("overrideSave"), redirected("overrideExport"));
let steps = p.get("commands").and_then(Value::as_array).map_or(&[][..], Vec::as_slice);
if p.get("destination").and_then(Value::as_str) == Some("saveAndClose") && !saves_move_to_each_file(steps, save) {
return true;
}
// The format of the open document's own file, which a Save step without a `path` writes.
let own_format = s.active().filter(|st| st.path.is_some() && !st.converted && !st.recovered).map(|st| st.format);
let names = |v: Option<&Value>| v.and_then(Value::as_str).is_some_and(|s| !s.is_empty());
p.get("commands").and_then(Value::as_array).is_some_and(|cmds| {
cmds.iter().any(|c| {
let params = c.get("params").cloned().unwrap_or_else(|| Value::Object(Default::default()));
let path = names(params.get("path"));
let own_file = || match params.get("format").and_then(Value::as_str).filter(|f| !f.is_empty()) {
None => own_format.is_some(),
Some(f) => own_format.is_some_and(|own| super::format(f).is_some_and(|f| f.id == own)),
};
match c.get("command").and_then(Value::as_str).unwrap_or("") {
"document.save" | "file.save" => !save && (path || own_file()),
"file.saveAs" => !save && path,
"file.saveCopy" | "file.saveAsTemplate" => path,
"document.export" | "document.exportSelection" => !export && path,
// The batch does not point these exports into `destinationFolder`.
"document.exportPdf" | "document.exportEps" | "document.exportDxf" | "document.exportForWeb" | "document.exportForOffice" => path,
// Override Export sets `path`, which Export for Screens does not read: its `folder` stays.
"document.exportForScreens" => super::screens::with_preset(s, &params).is_ok_and(|p| names(p.get("folder"))),
_ => false,
}
})
steps.iter().any(|c| {
let params = c.get("params").cloned().unwrap_or_else(|| Value::Object(Default::default()));
let path = names(params.get("path"));
let own_file = || match params.get("format").and_then(Value::as_str).filter(|f| !f.is_empty()) {
None => own_format.is_some(),
Some(f) => own_format.is_some_and(|own| super::format(f).is_some_and(|f| f.id == own)),
};
match c.get("command").and_then(Value::as_str).unwrap_or("") {
"document.save" | "file.save" => !save && (path || own_file()),
"file.saveAs" => !save && path,
"file.saveCopy" | "file.saveAsTemplate" => path,
"document.export" | "document.exportSelection" => !export && path,
// The batch does not point these exports into `destinationFolder`.
"document.exportPdf" | "document.exportEps" | "document.exportDxf" | "document.exportForWeb" | "document.exportForOffice" => path,
// Override Export sets `path`, which Export for Screens does not read: its `folder` stays.
"document.exportForScreens" => super::screens::with_preset(s, &params).is_ok_and(|p| names(p.get("folder"))),
_ => false,
}
})
}
/// Whether `steps` hold a Save, Save As or `document.save` step that Override Save points at each
/// data set's own file in the destination folder (`redirected`). Such a step makes that file the
/// document's own, so Save and Close then saves each data set to its own file.
fn saves_move_to_each_file(steps: &[Value], redirected: bool) -> bool {
redirected && steps.iter().any(|c| matches!(c.get("command").and_then(Value::as_str), Some("document.save" | "file.save" | "file.saveAs")))
}
#[cfg(not(target_arch = "wasm32"))]
fn batch_folder(s: &mut Session, cmd: &str, p: &Value, override_open: bool, run: &Run<'_>) -> Result<Value> {
use super::{absolute_path, may_read};
@@ -202,7 +209,7 @@ fn batch_datasets(s: &mut Session, cmd: &str, run: &Run<'_>) -> Result<Value> {
if names.len() > MAX_BATCH_FILES {
return Err(bad(cmd, format!("more than {MAX_BATCH_FILES} data sets")));
}
if run.destination == "saveAndClose" {
if run.destination == "saveAndClose" && !saves_move_to_each_file(run.steps, run.override_save && run.dest_folder.is_some()) {
if st.path.is_none() {
return Err(bad(cmd, "the document has never been saved"));
}
@@ -1413,6 +1420,38 @@ mod tests {
assert!(r.get("warning").is_none(), "{r}");
}
#[test]
fn batch_save_and_close_after_a_redirected_save_writes_each_data_set_to_its_own_file() {
let save_close = |dest: &std::path::Path| {
json!({
"source": "datasets",
"destination": "saveAndClose",
"destinationFolder": dest.to_string_lossy(),
"commands": [{"command": "document.save", "params": {}}]
})
};
let path = tmp("redirected-save-close").join("template.vectorcraft");
let mut s = saved_with_two_data_sets(&path);
let bytes = std::fs::read(&path).unwrap();
let dest = tmp("redirected-save-close-dest");
let p = save_close(&dest);
let mut kept = p.clone();
kept["overrideSave"] = json!(false);
assert!(batch_overwrites_one_file(&s, &kept), "without Override Save the save stays on the template");
let r = s.execute("action.batch", &p).unwrap();
assert_eq!(r["failed"], 0, "{r}");
assert!(r.get("warning").is_none(), "{r}");
assert_eq!(file_names(&dest), ["template_A.vectorcraft", "template_B.vectorcraft"], "{r}");
assert_eq!(std::fs::read(&path).unwrap(), bytes, "the template's file stays as it was");
// A document never saved gets its files from the redirected save too.
let mut unsaved = with_data_sets(&["a", "b"]);
let dest = tmp("redirected-save-close-unsaved");
let r = unsaved.execute("action.batch", &save_close(&dest)).unwrap();
assert_eq!(r["failed"], 0, "{r}");
assert!(r.get("warning").is_none(), "{r}");
assert_eq!(file_names(&dest), ["Untitled_a.vectorcraft", "Untitled_b.vectorcraft"], "{r}");
}
#[test]
fn one_file_rule_covers_save_and_close_and_steps_left_in_place() {
let saved = saved_with_two_data_sets(&tmp("one-file-rule").join("template.vectorcraft"));
+1 -1
View File
@@ -117,7 +117,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; for source datasets, save the open document over its own file after each data set, and refuse a document that was never saved, was converted from an older version or was restored by Data Recovery) | folder (save or export into destinationFolder and close), destinationFolder? (required when destination is folder; also the folder Override Save/Export write into), datasetFileNaming?: fileNumber | fileDataSetName (default) | dataSetName (used only for source datasets; ignored for folder), 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. Data set names use the document stem plus a three-digit ordinal, the document stem and an underscore plus the data set name, or the data set name alone. Invalid file name characters become `-`; a name already used in the batch or whose output already exists in a destination folder the batch may read and write gets the next free `-2`, `-3` suffix before the format-derived extension. Caps: 500 files or data sets, 8 folder levels, 256 steps. Batches cannot nest. → {processed: [{path|dataset, result|error}…], count, failed, stopped, warning?} (warning: for source datasets, when Save and Close, or a Save, Save a Copy, Save as Template or export step (document.export, exportSelection, exportForScreens, exportPdf, exportEps, exportDxf, exportForWeb, exportForOffice) that writes a file and is not pointed into destinationFolder, saves every data set over the same file)",
"{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; for source datasets, save the open document over its own file after each data set, and refuse a document that was never saved, was converted from an older version or was restored by Data Recovery; a Save or Save As step that overrideSave points into destinationFolder makes each data set's file the document's own first) | folder (save or export into destinationFolder and close), destinationFolder? (required when destination is folder; also the folder Override Save/Export write into), datasetFileNaming?: fileNumber | fileDataSetName (default) | dataSetName (used only for source datasets; ignored for folder), 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. Data set names use the document stem plus a three-digit ordinal, the document stem and an underscore plus the data set name, or the data set name alone. Invalid file name characters become `-`; a name already used in the batch or whose output already exists in a destination folder the batch may read and write gets the next free `-2`, `-3` suffix before the format-derived extension. Caps: 500 files or data sets, 8 folder levels, 256 steps. Batches cannot nest. → {processed: [{path|dataset, result|error}…], count, failed, stopped, warning?} (warning: for source datasets, when Save and Close without such a step, or a Save, Save a Copy, Save as Template or export step (document.export, exportSelection, exportForScreens, exportPdf, exportEps, exportDxf, exportForWeb, exportForOffice) that writes a file and is not pointed into destinationFolder, saves every data set over the same file)",
always,
action_batch::action_batch
),
+4 -2
View File
@@ -3517,8 +3517,10 @@ an earlier version 1 set without that flag includes every step. The Actions pane
destination `none` | `saveAndClose` | `folder` and Override Open / Save / Export. With a Data Sets source,
`saveAndClose` saves the open document over its own file after each data set, so the file keeps only the last one,
and a document that was never saved, was converted from an older version or was restored by Data Recovery is
refused before a data set is selected. The result carries `warning` when every data set saves over the same file:
with `saveAndClose`, or with a Save, Save a Copy, Save as Template or export step (PDF, EPS, DXF, Save for Web and
refused before a data set is selected. When `destinationFolder` is set and Override Save points a recorded Save or
Save As step into it, that step makes each data set's file the document's own, and `saveAndClose` saves each data
set to its own file. The result carries `warning` when every data set saves over the same file:
with `saveAndClose` without such a step, or with a Save, Save a Copy, Save as Template or export step (PDF, EPS, DXF, Save for Web and
Save for Office Documents included) that writes a file and that the batch does not point into `destinationFolder`. The dialog shows the same
warning. For a Data Sets source,
`datasetFileNaming?: fileNumber | fileDataSetName | dataSetName` chooses File + Number (`source001`),
+1 -1
View File
@@ -643,7 +643,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Actions panel: the panel menu's Duplicate copies the selected action, else the current action set with all its actions ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.duplicate`). VectorCraft's own rules, which the overview does not cover: the copy goes right after the original, is named after it with " copy" added and is selected.
- 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.
- Batch… offers File + Number, File + Data Set Name, and Data Set Name for a Data Sets source. The dialog defaults to File + Number; `action.batch` without `datasetFileNaming` keeps File + Data Set Name. An output already in a destination folder the batch may read and write gets the next free `-2`, `-3` suffix. [Batch options](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/batch-options.html).
- Batch… over data sets offers None, Save and Close and Folder as the destination, as for a folder source. Save and Close saves the open document over its own file after each data set, so the file keeps only the last data set. A recorded Save, Save a Copy, Save as Template or export step that writes a file and that the batch does not point into the destination folder (destination None, Override Save or Override Export off, or a copy, template, PDF, EPS, DXF, Save for Web or Save for Office Documents step) also writes the same file for every data set. In either case the dialog shows a warning and `action.batch` returns a `warning`. `action.batch` refuses Save and Close over the data sets of a document that was never saved, was converted from an older version or was restored by Data Recovery.
- Batch… over data sets offers None, Save and Close and Folder as the destination, as for a folder source. Save and Close saves the open document over its own file after each data set, so the file keeps only the last data set, unless a recorded Save step that Override Save points into the destination folder first makes each data set's file the document's own. A recorded Save, Save a Copy, Save as Template or export step that writes a file and that the batch does not point into the destination folder (destination None, Override Save or Override Export off, or a copy, template, PDF, EPS, DXF, Save for Web or Save for Office Documents step) also writes the same file for every data set. In either case the dialog shows a warning and `action.batch` returns a `warning`. Without such a Save step, `action.batch` refuses Save and Close over the data sets of a document that was never saved, was converted from an older version or was restored by Data Recovery.
- Export for Screens: the dialog's Example line shows the path of the file the export writes first, sub-folder included, named as the export names it, for the first chosen artboard, asset, layer or object (`document.exportForScreens`).
- Export presets: the Asset Export panel menu lists the export presets of Export for Screens, with Import Preset… and Export Preset…, which read and write `.vcexportpresets` files (`document.exportPresets.import`, `document.exportPresets.export`). An imported preset whose name is taken gets a number. In the panel, a preset applies to the shared export settings (`assets.settings.set {exportPreset}`), and Save Preset… saves them. With Create Sub-folders off, the panel's Export writes every file into the folder itself, mobile and density rows included.
- Export for Screens is laid out as the reference app's dialog: on the left the Artboards and Assets tabs (thumbnails with their checkboxes, number and name, in a grid or a list), Clear Selection and Prefix; on the right Select (All, Range, Include Bleed, Include Artboard Fill, Full Document), Export to, Open Location after Export, Create Sub-folders by Scale or by Format (on by default), Export PDFs As (Single File writes the chosen artboards as the pages of one PDF named after the document, `pdfs: "single"`), and the format rows with the iOS and Android buttons, the format presets (☰) and Format Settings; under them, how many items are selected and how many files the export writes. File name patterns, the Layers and Selection sources and per-item folders stay available through `document.exportForScreens` (scripts and MCP). Tests: `dialogs::tests_screens` (layout, iOS/Android, Clear Selection, sub-folders, Single File) and `fileio::tests_screens::single_file_pdfs_hold_every_chosen_artboard`.