M1: Hide when publishing from the Assets panel
component.hideWhenPublishing {ids?, on?} keeps this file's components and
sets out of the library it publishes. The model's sign of it is the name,
so the command writes it: hiding puts a "." before the name, showing takes
the leading "." and "_" away (one undo step, saved and synced like a
rename). The Assets panel's context menu on a local component toggles it
(checked while hidden) and the tile shows a crossed eye; also under
Libraries in the main menu.
This commit is contained in:
parent
485715f11e
commit
ce094f397a
|
|
@ -249,6 +249,7 @@ pub static MENU: &[MenuItem] = &[
|
|||
// Libraries
|
||||
m!("Libraries", "Open libraries", "libraries.open"),
|
||||
m!("Libraries", "Publish library…", "libraries.publish"),
|
||||
m!("Libraries", "Hide when publishing", "component.hideWhenPublishing"),
|
||||
// Help
|
||||
m!("Help", "Keyboard shortcuts", "help.shortcuts"),
|
||||
m!("Help", "Help page", "help.page"),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,32 @@ use crate::components::*;
|
|||
use crate::params::*;
|
||||
use crate::{EngineError, Result, Session, bad, spec};
|
||||
|
||||
/// `component.hideWhenPublishing`: the leading `.` of the names of this file's components and
|
||||
/// sets (those `ids` are or are in), put on or taken off.
|
||||
fn hide_when_publishing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use crate::library::{asset_root, hidden_name, with_hidden_name};
|
||||
let cmd = "component.hideWhenPublishing";
|
||||
let doc = s.doc()?;
|
||||
let mut ids: Vec<NodeId> = Vec::new();
|
||||
for i in req_targets(cmd, s, p)? {
|
||||
let root = asset_root(doc, i).ok_or_else(|| bad(cmd, format!("{i} isn't a component, a component set or a layer in one")))?;
|
||||
if doc.is_remote(root) {
|
||||
return Err(EngineError::Disabled(cmd.into(), crate::library::READ_ONLY.into()));
|
||||
}
|
||||
if !ids.contains(&root) {
|
||||
ids.push(root);
|
||||
}
|
||||
}
|
||||
let hidden = |doc: &Document, i: &NodeId| doc.node(*i).is_some_and(|n| hidden_name(&n.name));
|
||||
let on = boolean(p, "on").unwrap_or_else(|| !ids.iter().all(|i| hidden(doc, i)));
|
||||
let doc = s.doc_mut()?;
|
||||
for i in &ids {
|
||||
let n = doc.get_mut(*i)?;
|
||||
n.name = with_hidden_name(&n.name, on);
|
||||
}
|
||||
Ok(json!({"ids": ids, "hidden": on}))
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
spec!(
|
||||
|
|
@ -121,6 +147,13 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
true,
|
||||
reset_slot
|
||||
),
|
||||
spec!(
|
||||
"component.hideWhenPublishing",
|
||||
"Hide when publishing",
|
||||
"{ids? (components, sets or layers in them; default the selection), on? (default: toggle, hiding unless all are hidden)} keeps them out of the library this file publishes, the way a name starting with `.` or `_` does: hiding puts a `.` before the name, showing takes the leading `.` and `_` away → {ids, hidden}",
|
||||
true,
|
||||
hide_when_publishing
|
||||
),
|
||||
spec!("component.detach", "Detach instance", "{ids?}", true, detach),
|
||||
spec!("component.resetInstance", "Reset all changes", "{ids?}", true, reset_instance),
|
||||
spec!(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
//! Hide when publishing (`component.hideWhenPublishing`, the Assets panel's context menu): keeps a
|
||||
//! component or a set out of the library its file publishes. The model's sign of it is the name
|
||||
//! (a leading `.` or `_`), so the command writes the name: one undo step, saved and synced like
|
||||
//! any rename, and honoured by the next publish.
|
||||
|
||||
use figcraft_doc::NodeId;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::library::{hidden_name, with_hidden_name};
|
||||
use crate::params::id_of;
|
||||
use crate::{EngineError, Session};
|
||||
|
||||
const CMD: &str = "component.hideWhenPublishing";
|
||||
|
||||
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
|
||||
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
|
||||
}
|
||||
|
||||
fn component(s: &mut Session, name: &str, x: f64) -> NodeId {
|
||||
let f = id_of(&ex(s, "frame.create", json!({"x": x, "y": 0, "width": 80, "height": 40, "name": name}))["id"]).unwrap();
|
||||
id_of(&ex(s, "component.create", json!({"ids": [f]}))["id"]).unwrap()
|
||||
}
|
||||
|
||||
fn name(s: &Session, id: NodeId) -> String {
|
||||
s.doc().unwrap().node(id).unwrap().name.clone()
|
||||
}
|
||||
|
||||
/// The names of the components the library published last offers.
|
||||
fn offered(s: &Session) -> Vec<String> {
|
||||
let lib = s.doc().unwrap().published.clone().expect("published");
|
||||
let mut names: Vec<String> = lib.manifest.components.iter().filter(|a| a.published).map(|a| a.name.clone()).collect();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_gain_and_lose_the_mark() {
|
||||
// (name, hide, result)
|
||||
let rows = [
|
||||
("Button", true, ".Button"),
|
||||
(".Button", true, ".Button"),
|
||||
("_Button", true, "_Button"),
|
||||
("Buttons/Primary", true, ".Buttons/Primary"),
|
||||
(".Button", false, "Button"),
|
||||
("_Button", false, "Button"),
|
||||
("._ Button", false, "Button"),
|
||||
("Button", false, "Button"),
|
||||
("...", false, "Component"),
|
||||
("", true, "."),
|
||||
];
|
||||
for (name, hide, want) in rows {
|
||||
let got = with_hidden_name(name, hide);
|
||||
assert_eq!(got, want, "{name:?} hidden={hide}");
|
||||
assert_eq!(hidden_name(&got), hide, "{got:?} reads back as hidden={hide}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_command_toggles_sets_and_is_one_undo_step() {
|
||||
let mut s = Session::new();
|
||||
let (a, b) = (component(&mut s, "Button", 0.0), component(&mut s, "Card", 100.0));
|
||||
// The selection by default; toggling hides.
|
||||
ex(&mut s, "select.set", json!({"ids": [a]}));
|
||||
let r = ex(&mut s, CMD, json!({}));
|
||||
assert_eq!((r["hidden"].as_bool(), name(&s, a).as_str(), name(&s, b).as_str()), (Some(true), ".Button", "Card"));
|
||||
// Several at once: hidden unless all of them are already.
|
||||
let r = ex(&mut s, CMD, json!({"ids": [a, b]}));
|
||||
assert_eq!((r["hidden"].as_bool(), name(&s, a), name(&s, b)), (Some(true), ".Button".to_string(), ".Card".to_string()));
|
||||
let undo = s.ds().unwrap().history.undo.len();
|
||||
let r = ex(&mut s, CMD, json!({"ids": [a, b]}));
|
||||
assert_eq!((r["hidden"].as_bool(), name(&s, a), name(&s, b)), (Some(false), "Button".to_string(), "Card".to_string()));
|
||||
assert_eq!(s.ds().unwrap().history.undo.len(), undo + 1, "one undo step for both");
|
||||
ex(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!((name(&s, a), name(&s, b)), (".Button".to_string(), ".Card".to_string()));
|
||||
ex(&mut s, "edit.redo", json!({}));
|
||||
assert_eq!(name(&s, a), "Button");
|
||||
// `on` sets, whatever it was.
|
||||
ex(&mut s, CMD, json!({"ids": [a], "on": false}));
|
||||
assert_eq!(name(&s, a), "Button");
|
||||
ex(&mut s, CMD, json!({"ids": [a], "on": true}));
|
||||
ex(&mut s, CMD, json!({"ids": [a], "on": true}));
|
||||
assert_eq!(name(&s, a), ".Button");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_layer_in_a_component_or_a_variant_hides_the_whole_asset() {
|
||||
let mut s = Session::new();
|
||||
let a = component(&mut s, "Button", 0.0);
|
||||
let inner = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 4, "y": 4, "width": 8, "height": 8, "parent": a}))["id"]).unwrap();
|
||||
let r = ex(&mut s, CMD, json!({"ids": [inner]}));
|
||||
assert_eq!(r["ids"], json!([a]));
|
||||
assert_eq!(name(&s, a), ".Button");
|
||||
// A variant: its set is the asset.
|
||||
let (v1, v2) = (component(&mut s, "Size=Small", 200.0), component(&mut s, "Size=Large", 300.0));
|
||||
let set = id_of(&ex(&mut s, "component.combineVariants", json!({"ids": [v1, v2]}))["id"]).unwrap();
|
||||
let before = name(&s, v1);
|
||||
ex(&mut s, CMD, json!({"ids": [v1]}));
|
||||
assert!(hidden_name(&name(&s, set)), "the set is hidden: {}", name(&s, set));
|
||||
assert_eq!(name(&s, v1), before, "the variant keeps its name (its property values)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_isnt_a_component_is_refused() {
|
||||
let mut s = Session::new();
|
||||
let r = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"]).unwrap();
|
||||
for p in [json!({"ids": [r]}), json!({"ids": []}), json!({"ids": [999_999]}), json!({"ids": "x"}), json!({"on": "yes"})] {
|
||||
ex(&mut s, "select.none", json!({}));
|
||||
let e = s.execute(CMD, &p).expect_err(&p.to_string());
|
||||
assert!(!matches!(e, EngineError::Internal { .. }), "{p}: {e}");
|
||||
}
|
||||
assert!(!hidden_name(&name(&s, r)), "nothing renamed");
|
||||
assert!(s.ds().unwrap().history.undo.len() == 1, "and nothing to undo but the rectangle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publishing_leaves_hidden_components_out_and_brings_them_back() {
|
||||
let mut s = Session::new();
|
||||
let (a, _b) = (component(&mut s, "Button", 0.0), component(&mut s, "Card", 100.0));
|
||||
ex(&mut s, "library.publish", json!({}));
|
||||
assert_eq!(offered(&s), ["Button", "Card"]);
|
||||
ex(&mut s, CMD, json!({"ids": [a]}));
|
||||
ex(&mut s, "library.publish", json!({}));
|
||||
assert_eq!(offered(&s), ["Card"], "hidden: taken out of the library");
|
||||
ex(&mut s, CMD, json!({"ids": [a]}));
|
||||
ex(&mut s, "library.publish", json!({}));
|
||||
assert_eq!(offered(&s), ["Button", "Card"], "shown again: back in the library");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_is_synced_like_any_edit() {
|
||||
// What a live session sends is the change between two documents: the rename is one.
|
||||
let mut s = Session::new();
|
||||
let a = component(&mut s, "Button", 0.0);
|
||||
let mut replica = figcraft_collab::Replica::new(s.doc().unwrap().clone());
|
||||
ex(&mut s, CMD, json!({"ids": [a]}));
|
||||
let d = figcraft_collab::diff(&replica, s.doc().unwrap()).unwrap();
|
||||
assert!(!d.changes.is_empty(), "the change is one a session sends");
|
||||
replica.apply(&d.changes, figcraft_collab::Mode::Strict).unwrap();
|
||||
assert_eq!(replica.document().node(a).unwrap().name, ".Button");
|
||||
}
|
||||
|
|
@ -1389,6 +1389,8 @@ mod fx_behaviour_tests;
|
|||
#[cfg(test)]
|
||||
mod gesture_preview_tests;
|
||||
#[cfg(test)]
|
||||
mod hide_publishing_tests;
|
||||
#[cfg(test)]
|
||||
mod history_behaviour_tests;
|
||||
#[cfg(test)]
|
||||
mod html_codegen_tests;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,20 @@ pub fn hidden_name(name: &str) -> bool {
|
|||
name.starts_with('.') || name.starts_with('_')
|
||||
}
|
||||
|
||||
/// `name` with or without the mark that keeps an asset out of a library by default: hiding puts
|
||||
/// a `.` before it, showing takes every leading `.` and `_` away (a name of nothing else keeps
|
||||
/// one letter's worth: "Component").
|
||||
pub fn with_hidden_name(name: &str, hidden: bool) -> String {
|
||||
match (hidden, hidden_name(name)) {
|
||||
(true, false) => format!(".{name}"),
|
||||
(false, true) => {
|
||||
let shown = name.trim_start_matches(['.', '_']).trim_start();
|
||||
if shown.is_empty() { "Component".to_string() } else { shown.to_string() }
|
||||
}
|
||||
_ => name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the Assets panel files asset `id`: under its page, the frames, sections and groups
|
||||
/// holding it, then the folders its name makes with slashes (`Buttons/Primary` is `Primary` in
|
||||
/// `Buttons`). Returns the folders, outermost first, and the name shown on its tile.
|
||||
|
|
|
|||
|
|
@ -542,6 +542,7 @@ name = "English"
|
|||
"Hide contents" = "Hide contents"
|
||||
"Hide from publishing" = "Hide from publishing"
|
||||
"Hide section contents" = "Hide section contents"
|
||||
"Hide when publishing" = "Hide when publishing"
|
||||
"Highlight layers on hover" = "Highlight layers on hover"
|
||||
"Highlights" = "Highlights"
|
||||
"Hold Alt" = "Hold Alt"
|
||||
|
|
|
|||
|
|
@ -543,6 +543,7 @@ status = "draft — needs native review"
|
|||
"Hide contents" = "Ocultar contenido"
|
||||
"Hide from publishing" = "Ocultar al publicar"
|
||||
"Hide section contents" = "Ocultar contenido de la sección"
|
||||
"Hide when publishing" = "Ocultar al publicar"
|
||||
"Highlight layers on hover" = "Resaltar capas al pasar el cursor"
|
||||
"Highlights" = "Luces"
|
||||
"Hold Alt" = "Mantén Alt"
|
||||
|
|
@ -1570,6 +1571,7 @@ status = "draft — needs native review"
|
|||
"component.editProperty" = "Editar propiedad de componente"
|
||||
"component.exposeInstance" = "Exponer instancia anidada"
|
||||
"component.goToMain" = "Ir al componente principal"
|
||||
"component.hideWhenPublishing" = "Ocultar al publicar"
|
||||
"component.list" = "Listar componentes"
|
||||
"component.makeSlot" = "Convertir en ranura"
|
||||
"component.properties" = "Propiedades del componente"
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ struct Tile {
|
|||
/// The name on the tile: its last slash part.
|
||||
label: String,
|
||||
set: bool,
|
||||
/// Kept out of the library this file publishes (its name starts with `.` or `_`).
|
||||
hidden: bool,
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
|
|
@ -73,7 +75,7 @@ impl Tile {
|
|||
fn new(doc: &Document, id: NodeId, name: &str, set: bool) -> (String, Self) {
|
||||
let (folders, label) = library::asset_folder(doc, id);
|
||||
let label = if label.is_empty() { name.to_string() } else { label };
|
||||
(folders.join(" / "), Self { id, name: name.to_string(), label, set })
|
||||
(folders.join(" / "), Self { id, name: name.to_string(), label, set, hidden: library::hidden_name(name) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +160,8 @@ enum Act {
|
|||
Drag(AssetDrag),
|
||||
Style(Source, String),
|
||||
Toggle(String),
|
||||
/// Hide the local component when publishing, or show it again.
|
||||
Hide(NodeId, bool),
|
||||
}
|
||||
|
||||
fn drag_id() -> Id {
|
||||
|
|
@ -443,7 +447,22 @@ fn tiles(ui: &mut Ui, w: f32, src: &Source, list: &[Tile], thumbs: &Thumbs, want
|
|||
),
|
||||
}
|
||||
let label = Rect::from_min_size(pos2(x, r.max.y + 2.0), vec2(tile, TILE_LABEL - 4.0));
|
||||
crate::bidi::paint_name(ui.painter(), pos2(x, label.center().y), &item.label, regular(11.0), t.text, tile);
|
||||
// Hidden when publishing: a crossed eye on the tile and a dimmer name.
|
||||
let own_hidden = *src == Source::Local && item.hidden;
|
||||
if own_hidden {
|
||||
let badge = Rect::from_min_size(r.right_top() + vec2(-22.0, 6.0), vec2(16.0, 16.0));
|
||||
icons::paint(ui.painter(), badge, Icon::EyeOff, t.icon_secondary);
|
||||
}
|
||||
let name_colour = if own_hidden { t.text_secondary } else { t.text };
|
||||
crate::bidi::paint_name(ui.painter(), pos2(x, label.center().y), &item.label, regular(11.0), name_colour, tile);
|
||||
if *src == Source::Local {
|
||||
crate::right::context_popup(&resp).show(|ui| {
|
||||
if crate::right::menu_check(ui, item.hidden, tr("Hide when publishing"), "") {
|
||||
*act = Some(Act::Hide(item.id, !item.hidden));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
let resp = resp.on_hover_text(trf(
|
||||
"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it.",
|
||||
&[("name", &item.name)],
|
||||
|
|
@ -633,6 +652,9 @@ pub fn show(app: &mut FigApp, ui: &mut Ui) {
|
|||
place(app, &src, id, cx, cy);
|
||||
}
|
||||
Some(Act::Style(src, style)) => apply_style(app, &src, &style),
|
||||
Some(Act::Hide(id, on)) => {
|
||||
app.run("component.hideWhenPublishing", json!({"ids": [id], "on": on}));
|
||||
}
|
||||
Some(Act::Toggle(id)) => {
|
||||
let mut c = collapsed;
|
||||
if !c.remove(&id) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue