From 0d1a8b6ffa838cb46ce6d252f1882a604de63a82 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 07:21:38 -0800 Subject: [PATCH] Actions: Batch Save and Close over data sets counts only saves that make the file the document's own A recorded save in the template format writes a copy and leaves the document's file as it was, so it no longer clears the warning or the refusals. A recorded Save or Save As with its own path gives a never-saved, converted or restored document its file before Save and Close, so the batch runs and warns that every data set saves over that one file. --- crates/engine/src/cmd/fileio/action_batch.rs | 43 +++++++++++++++++++- crates/engine/src/cmd/fileio/commands.rs | 2 +- docs/mcp.md | 3 +- docs/target-app-parity.md | 2 +- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/fileio/action_batch.rs b/crates/engine/src/cmd/fileio/action_batch.rs index 942f58e77..ded4dcd5d 100644 --- a/crates/engine/src/cmd/fileio/action_batch.rs +++ b/crates/engine/src/cmd/fileio/action_batch.rs @@ -161,7 +161,25 @@ pub fn batch_overwrites_one_file(s: &Session, p: &Value) -> bool { /// 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"))) + redirected && steps.iter().any(|c| retargeting_save(c, true)) +} + +/// Whether step `c` is a Save, Save As or `document.save` that writes a file (its own `path`, or one +/// in the destination folder when `redirected`) and makes that file the document's own: a save in +/// the native template format writes a copy and leaves the document's file as it was. +fn retargeting_save(c: &Value, redirected: bool) -> bool { + if !matches!(c.get("command").and_then(Value::as_str), Some("document.save" | "file.save" | "file.saveAs")) { + return false; + } + let params = c.get("params"); + let path = params.and_then(|p| p.get("path")).and_then(Value::as_str).filter(|p| !p.is_empty()); + let format_ok = match params.and_then(|p| p.get("format")).and_then(Value::as_str).filter(|f| !f.is_empty()) { + Some(f) => super::format(f).is_some_and(|f| f.id != "template"), + // Override Save gives the step a native file name; else its own path names the format. + None if redirected => true, + None => path.and_then(super::format_for_name).is_none_or(|f| f.id != "template"), + }; + format_ok && (redirected || path.is_some()) } #[cfg(not(target_arch = "wasm32"))] @@ -209,7 +227,9 @@ fn batch_datasets(s: &mut Session, cmd: &str, run: &Run<'_>) -> Result { if names.len() > MAX_BATCH_FILES { return Err(bad(cmd, format!("more than {MAX_BATCH_FILES} data sets"))); } - if run.destination == "saveAndClose" && !saves_move_to_each_file(run.steps, run.override_save && run.dest_folder.is_some()) { + let redirected = run.override_save && run.dest_folder.is_some(); + // A recorded save with a file to write gives the document a file of its own before Save and Close. + if run.destination == "saveAndClose" && !run.steps.iter().any(|c| retargeting_save(c, redirected)) { if st.path.is_none() { return Err(bad(cmd, "the document has never been saved")); } @@ -1450,6 +1470,25 @@ mod tests { 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}"); + // A template save writes a copy: Save and Close still saves over the document's own file. + let mut template = save_close(&dest); + template["commands"] = json!([{"command": "document.save", "params": {"format": "template"}}]); + assert!(batch_overwrites_one_file(&s, &template), "{template}"); + let mut never = with_data_sets(&["a", "b"]); + let msg = never.execute("action.batch", &template).unwrap_err().to_string(); + assert!(msg.ends_with("the document has never been saved"), "{msg}"); + // A recorded Save As with its own path gives a never-saved document its file, the same one + // for every data set. + let one = tmp("save-as-then-close").join("one.vectorcraft"); + let p = json!({ + "source": "datasets", + "destination": "saveAndClose", + "commands": [{"command": "file.saveAs", "params": {"path": one.to_string_lossy()}}] + }); + let r = never.execute("action.batch", &p).unwrap(); + assert_eq!(r["failed"], 0, "{r}"); + assert_eq!(r["warning"], ONE_FILE_WARNING, "{r}"); + assert!(one.exists(), "{r}"); } #[test] diff --git a/crates/engine/src/cmd/fileio/commands.rs b/crates/engine/src/cmd/fileio/commands.rs index 65872508f..b71ede096 100644 --- a/crates/engine/src/cmd/fileio/commands.rs +++ b/crates/engine/src/cmd/fileio/commands.rs @@ -117,7 +117,7 @@ pub fn specs() -> Vec { "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; 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)", + "{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, unless a recorded Save or Save As step writes a file first; a Save or Save As step that overrideSave points into destinationFolder makes each data set's file the document's own) | 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 ), diff --git a/docs/mcp.md b/docs/mcp.md index 312230a86..facbe02e4 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3517,7 +3517,8 @@ 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. When `destinationFolder` is set and Override Save points a recorded Save or +refused before a data set is selected, unless a recorded Save or Save As step (not in the template format) writes a +file and makes it the document's own first. 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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index cddb2ecb9..794ab3fb8 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -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, 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. + - 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`. Unless a recorded Save or Save As step writes a file and makes it the document's own first, `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`.