Merge pull request #5015 from dotCooCoo/fix/batch-save-close-data-sets

Actions: Batch over data sets keeps Save and Close and warns when each data set saves over one file
This commit is contained in:
BFlat
2026-11-09 01:14:45 +08:00
committed by GitHub
33 changed files with 428 additions and 8 deletions
+1
View File
@@ -54,6 +54,7 @@ pub mod zip;
use serde_json::{Value, json};
pub use action_batch::batch_overwrites_one_file;
pub use dxf::{UNSUPPORTED, Unsupported, unsupported};
pub use encode::{ARTBOARD_PARAMS, ArtboardPick, Encoded, encode, encode_all, encode_with_warnings};
pub(crate) use encode::{anti_alias, background, with_single_artboard};
+302 -1
View File
@@ -95,11 +95,93 @@ pub(super) fn action_batch(s: &mut Session, p: &Value) -> Result<Value> {
};
match source {
"folder" => batch_folder(s, C, p, override_open, &run),
"datasets" => batch_datasets(s, C, &run),
"datasets" => {
let one_file = batch_overwrites_one_file(s, p);
let mut r = batch_datasets(s, C, &run)?;
if one_file {
r["warning"] = json!(ONE_FILE_WARNING);
}
Ok(r)
}
_ => Err(bad(C, "`source` must be `folder` or `datasets`")),
}
}
/// The `warning` of a Data Sets batch for which [`batch_overwrites_one_file`] holds.
#[cfg(not(target_arch = "wasm32"))]
const ONE_FILE_WARNING: &str = "each data set saves over the same file, which keeps only the last data set";
/// 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, 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
/// converted from an older version or restored by Data Recovery) and the step gives no `format` or
/// the document's own; an Export for Screens step whose `folder`, given or from its
/// `exportPreset`, names a folder, whatever the overrides.
/// Other steps return their data instead of writing a file.
pub fn batch_overwrites_one_file(s: &Session, p: &Value) -> bool {
if p.get("source").and_then(Value::as_str) != Some("datasets") {
return false;
}
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());
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| 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"))]
fn batch_folder(s: &mut Session, cmd: &str, p: &Value, override_open: bool, run: &Run<'_>) -> Result<Value> {
use super::{absolute_path, may_read};
@@ -145,6 +227,18 @@ 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")));
}
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"));
}
// `save_plan` writes no file for a document converted from an older version or restored by
// Data Recovery.
if st.converted || st.recovered {
return Err(bad(cmd, "save the document with Save As before Save and Close"));
}
}
let doc_stem = st.path.as_deref().and_then(|p| std::path::Path::new(p).file_stem().and_then(|s| s.to_str())).unwrap_or("Untitled").to_string();
let stems = dataset_stems(&names, &doc_stem, run.dataset_file_naming, run, &st.doc)?;
let steps = filter_steps(run.steps, true, false);
@@ -1292,6 +1386,213 @@ mod tests {
assert_eq!(file_names(&dest), ["Untitled_A-B-3.vectorcraft", "Untitled_a-b-2.vectorcraft", "Untitled_a-b.vectorcraft"], "{r}");
}
/// A saved document with a text object bound to `Name` and two data sets, `A` and `B`.
fn saved_with_two_data_sets(path: &std::path::Path) -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 100})).unwrap();
let id = s.execute("text.create", &json!({"x": 10, "y": 40, "text": "A"})).unwrap()["id"].as_u64().unwrap();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [id]})).unwrap();
s.execute("dataset.capture", &json!({"name": "A"})).unwrap();
s.execute("text.setText", &json!({"ids": [id], "text": "B"})).unwrap();
s.execute("dataset.capture", &json!({"name": "B"})).unwrap();
s.execute("document.save", &json!({"path": path.to_string_lossy()})).unwrap();
s
}
fn save_and_close_over_data_sets(s: &mut Session) -> Result<Value> {
s.execute(
"action.batch",
&json!({"source": "datasets", "destination": "saveAndClose", "commands": [{"command": "object.unlockAll", "params": {}}]}),
)
}
#[test]
fn batch_save_and_close_over_data_sets_saves_each_over_the_template_and_warns() {
let path = tmp("save-close-sets").join("template.vectorcraft");
let mut s = saved_with_two_data_sets(&path);
std::fs::write(&path, b"stale").unwrap();
let r = save_and_close_over_data_sets(&mut s).unwrap();
assert_eq!((r["count"].as_u64(), r["failed"].as_u64()), (Some(2), Some(0)), "{r}");
assert_eq!(r["warning"], ONE_FILE_WARNING, "{r}");
assert_ne!(std::fs::read(&path).unwrap(), b"stale", "the template's file is saved over");
assert_eq!(file_names(path.parent().unwrap()), ["template.vectorcraft"], "{r}");
let st = s.doc().unwrap();
assert_eq!(st.doc.variables.active_dataset.as_deref(), Some("B"), "the last data set is applied");
assert_eq!(st.path.as_deref(), Some(path.to_string_lossy().as_ref()), "the template stays open");
assert!(!st.is_dirty(), "the last data set is saved");
}
#[test]
fn batch_warns_when_a_recorded_save_writes_one_file_for_every_data_set() {
let path = tmp("recorded-save-sets").join("template.vectorcraft");
let mut s = saved_with_two_data_sets(&path);
let save = json!([{"command": "document.save", "params": {}}]);
let r = s.execute("action.batch", &json!({"source": "datasets", "commands": save})).unwrap();
assert_eq!(r["failed"], 0, "{r}");
assert_eq!(r["warning"], ONE_FILE_WARNING, "{r}");
let dest = tmp("recorded-save-dest");
let p = json!({"source": "datasets", "destination": "folder", "destinationFolder": dest.to_string_lossy(), "commands": save});
let r = s.execute("action.batch", &p).unwrap();
assert!(r.get("warning").is_none(), "Override Save points each save into the folder: {r}");
let unlock = json!({"source": "datasets", "commands": [{"command": "object.unlockAll", "params": {}}]});
let r = s.execute("action.batch", &unlock).unwrap();
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}");
// 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]
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"));
let unsaved = with_data_sets(&["a"]);
let with = |extra: Value| {
let mut p = json!({"source": "datasets", "commands": [{"command": "document.export", "params": {"path": "/x/a.png"}}]});
for (k, v) in extra.as_object().unwrap() {
p[k] = v.clone();
}
batch_overwrites_one_file(&saved, &p)
};
assert!(with(json!({})), "an export with its own path");
assert!(!with(json!({"destination": "folder", "destinationFolder": "/out"})));
assert!(with(json!({"destination": "folder", "destinationFolder": "/out", "overrideExport": false})));
assert!(!with(json!({"destination": "folder", "destinationFolder": "/out", "overrideSave": false})));
assert!(!with(json!({"source": "folder"})), "a folder source opens a file per pass");
assert!(with(json!({"destination": "saveAndClose", "commands": [{"command": "object.unlockAll"}]})));
let saved_as = json!({"commands": [{"command": "file.saveAs", "params": {"path": "/x/a.svg"}}]});
assert!(with(saved_as.clone()));
assert!(!with(json!({"destination": "folder", "destinationFolder": "/out", "commands": saved_as["commands"]})));
assert!(!with(json!({"commands": [{"command": "file.saveAs", "params": {}}]})), "no path: the data comes back");
assert!(!with(json!({"commands": [{"command": "document.export", "params": {"format": "png"}}]})), "no path: the data comes back");
let copy = json!([{"command": "file.saveCopy", "params": {"path": "/x/a.vectorcraft"}}]);
assert!(with(json!({"destination": "folder", "destinationFolder": "/out", "commands": copy})), "Override Save leaves copies alone");
let save = |format: Option<&str>| {
let params = format.map_or(json!({}), |f| json!({"format": f}));
json!({"source": "datasets", "commands": [{"command": "document.save", "params": params}]})
};
assert!(batch_overwrites_one_file(&saved, &save(None)), "the document's own file");
assert!(batch_overwrites_one_file(&saved, &save(Some("vectorcraft"))), "the document's own format");
assert!(!batch_overwrites_one_file(&unsaved, &save(None)), "a document never saved has no file to write");
assert!(!batch_overwrites_one_file(&saved, &save(Some("svg"))), "another format without a path: the data comes back");
assert!(!with(json!({"commands": [{"command": "file.saveCopy", "params": {"folder": "/x"}}]})), "a folder alone: the data comes back");
let screens = json!([{"command": "document.exportForScreens", "params": {"folder": "/x"}}]);
assert!(with(json!({"destination": "folder", "destinationFolder": "/out", "commands": screens})), "Override Export leaves its folder");
assert!(!with(json!({"commands": [{"command": "document.exportForScreens", "params": {}}]})));
let preset = json!([{"command": "document.exportForScreens", "params": {"exportPreset": "Logo Package"}}]);
assert!(!with(json!({"commands": preset})), "a preset without a folder: the data comes back");
for id in ["document.exportPdf", "document.exportEps", "document.exportDxf", "document.exportForWeb", "document.exportForOffice"] {
let step = json!([{"command": id, "params": {"path": "/x/a.out"}}]);
assert!(with(json!({"destination": "folder", "destinationFolder": "/out", "commands": step})), "{id} keeps its path");
assert!(!with(json!({"commands": [{"command": id, "params": {}}]})), "{id} without a path: the data comes back");
}
}
#[test]
fn batch_refuses_save_and_close_over_data_sets_of_a_converted_or_recovered_document() {
for converted in [true, false] {
let path = tmp(if converted { "converted-sets" } else { "recovered-sets" }).join("template.vectorcraft");
let mut s = saved_with_two_data_sets(&path);
let bytes = std::fs::read(&path).unwrap();
let (active, revision) = {
let st = s.doc_mut().unwrap();
if converted {
st.converted = true;
} else {
st.recovered = true;
}
(st.doc.variables.active_dataset.clone(), st.revision)
};
let msg = save_and_close_over_data_sets(&mut s).unwrap_err().to_string();
assert!(msg.ends_with("save the document with Save As before Save and Close"), "{msg}");
assert_eq!(std::fs::read(&path).unwrap(), bytes, "the file is not written");
let st = s.doc().unwrap();
assert_eq!(st.doc.variables.active_dataset, active, "no data set is selected");
assert_eq!(st.revision, revision, "no step runs");
}
}
#[test]
fn batch_refuses_save_and_close_over_data_sets_of_an_unsaved_document() {
let mut s = with_data_sets(&["a", "b"]);
let (active, revision) = {
let st = s.doc().unwrap();
(st.doc.variables.active_dataset.clone(), st.revision)
};
let msg = save_and_close_over_data_sets(&mut s).unwrap_err().to_string();
assert!(msg.ends_with("the document has never been saved"), "{msg}");
let st = s.doc().unwrap();
assert_eq!(st.doc.variables.active_dataset, active, "no data set is selected");
assert_eq!(st.revision, revision, "no step runs");
assert!(st.path.is_none());
}
#[test]
fn batch_save_and_close_saves_and_closes_each_source_file() {
let src = tmp("save-close-src");
let a = write_svg(&src, "a.svg", "#f00");
let before = std::fs::read(&a).unwrap();
let mut s = Session::new();
let r = s
.execute(
"action.batch",
&json!({"folder": src.to_string_lossy(), "destination": "saveAndClose", "commands": [{"command": "object.unlockAll", "params": {}}]}),
)
.unwrap();
assert_eq!(r["failed"], 0, "{r}");
assert_ne!(std::fs::read(&a).unwrap(), before, "the source file is saved over");
assert_eq!(file_names(&src), ["a.svg"], "{r}");
assert!(s.doc().is_err(), "the source file is closed");
}
#[test]
fn batch_refuses_nesting_and_empty_commands() {
let mut s = Session::new();
+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) | 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}",
"{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
),
+1 -1
View File
@@ -670,7 +670,7 @@ fn find_preset(s: &Session, name: &str) -> Option<ScreenExportPreset> {
}
/// `p` over the settings of its `exportPreset`, when it names one (its own keys win).
fn with_preset(s: &Session, p: &Value) -> Result<Value> {
pub(super) fn with_preset(s: &Session, p: &Value) -> Result<Value> {
let Some(name) = str_param(p, "exportPreset") else { return Ok(p.clone()) };
let preset = find_preset(s, name).ok_or_else(|| bad(C, format!("no export preset `{name}` (see document.exportPresets.list)")))?;
let mut merged = preset.settings;
+60 -4
View File
@@ -92,9 +92,23 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
form::check(ui, d, "overrideSave", "Override Action \"Save\" Commands");
form::check(ui, d, "overrideExport", "Override Action \"Export\" Commands");
form::check(ui, d, "stopOnError", "Stop on Errors");
if overwrites_one_file(app, d) {
ui.add_space(6.0);
ui.label(egui::RichText::new(tl!("Each data set saves over the same file, which keeps only the last data set.")).size(11.5).color(t.warning));
}
false
}
/// Whether the batch the dialog would run saves every data set over the same file
/// ([`vectorcraft_engine::cmd::fileio::batch_overwrites_one_file`]).
fn overwrites_one_file(app: &VectorcraftApp, d: &Dialog) -> bool {
let set = d.fields.get("set").and_then(Value::as_u64).unwrap_or(0) as usize;
let action = d.fields.get("action").and_then(Value::as_u64).unwrap_or(0) as usize;
let steps = app.ui.action_sets.get(set).and_then(|s| s.actions.get(action)).map_or(&[][..], |a| &a.steps[..]);
let commands = steps.iter().map(|(c, p)| json!({"command": c, "params": p})).collect();
vectorcraft_engine::cmd::fileio::batch_overwrites_one_file(&app.session, &params(d, "", commands))
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let set = d.fields.get("set").and_then(Value::as_u64).unwrap_or(0) as usize;
let action = d.fields.get("action").and_then(Value::as_u64).unwrap_or(0) as usize;
@@ -161,6 +175,18 @@ mod tests {
use vectorcraft_engine::Session;
fn shown(source: &str) -> Vec<String> {
let mut d = Dialog::new(KIND, initial_fields(0, 0, "Test"));
d.fields["source"] = json!(source);
render(&mut d)
}
/// Draws `d` once and returns the texts it shows.
fn render(d: &mut Dialog) -> Vec<String> {
render_in(&mut VectorcraftApp::new(Session::new(), Default::default()), d)
}
/// Draws `d` once over `app` and returns the texts it shows.
fn render_in(app: &mut VectorcraftApp, d: &mut Dialog) -> Vec<String> {
fn collect(shape: &egui::Shape, out: &mut Vec<String>) {
match shape {
egui::Shape::Text(t) => out.push(t.galley.text().to_string()),
@@ -168,13 +194,10 @@ mod tests {
_ => {}
}
}
let mut app = VectorcraftApp::new(Session::new(), Default::default());
let mut d = Dialog::new(KIND, initial_fields(0, 0, "Test"));
d.fields["source"] = json!(source);
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let mut frame = ctx.run_ui(Default::default(), |ui| {
body(&mut app, ui, &mut d);
body(app, ui, d);
});
frame.textures_delta.clear();
let mut labels = vec![];
@@ -220,4 +243,37 @@ mod tests {
d.fields["source"] = json!("folder");
assert!(params(&d, "Test", vec![]).get("datasetFileNaming").is_none());
}
const ONE_FILE: &str = "Each data set saves over the same file, which keeps only the last data set.";
#[test]
fn data_sets_offer_save_and_close_with_a_warning() {
let mut d = Dialog::new(KIND, initial_fields(0, 0, "Test"));
d.fields["destination"] = json!("saveAndClose");
let labels = render(&mut d);
assert!(labels.iter().any(|label| label == "Save and Close"), "{labels:?}");
assert!(!labels.iter().any(|label| label == ONE_FILE), "a folder source opens a file per pass: {labels:?}");
d.fields["source"] = json!("datasets");
let labels = render(&mut d);
assert!(labels.iter().any(|label| label == "Save and Close"), "{labels:?}");
assert!(labels.iter().any(|label| label == ONE_FILE), "{labels:?}");
assert_eq!(params(&d, "Test", vec![])["destination"], "saveAndClose");
d.fields["destination"] = json!("folder");
assert!(!render(&mut d).iter().any(|label| label == ONE_FILE));
}
#[test]
fn data_sets_warn_about_a_recorded_save_left_in_place() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.ui.action_sets[0].actions[0].steps.push(("document.save".into(), json!({"path": "/x/a.vectorcraft"})));
let mut d = Dialog::new(KIND, initial_fields(0, 0, "Test"));
d.fields["source"] = json!("datasets");
d.fields["destinationFolder"] = json!("/out");
assert!(!render_in(&mut app, &mut d).iter().any(|label| label == ONE_FILE), "Override Save points the save into the folder");
d.fields["overrideSave"] = json!(false);
assert!(render_in(&mut app, &mut d).iter().any(|label| label == ONE_FILE));
d.fields["overrideSave"] = json!(true);
d.fields["destination"] = json!("none");
assert!(render_in(&mut app, &mut d).iter().any(|label| label == ONE_FILE));
}
}
+2
View File
@@ -6099,6 +6099,8 @@ breakText Glyphs Глифи
File + Data Set Name Файл + име на набора от данни
Data Set Name Име на набора от данни
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` трябва да е `fileNumber`, `fileDataSetName` или `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Всеки набор от данни се записва върху същия файл, в който остава само последният набор от данни.
@msg save the document with Save As before Save and Close запазете документа със „Запази като“ преди „Запази и затвори“
@msg no free data set file name in destination folder Няма свободно име за файл на набор от данни в целевата папка
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6111,6 +6111,8 @@ shape Open Otvoren
File + Data Set Name Datoteka + naziv skupa podataka
Data Set Name Naziv skupa podataka
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` mora biti `fileNumber`, `fileDataSetName` ili `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Svaki skup podataka se sačuva preko iste datoteke, u kojoj ostaje samo posljednji skup podataka.
@msg save the document with Save As before Save and Close sačuvajte dokument pomoću „Sačuvaj kao“ prije „Sačuvaj i zatvori“
@msg no free data set file name in destination folder Nema slobodnog naziva datoteke skupa podataka u odredišnoj mapi
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6255,6 +6255,8 @@ breakText Glyphs Glifs
File + Data Set Name Fitxer + nom del conjunt de dades
Data Set Name Nom del conjunt de dades
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` ha de ser `fileNumber`, `fileDataSetName` o `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Cada conjunt de dades es desa sobre el mateix fitxer, que només conserva l'últim conjunt de dades.
@msg save the document with Save As before Save and Close deseu el document amb «Desa com a» abans de «Desa i tanca»
@msg no free data set file name in destination folder No hi ha cap nom de fitxer de conjunt de dades lliure a la carpeta de destinació
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6274,6 +6274,8 @@ breakText Glyphs Glyfy
File + Data Set Name Soubor + název sady dat
Data Set Name Název sady dat
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` musí být `fileNumber`, `fileDataSetName` nebo `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Každá sada dat se uloží přes stejný soubor, ve kterém zůstane jen poslední sada dat.
@msg save the document with Save As before Save and Close před „Uložit a zavřít“ uložte dokument pomocí „Uložit jako“
@msg no free data set file name in destination folder V cílové složce není volný název souboru sady dat
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6114,6 +6114,8 @@ breakText Glyphs Glyffer
File + Data Set Name Fil + datasætnavn
Data Set Name Datasætnavn
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` skal være `fileNumber`, `fileDataSetName` eller `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Hvert datasæt gemmes oven i den samme fil, som kun beholder det sidste datasæt.
@msg save the document with Save As before Save and Close gem dokumentet med Gem som før Gem og luk
@msg no free data set file name in destination folder Intet ledigt filnavn til datasættet i destinationsmappen
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5859,6 +5859,8 @@ breakText Glyphs Glyphen
File + Data Set Name Datei + Datensatzname
Data Set Name Datensatzname
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` muss `fileNumber`, `fileDataSetName` oder `dataSetName` sein
Each data set saves over the same file, which keeps only the last data set. Jeder Datensatz wird über dieselbe Datei gespeichert, die nur den letzten Datensatz behält.
@msg save the document with Save As before Save and Close speichern Sie das Dokument mit „Speichern unter“, bevor Sie „Speichern und schließen“ wählen
@msg no free data set file name in destination folder Kein freier Dateiname für den Datensatz im Zielordner
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6225,6 +6225,8 @@ breakText Glyphs Γλύφοι
File + Data Set Name Αρχείο + όνομα συνόλου δεδομένων
Data Set Name Όνομα συνόλου δεδομένων
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` Το `datasetFileNaming` πρέπει να είναι `fileNumber`, `fileDataSetName` ή `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Κάθε σύνολο δεδομένων αποθηκεύεται πάνω στο ίδιο αρχείο, το οποίο κρατά μόνο το τελευταίο σύνολο δεδομένων.
@msg save the document with Save As before Save and Close αποθηκεύστε το έγγραφο με «Αποθήκευση ως» πριν από «Αποθήκευση και κλείσιμο»
@msg no free data set file name in destination folder Δεν υπάρχει διαθέσιμο όνομα αρχείου συνόλου δεδομένων στον φάκελο προορισμού
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 168 and column 5.
+2
View File
@@ -6216,6 +6216,8 @@ breakText Glyphs Glifos
File + Data Set Name Archivo + nombre del conjunto de datos
Data Set Name Nombre del conjunto de datos
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` debe ser `fileNumber`, `fileDataSetName` o `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Cada conjunto de datos se guarda sobre el mismo archivo, que solo conserva el último conjunto de datos.
@msg save the document with Save As before Save and Close guarde el documento con «Guardar como» antes de «Guardar y cerrar»
@msg no free data set file name in destination folder No hay un nombre de archivo libre para el conjunto de datos en la carpeta de destino
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6304,6 +6304,8 @@ breakText Glyphs Merkit
File + Data Set Name Tiedosto + tietojoukon nimi
Data Set Name Tietojoukon nimi
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming`-arvon on oltava `fileNumber`, `fileDataSetName` tai `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Jokainen tietojoukko tallennetaan saman tiedoston päälle, johon jää vain viimeinen tietojoukko.
@msg save the document with Save As before Save and Close tallenna asiakirja komennolla Tallenna nimellä ennen kuin valitset Tallenna ja sulje
@msg no free data set file name in destination folder Kohdekansiossa ei ole vapaata tietojoukon tiedostonimeä
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6269,6 +6269,8 @@ breakText Glyphs Glyphes
File + Data Set Name Fichier + nom du jeu de données
Data Set Name Nom du jeu de données
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` doit être `fileNumber`, `fileDataSetName` ou `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Chaque jeu de données est enregistré sur le même fichier, qui ne conserve que le dernier jeu de données.
@msg save the document with Save As before Save and Close enregistrez le document avec Enregistrer sous avant Enregistrer et fermer
@msg no free data set file name in destination folder Aucun nom de fichier libre pour le jeu de données dans le dossier de destination
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6258,6 +6258,8 @@ breakText Glyphs Glifák
File + Data Set Name Fájl + adatkészlet neve
Data Set Name Adatkészlet neve
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` A `datasetFileNaming` értéke `fileNumber`, `fileDataSetName` vagy `dataSetName` lehet
Each data set saves over the same file, which keeps only the last data set. Minden adatkészlet ugyanabba a fájlba mentődik, amely csak az utolsó adatkészletet őrzi meg.
@msg save the document with Save As before Save and Close mentse a dokumentumot a „Mentés másként” paranccsal a „Mentés és bezárás” előtt
@msg no free data set file name in destination folder Nincs szabad adatkészlet-fájlnév a célmappában
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6114,6 +6114,8 @@ breakText Glyphs Glif
File + Data Set Name File + nama set data
Data Set Name Nama set data
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` harus `fileNumber`, `fileDataSetName`, atau `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Setiap set data disimpan menimpa file yang sama, sehingga hanya set data terakhir yang tersisa.
@msg save the document with Save As before Save and Close simpan dokumen dengan Simpan sebagai sebelum Simpan dan tutup
@msg no free data set file name in destination folder Tidak ada nama file set data yang tersedia di folder tujuan
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2260 and column 5.
+2
View File
@@ -5893,6 +5893,8 @@ breakText Glyphs Glifi
File + Data Set Name File + nome set di dati
Data Set Name Nome set di dati
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` deve essere `fileNumber`, `fileDataSetName` o `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Ogni set di dati viene salvato sullo stesso file, che conserva solo l'ultimo set di dati.
@msg save the document with Save As before Save and Close salva il documento con «Salva con nome» prima di «Salva e chiudi»
@msg no free data set file name in destination folder Nessun nome file libero per il set di dati nella cartella di destinazione
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5958,6 +5958,8 @@ breakText Glyphs 字形
File + Data Set Name ファイル名 + データセット名
Data Set Name データセット名
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` は `fileNumber`、`fileDataSetName`、`dataSetName` のいずれかである必要があります
Each data set saves over the same file, which keeps only the last data set. 各データセットが同じファイルに上書き保存されるため、最後のデータセットだけが残ります。
@msg save the document with Save As before Save and Close 「保存して閉じる」の前に「別名で保存」でドキュメントを保存してください
@msg no free data set file name in destination folder 保存先フォルダーに使用可能なデータセットのファイル名がありません
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6271,6 +6271,8 @@ breakText Glyphs გლიფები
File + Data Set Name ფაილი + მონაცემთა ნაკრების სახელი
Data Set Name მონაცემთა ნაკრების სახელი
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` უნდა იყოს `fileNumber`, `fileDataSetName` ან `dataSetName`
Each data set saves over the same file, which keeps only the last data set. თითოეული მონაცემთა ნაკრები ერთსა და იმავე ფაილზე ინახება, რომელშიც მხოლოდ ბოლო მონაცემთა ნაკრები რჩება.
@msg save the document with Save As before Save and Close „შენახვა და დახურვამდე“ შეინახეთ დოკუმენტი „შენახვა როგორც“ ბრძანებით
@msg no free data set file name in destination folder დანიშნულების საქაღალდეში მონაცემთა ნაკრებისთვის თავისუფალი ფაილის სახელი არ არის
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it is too large.
+2
View File
@@ -5854,6 +5854,8 @@ breakText Glyphs 글리프
File + Data Set Name 파일 + 데이터 세트 이름
Data Set Name 데이터 세트 이름
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming`은 `fileNumber`, `fileDataSetName` 또는 `dataSetName`이어야 합니다
Each data set saves over the same file, which keeps only the last data set. 각 데이터 세트가 같은 파일에 덮어써지므로 마지막 데이터 세트만 남습니다.
@msg save the document with Save As before Save and Close '저장 후 닫기' 전에 '다른 이름으로 저장'으로 문서를 저장하십시오
@msg no free data set file name in destination folder 대상 폴더에 사용할 수 있는 데이터 세트 파일 이름이 없습니다
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6160,6 +6160,8 @@ breakText Glyphs Glyphs
File + Data Set Name Bestand + naam gegevensset
Data Set Name Naam gegevensset
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` moet `fileNumber`, `fileDataSetName` of `dataSetName` zijn
Each data set saves over the same file, which keeps only the last data set. Elke gegevensset wordt over hetzelfde bestand opgeslagen, dat alleen de laatste gegevensset bewaart.
@msg save the document with Save As before Save and Close sla het document op met Opslaan als voordat u Opslaan en sluiten kiest
@msg no free data set file name in destination folder Geen vrije bestandsnaam voor de gegevensset in de doelmap
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6128,6 +6128,8 @@ breakText Glyphs Glify
File + Data Set Name Plik + nazwa zestawu danych
Data Set Name Nazwa zestawu danych
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` musi mieć wartość `fileNumber`, `fileDataSetName` lub `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Każdy zestaw danych jest zapisywany w tym samym pliku, który zachowuje tylko ostatni zestaw danych.
@msg save the document with Save As before Save and Close zapisz dokument poleceniem „Zapisz jako” przed „Zapisz i zamknij”
@msg no free data set file name in destination folder Brak wolnej nazwy pliku zestawu danych w folderze docelowym
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6190,6 +6190,8 @@ breakText Glyphs Glifos
File + Data Set Name Arquivo + nome do conjunto de dados
Data Set Name Nome do conjunto de dados
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` deve ser `fileNumber`, `fileDataSetName` ou `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Cada conjunto de dados é salvo sobre o mesmo arquivo, que mantém apenas o último conjunto de dados.
@msg save the document with Save As before Save and Close salve o documento com Salvar Como antes de Salvar e fechar
@msg no free data set file name in destination folder Não há nome de arquivo livre para o conjunto de dados na pasta de destino
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -6251,6 +6251,8 @@ breakText Glyphs Глифы
File + Data Set Name Файл + имя набора данных
Data Set Name Имя набора данных
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` должно быть `fileNumber`, `fileDataSetName` или `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Каждый набор данных сохраняется поверх одного и того же файла, в котором остаётся только последний набор данных.
@msg save the document with Save As before Save and Close сохраните документ командой «Сохранить как» перед «Сохранить и закрыть»
@msg no free data set file name in destination folder В папке назначения нет свободного имени файла для набора данных
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6326,6 +6326,8 @@ breakText Glyphs Glyfer
File + Data Set Name Fil + datauppsättningens namn
Data Set Name Datauppsättningens namn
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` måste vara `fileNumber`, `fileDataSetName` eller `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Varje datauppsättning sparas över samma fil, som bara behåller den sista datauppsättningen.
@msg save the document with Save As before Save and Close spara dokumentet med Spara som före Spara och stäng
@msg no free data set file name in destination folder Inget ledigt filnamn för datauppsättningen i målmappen
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -6231,6 +6231,8 @@ breakText Glyphs Glifler
File + Data Set Name Dosya + veri kümesi adı
Data Set Name Veri kümesi adı
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` değeri `fileNumber`, `fileDataSetName` veya `dataSetName` olmalıdır
Each data set saves over the same file, which keeps only the last data set. Her veri kümesi aynı dosyanın üzerine kaydedilir ve dosyada yalnızca son veri kümesi kalır.
@msg save the document with Save As before Save and Close Kaydet ve Kapat'tan önce belgeyi Farklı Kaydet ile kaydedin
@msg no free data set file name in destination folder Hedef klasörde veri kümesi için boş bir dosya adı yok
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -5873,6 +5873,8 @@ breakText Glyphs Гліфи
File + Data Set Name Файл + назва набору даних
Data Set Name Назва набору даних
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` має бути `fileNumber`, `fileDataSetName` або `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Кожен набір даних зберігається поверх того самого файлу, у якому лишається тільки останній набір даних.
@msg save the document with Save As before Save and Close збережіть документ командою «Зберегти як» перед «Зберегти й закрити»
@msg no free data set file name in destination folder У папці призначення немає вільного імені файлу набору даних
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 182 and column 5.
+2
View File
@@ -5874,6 +5874,8 @@ breakText Glyphs Glyph
File + Data Set Name Tệp + tên tập dữ liệu
Data Set Name Tên tập dữ liệu
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` phải là `fileNumber`, `fileDataSetName` hoặc `dataSetName`
Each data set saves over the same file, which keeps only the last data set. Mỗi tập dữ liệu được lưu đè lên cùng một tệp, nên tệp chỉ giữ lại tập dữ liệu cuối cùng.
@msg save the document with Save As before Save and Close hãy lưu tài liệu bằng Lưu thành trước khi dùng Lưu và đóng
@msg no free data set file name in destination folder Không có tên tệp tập dữ liệu còn trống trong thư mục đích
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 204 and column 5.
+2
View File
@@ -6115,6 +6115,8 @@ breakText Glyphs 字形
File + Data Set Name 文件 + 数据集名称
Data Set Name 数据集名称
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` 必须是 `fileNumber`、`fileDataSetName` 或 `dataSetName`
Each data set saves over the same file, which keeps only the last data set. 每个数据集都会存储到同一个文件并覆盖它,该文件只保留最后一个数据集。
@msg save the document with Save As before Save and Close 请先用“另存为”保存文档,再使用“保存并关闭”
@msg no free data set file name in destination folder 目标文件夹中没有可用的数据集文件名
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5911,6 +5911,8 @@ breakText Glyphs 字符
File + Data Set Name 檔案 + 資料集名稱
Data Set Name 資料集名稱
@msg `datasetFileNaming` must be `fileNumber`, `fileDataSetName` or `dataSetName` `datasetFileNaming` 必須是 `fileNumber`、`fileDataSetName` 或 `dataSetName`
Each data set saves over the same file, which keeps only the last data set. 每個資料集都會儲存到同一個檔案並覆寫它,該檔案只保留最後一個資料集。
@msg save the document with Save As before Save and Close 請先用「另存新檔」儲存文件,再使用「儲存並關閉」
@msg no free data set file name in destination folder 目的地資料夾中沒有可用的資料集檔案名稱
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Can't render this file because it contains an unexpected character in line 6 and column 47.
+10 -1
View File
@@ -3514,7 +3514,16 @@ Actions panel plays an action as a `command.batch`, omitting steps whose playbac
cleared. An action set saved as `.vcactions` keeps each excluded step with `excluded: true`; loading
an earlier version 1 set without that flag includes every step. The Actions panel menu's Batch… runs
`action.batch`: the same steps on every readable file in a folder (or every data set of the open document), with
destination `none` | `saveAndClose` | `folder` and Override Open / Save / Export. For a Data Sets source,
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, 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
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`),
File + Data Set Name (`source_Alpha`), or Data Set Name (`Alpha`). An omitted parameter keeps File + Data Set Name
for existing callers; the dialog starts with File + Number. A folder source ignores this parameter. The folder
+1
View File
@@ -643,6 +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`. 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`.