mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
M8: Opening a file contacts no team library server it names until you check it (#1621)
* M8: opening a file contacts no team library server it names until you check it (#1097) * M8: translate the team library check strings (#1097) * M8: the Assets panel library test checks what allows a library, without its own fake link (Sonar duplication and coverage)
This commit is contained in:
+2
-1
File diff suppressed because one or more lines are too long
@@ -26,6 +26,15 @@ pub const NOT_FETCHED: &str = "the library hasn't been fetched from its server y
|
||||
pub const NO_LIBRARY: &str = "that file has no library on the server yet: publish it there first (Libraries ▸ Publish to server)";
|
||||
/// Most components one usage report lists.
|
||||
pub const MAX_USAGE: usize = 10_000;
|
||||
/// Most team libraries remembered as yours ([`Prefs::team_libraries`](crate::Prefs)): adding or
|
||||
/// checking another forgets the one used longest ago.
|
||||
pub const MAX_TEAM_LIBRARIES: usize = 500;
|
||||
|
||||
/// How [`Prefs::team_libraries`](crate::Prefs) keeps a link: its SHA-256, so the preferences hold
|
||||
/// no token.
|
||||
fn link_key(link: &str) -> String {
|
||||
figcraft_format::image_hash(link.trim().as_bytes())
|
||||
}
|
||||
|
||||
/// Whether a library's path is a share link of a file on a server (else it is a library file).
|
||||
pub fn is_server_link(path: &str) -> bool {
|
||||
@@ -156,15 +165,35 @@ impl Session {
|
||||
self.take_library(path, reply).map_err(EngineError::Other)
|
||||
}
|
||||
|
||||
/// Whether team library `link` is one you added or checked here ([`Prefs::team_libraries`](crate::Prefs)):
|
||||
/// only those are checked for updates by themselves.
|
||||
pub fn library_allowed(&self, link: &str) -> bool {
|
||||
self.prefs.team_libraries.contains(&link_key(link))
|
||||
}
|
||||
|
||||
/// Remember team library `link` as one you use (the app saves the preferences at once).
|
||||
pub(crate) fn allow_library(&mut self, link: &str) {
|
||||
let key = link_key(link);
|
||||
let list = &mut self.prefs.team_libraries;
|
||||
if list.first() != Some(&key) {
|
||||
list.retain(|k| *k != key);
|
||||
list.insert(0, key);
|
||||
list.truncate(MAX_TEAM_LIBRARIES);
|
||||
self.live.unsaved = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask the servers of the active document's team libraries for newer versions, in the
|
||||
/// background, reporting how the document uses each (the Assets panel calls it now and then).
|
||||
/// Only libraries you added or checked here are asked: opening a file that names another
|
||||
/// server contacts nothing until you check it (`library.check`, #1097).
|
||||
pub fn refresh_server_libraries(&mut self) {
|
||||
let Ok(doc) = self.doc() else { return };
|
||||
let jobs: Vec<(String, LibraryReq)> = doc
|
||||
.libraries
|
||||
.enabled
|
||||
.iter()
|
||||
.filter(|l| is_server_link(&l.path))
|
||||
.filter(|l| is_server_link(&l.path) && self.library_allowed(&l.path))
|
||||
.map(|l| {
|
||||
let have = match self.libraries.cached(&l.path) {
|
||||
Some(Ok(lib)) => Some(lib.manifest.version),
|
||||
|
||||
@@ -85,6 +85,13 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
false,
|
||||
updates
|
||||
),
|
||||
spec!(
|
||||
"library.check",
|
||||
"Check team libraries for updates",
|
||||
"{library? (an enabled team library: id, name or link; all of this file's when omitted)} asks their servers for the latest version in the background, and from then on checks them by themselves: opening a file doesn't contact the server of a team library you haven't added or checked → {checking}",
|
||||
false,
|
||||
check
|
||||
),
|
||||
spec!(
|
||||
"library.acceptUpdates",
|
||||
"Accept library updates",
|
||||
@@ -524,6 +531,10 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let raw = req_string(cmd, p, "path")?;
|
||||
let path = if is_server_link(&raw) { raw.trim().to_string() } else { absolute_path(raw.trim()) };
|
||||
let lib = s.load_library(&path)?;
|
||||
if is_server_link(&path) {
|
||||
// Added by you: its server may be checked for updates from now on (#1097).
|
||||
s.allow_library(&path);
|
||||
}
|
||||
let m = &lib.manifest;
|
||||
let doc = s.doc()?;
|
||||
if doc.published.as_ref().is_some_and(|me| me.manifest.id == m.id) {
|
||||
@@ -549,6 +560,27 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `library.check`: the user lets this file's team libraries (or one) be checked; they are asked now.
|
||||
fn check(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "library.check";
|
||||
let doc = s.doc()?;
|
||||
let links: Vec<String> = match string(p, "library") {
|
||||
Some(key) => {
|
||||
let l = find_link(doc, &key).ok_or_else(|| bad(cmd, format!("no enabled library `{key}` (library.list shows them)")))?;
|
||||
if !is_server_link(&l.path) {
|
||||
return Err(bad(cmd, format!("`{key}` is a library file, not a team library on a server")));
|
||||
}
|
||||
vec![l.path.clone()]
|
||||
}
|
||||
None => doc.libraries.enabled.iter().filter(|l| is_server_link(&l.path)).map(|l| l.path.clone()).collect(),
|
||||
};
|
||||
for l in &links {
|
||||
s.allow_library(l);
|
||||
}
|
||||
s.refresh_server_libraries();
|
||||
Ok(json!({"checking": links.len()}))
|
||||
}
|
||||
|
||||
fn remove(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "library.remove";
|
||||
let key = req_string(cmd, p, "id")?;
|
||||
|
||||
@@ -107,6 +107,8 @@ const DENIED: &[&str] = &[
|
||||
"plugins.",
|
||||
"libraries.",
|
||||
"library.add",
|
||||
// Contacts the servers a file names, and remembers them as yours (#1097).
|
||||
"library.check",
|
||||
// Uploads the open document to a server.
|
||||
"library.publishToServer",
|
||||
"edit.undo",
|
||||
|
||||
@@ -361,6 +361,8 @@ mod jam_open_tests;
|
||||
#[cfg(test)]
|
||||
mod keymap_tests;
|
||||
#[cfg(test)]
|
||||
mod library_beacon_tests;
|
||||
#[cfg(test)]
|
||||
mod library_path_tests;
|
||||
#[cfg(test)]
|
||||
mod library_range_styles_tests;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Opening a file that names a team library on some server doesn't contact that server (#1097):
|
||||
//! only team libraries you added or checked are asked for updates by themselves.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use figcraft_collab::message::{ClientMsg, LibraryReply, LibraryReq};
|
||||
use figcraft_doc::{Document, LibraryLink};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::live::{Event, HostOptions, LiveLink, NetRequest, Presence, SessionInfo};
|
||||
use crate::{Prefs, Session};
|
||||
|
||||
const LINK: &str = "ws://tracker.example:9/?file=f&token=per-file-secret";
|
||||
|
||||
/// Every request the app would send anywhere.
|
||||
#[derive(Default)]
|
||||
struct Wire {
|
||||
sent: Vec<String>,
|
||||
}
|
||||
|
||||
struct Link(Arc<Mutex<Wire>>);
|
||||
|
||||
impl Link {
|
||||
fn note(&self, what: &str) {
|
||||
self.0.lock().unwrap().sent.push(what.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl LiveLink for Link {
|
||||
fn host(&mut self, _: u64, _: HostOptions) -> Result<SessionInfo, String> {
|
||||
self.note("host");
|
||||
Err("no".into())
|
||||
}
|
||||
fn join(&mut self, _: u64, _: &str, _: &str) -> Result<(), String> {
|
||||
self.note("join");
|
||||
Err("no".into())
|
||||
}
|
||||
fn leave(&mut self, _: u64) {}
|
||||
fn presence(&mut self, _: u64, _: &Presence) {}
|
||||
fn send(&mut self, _: u64, _: ClientMsg) {
|
||||
self.note("send");
|
||||
}
|
||||
fn poll(&mut self) -> Vec<(u64, Event)> {
|
||||
Vec::new()
|
||||
}
|
||||
fn library(&mut self, link: &str, _: LibraryReq) -> Result<LibraryReply, String> {
|
||||
self.note(link);
|
||||
Ok(LibraryReply::Empty)
|
||||
}
|
||||
fn library_background(&mut self, jobs: Vec<(String, LibraryReq)>) {
|
||||
for (link, _) in jobs {
|
||||
self.note(&link);
|
||||
}
|
||||
}
|
||||
fn background(&mut self, _: u64, _: NetRequest) -> bool {
|
||||
self.note("background");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A session with `prefs` that opened a file (from disk) whose libraries include [`LINK`].
|
||||
fn opened(prefs: Prefs, tag: &str) -> (Session, Arc<Mutex<Wire>>, std::path::PathBuf) {
|
||||
let mut d = Document::new("Someone else's file");
|
||||
d.libraries_mut().enabled.push(LibraryLink { id: "lib".into(), name: "Kit".into(), path: LINK.into(), version: 3 });
|
||||
let path = std::env::temp_dir().join(format!("figcraft-beacon-{}-{tag}.figcraft", std::process::id()));
|
||||
std::fs::write(&path, figcraft_format::save_bytes(&d, None).unwrap()).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.prefs = prefs;
|
||||
let wire = Arc::new(Mutex::new(Wire::default()));
|
||||
s.live.install(Box::new(Link(wire.clone())));
|
||||
s.execute("file.open", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().libraries.enabled.len(), 1);
|
||||
(s, wire, path)
|
||||
}
|
||||
|
||||
fn sent(w: &Arc<Mutex<Wire>>) -> Vec<String> {
|
||||
w.lock().unwrap().sent.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_a_file_naming_an_unknown_team_library_contacts_nothing() {
|
||||
let (mut s, wire, path) = opened(Prefs::default(), "unknown");
|
||||
// What the Assets panel does now and then while the file is shown.
|
||||
for _ in 0..3 {
|
||||
s.refresh_server_libraries();
|
||||
}
|
||||
assert!(sent(&wire).is_empty(), "no connection attempt: {:?}", sent(&wire));
|
||||
assert!(!s.library_allowed(LINK));
|
||||
// You check it: now it is asked, and from then on by itself.
|
||||
assert_eq!(s.execute("library.check", &json!({})).unwrap()["checking"], 1);
|
||||
assert_eq!(sent(&wire), vec![LINK.to_string()]);
|
||||
assert!(s.library_allowed(LINK) && s.live.unsaved, "kept with the preferences");
|
||||
s.refresh_server_libraries();
|
||||
assert_eq!(sent(&wire).len(), 2);
|
||||
// Kept as a hash: the preferences hold no token.
|
||||
assert!(!serde_json::to_string(&s.prefs).unwrap().contains("per-file-secret"));
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_team_library_you_checked_before_is_checked_when_a_file_names_it() {
|
||||
let mut mine = Session::new();
|
||||
mine.allow_library(LINK);
|
||||
let (mut s, wire, path) = opened(mine.prefs.clone(), "checked");
|
||||
s.refresh_server_libraries();
|
||||
assert_eq!(sent(&wire), vec![LINK.to_string()]);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checking_one_team_library_by_name() {
|
||||
let (mut s, wire, path) = opened(Prefs::default(), "named");
|
||||
assert!(s.execute("library.check", &json!({"library": "Nope"})).is_err());
|
||||
assert_eq!(s.execute("library.check", &json!({"library": "Kit"})).unwrap()["checking"], 1);
|
||||
assert_eq!(sent(&wire), vec![LINK.to_string()]);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
@@ -110,6 +110,12 @@ pub struct Prefs {
|
||||
/// `widget:<plugin>`.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub recent_actions: Vec<String>,
|
||||
/// The team libraries you added (`library.add`) or checked (`library.check`), as hashes of
|
||||
/// their links (which carry tokens), newest first, at most
|
||||
/// [`MAX_TEAM_LIBRARIES`](crate::library_server::MAX_TEAM_LIBRARIES): only these are checked
|
||||
/// for updates by themselves. A file naming another one doesn't contact its server (#1097).
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub team_libraries: Vec<String>,
|
||||
}
|
||||
|
||||
/// Recently run actions kept for the Actions palette (newest first).
|
||||
@@ -150,6 +156,7 @@ impl Default for Prefs {
|
||||
view: BTreeMap::new(),
|
||||
board_grid: Default::default(),
|
||||
recent_actions: Vec::new(),
|
||||
team_libraries: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,8 +196,8 @@ pub struct Lives {
|
||||
/// Registers the app as the opener of `figcraft://` invite links (the desktop app on Windows
|
||||
/// installs one; see [`crate::invite_links`]).
|
||||
pub links: Option<Box<dyn crate::invite_links::LinkRegistrar>>,
|
||||
/// The recorded online files changed since the app last saved the preferences (it saves them
|
||||
/// at once: a crash must not lose an owner token).
|
||||
/// The recorded online files or your team libraries changed since the app last saved the
|
||||
/// preferences (it saves them at once: a crash must not lose an owner token).
|
||||
pub unsaved: bool,
|
||||
/// Servers that couldn't say what became of your files at the last `collab.files {refresh}`.
|
||||
pub servers: crate::online::Problems,
|
||||
|
||||
@@ -946,6 +946,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "غير متاح في هذا الخط"
|
||||
"Not properties, kept as written: {names}" = "ليست خصائص، أُبقيت كما كُتبت: {names}"
|
||||
"Not read yet" = "لم تُقرأ بعد"
|
||||
"Check for updates" = "التحقق من التحديثات"
|
||||
"Not checked yet" = "لم يُتحقق منها بعد"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "يستخدم هذا الملف مكتبات فريق على خوادم لم تستخدمها هنا. لا يُتصل بها حتى تتحقق منها."
|
||||
"Not shared" = "غير مشارك"
|
||||
"Note {n}" = "الملاحظة {n}"
|
||||
"Nothing changed since this version." = "لم يتغير شيء منذ هذا الإصدار."
|
||||
|
||||
@@ -914,6 +914,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "এই ফন্টে নেই"
|
||||
"Not properties, kept as written: {names}" = "প্রপার্টি নয়, যেমনটি লেখা হয়েছে তেমনি রাখা হয়েছে: {names}"
|
||||
"Not read yet" = "এখনো পড়া হয়নি"
|
||||
"Check for updates" = "আপডেট দেখুন"
|
||||
"Not checked yet" = "এখনো যাচাই করা হয়নি"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "এই ফাইলটি এমন সার্ভারের টিম লাইব্রেরি ব্যবহার করে যা আপনি এখানে ব্যবহার করেননি। আপনি যাচাই না করা পর্যন্ত সেগুলোর সাথে যোগাযোগ করা হয় না।"
|
||||
"Not shared" = "শেয়ার করা হয়নি"
|
||||
"Note {n}" = "নোট {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "এটি ডেভের জন্য প্রস্তুত হিসেবে চিহ্নিত করার পর থেকে কিছু পরিবর্তিত হয়নি।"
|
||||
|
||||
@@ -896,6 +896,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "Není v tomto písmu"
|
||||
"Not properties, kept as written: {names}" = "Nejde o vlastnosti, ponecháno beze změny: {names}"
|
||||
"Not read yet" = "Dosud nepřečteno"
|
||||
"Check for updates" = "Zkontrolovat aktualizace"
|
||||
"Not checked yet" = "Dosud nezkontrolováno"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Tento soubor používá týmové knihovny na serverech, které jste tu dosud nepoužili. Nekontaktují se, dokud je nezkontrolujete."
|
||||
"Not shared" = "Nesdíleno"
|
||||
"Note {n}" = "Poznámka {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "Od označení jako připravené pro vývoj se nic nezměnilo."
|
||||
|
||||
@@ -870,6 +870,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "Nicht in dieser Schrift"
|
||||
"Not properties, kept as written: {names}" = "Keine Eigenschaften, unverändert übernommen: {names}"
|
||||
"Not read yet" = "Noch nicht gelesen"
|
||||
"Check for updates" = "Nach Updates suchen"
|
||||
"Not checked yet" = "Noch nicht geprüft"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Diese Datei nutzt Teambibliotheken auf Servern, die Sie hier noch nicht verwendet haben. Diese Server werden erst kontaktiert, wenn Sie die Bibliotheken prüfen."
|
||||
"Not shared" = "Nicht geteilt"
|
||||
"Note {n}" = "Notiz {n}"
|
||||
"Nothing changed since this version." = "Seit dieser Version hat sich nichts geändert."
|
||||
|
||||
@@ -221,6 +221,7 @@ name = "English"
|
||||
"Can comment" = "Can comment"
|
||||
"Check" = "Check"
|
||||
"Check for Updates" = "Check for Updates"
|
||||
"Check for updates" = "Check for updates"
|
||||
"Check for updates…" = "Check for updates…"
|
||||
"Checking for updates…" = "Checking for updates…"
|
||||
"Checking the fonts installed on this computer…" = "Checking the fonts installed on this computer…"
|
||||
@@ -953,6 +954,7 @@ name = "English"
|
||||
"Not in this font" = "Not in this font"
|
||||
"Not now" = "Not now"
|
||||
"Not properties, kept as written: {names}" = "Not properties, kept as written: {names}"
|
||||
"Not checked yet" = "Not checked yet"
|
||||
"Not read yet" = "Not read yet"
|
||||
"Not shared" = "Not shared"
|
||||
"Note {n}" = "Note {n}"
|
||||
@@ -1502,6 +1504,7 @@ name = "English"
|
||||
"This branch was merged or archived." = "This branch was merged or archived."
|
||||
"This computer only" = "This computer only"
|
||||
"This computer remembers the owner token, so the file can be managed from the home screen's Online list. Copy it to manage the file from somewhere else: it is a password for this file and won't be shown again." = "This computer remembers the owner token, so the file can be managed from the home screen's Online list. Copy it to manage the file from somewhere else: it is a password for this file and won't be shown again."
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "This file uses team libraries on servers you haven't used here. They aren't contacted until you check them."
|
||||
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again."
|
||||
"This condition is edited as an expression" = "This condition is edited as an expression"
|
||||
"This file" = "This file"
|
||||
|
||||
@@ -940,6 +940,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "No está en esta fuente"
|
||||
"Not properties, kept as written: {names}" = "No son propiedades, se quedan tal cual: {names}"
|
||||
"Not read yet" = "Aún no leída"
|
||||
"Check for updates" = "Buscar actualizaciones"
|
||||
"Not checked yet" = "Aún sin comprobar"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Este archivo usa bibliotecas de equipo en servidores que no has usado aquí. No se contactan hasta que las compruebes."
|
||||
"Not shared" = "Sin compartir"
|
||||
"Note {n}" = "Nota {n}"
|
||||
"Nothing changed since this version." = "Nada cambió desde esta versión."
|
||||
|
||||
@@ -916,6 +916,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "Ei tässä fontissa"
|
||||
"Not properties, kept as written: {names}" = "Eivät ole ominaisuuksia, säilytetään sellaisinaan: {names}"
|
||||
"Not read yet" = "Ei vielä luettu"
|
||||
"Check for updates" = "Tarkista päivitykset"
|
||||
"Not checked yet" = "Ei vielä tarkistettu"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Tämä tiedosto käyttää tiimikirjastoja palvelimilla, joita et ole käyttänyt täällä. Niihin ei oteta yhteyttä ennen kuin tarkistat ne."
|
||||
"Not shared" = "Ei jaettu"
|
||||
"Note {n}" = "Huomautus {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "Mikään ei ole muuttunut kehitysvalmiiksi merkitsemisen jälkeen."
|
||||
|
||||
@@ -914,6 +914,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "Absent de cette police"
|
||||
"Not properties, kept as written: {names}" = "Pas des propriétés, conservés tels quels : {names}"
|
||||
"Not read yet" = "Pas encore lu"
|
||||
"Check for updates" = "Rechercher des mises à jour"
|
||||
"Not checked yet" = "Pas encore vérifiée"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Ce fichier utilise des bibliothèques d’équipe sur des serveurs que vous n’avez pas utilisés ici. Ils ne sont contactés que lorsque vous les vérifiez."
|
||||
"Not shared" = "Non partagé"
|
||||
"Note {n}" = "Note {n}"
|
||||
"Nothing changed since this version." = "Rien n’a changé depuis cette version."
|
||||
|
||||
@@ -903,6 +903,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "Tidak ada di font ini"
|
||||
"Not properties, kept as written: {names}" = "Bukan properti, dibiarkan apa adanya: {names}"
|
||||
"Not read yet" = "Belum dibaca"
|
||||
"Check for updates" = "Periksa pembaruan"
|
||||
"Not checked yet" = "Belum diperiksa"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "File ini memakai pustaka tim di server yang belum pernah Anda gunakan di sini. Server itu tidak dihubungi sampai Anda memeriksanya."
|
||||
"Not shared" = "Tidak dibagikan"
|
||||
"Note {n}" = "Catatan {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "Tidak ada perubahan sejak ditandai siap untuk dev."
|
||||
|
||||
@@ -884,6 +884,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "Non presente in questo font"
|
||||
"Not properties, kept as written: {names}" = "Non sono proprietà, mantenute come scritte: {names}"
|
||||
"Not read yet" = "Non ancora letto"
|
||||
"Check for updates" = "Verifica aggiornamenti"
|
||||
"Not checked yet" = "Non ancora verificata"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Questo file usa librerie del team su server che non hai usato qui. Non vengono contattati finché non le verifichi."
|
||||
"Not shared" = "Non condiviso"
|
||||
"Note {n}" = "Nota {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "Nulla è cambiato da quando è stato segnato come pronto per lo sviluppo."
|
||||
|
||||
@@ -907,6 +907,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "このフォントにはない"
|
||||
"Not properties, kept as written: {names}" = "プロパティではないため記述どおりに保持:{names}"
|
||||
"Not read yet" = "未読"
|
||||
"Check for updates" = "アップデートを確認"
|
||||
"Not checked yet" = "未確認"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "このファイルは、ここで使ったことのないサーバー上のチームライブラリを使う。確認するまでそれらのサーバーには接続しない。"
|
||||
"Not shared" = "共有していない"
|
||||
"Note {n}" = "メモ {n}"
|
||||
"Nothing changed since this version." = "このバージョンから変更なし。"
|
||||
|
||||
@@ -923,6 +923,9 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
|
||||
"Not in this font" = "ამ შრიფტში არ არის"
|
||||
"Not properties, kept as written: {names}" = "არ არის თვისებები, დარჩა დაწერილი სახით: {names}"
|
||||
"Not read yet" = "ჯერ არ არის წაკითხული"
|
||||
"Check for updates" = "განახლებების შემოწმება"
|
||||
"Not checked yet" = "ჯერ არ არის შემოწმებული"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "ეს ფაილი იყენებს გუნდის ბიბლიოთეკებს სერვერებზე, რომლებიც აქ არ გამოგიყენებიათ. მათ არ დავუკავშირდებით, სანამ არ შეამოწმებთ."
|
||||
"Not shared" = "არ არის გაზიარებული"
|
||||
"Note {n}" = "შენიშვნა {n}"
|
||||
"Nothing changed since this version." = "ამ ვერსიის შემდეგ არაფერი შეცვლილა."
|
||||
|
||||
@@ -866,6 +866,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "이 글꼴에 없음"
|
||||
"Not properties, kept as written: {names}" = "속성이 아니므로 작성된 대로 유지: {names}"
|
||||
"Not read yet" = "아직 읽지 않음"
|
||||
"Check for updates" = "업데이트 확인"
|
||||
"Not checked yet" = "아직 확인 안 함"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "이 파일은 여기서 사용한 적 없는 서버의 팀 라이브러리를 사용합니다. 확인하기 전까지는 해당 서버에 연결하지 않습니다."
|
||||
"Not shared" = "공유 안 함"
|
||||
"Note {n}" = "메모 {n}"
|
||||
"Nothing changed since this version." = "이 버전 이후 변경된 내용 없음."
|
||||
|
||||
@@ -916,6 +916,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "Niet in dit lettertype"
|
||||
"Not properties, kept as written: {names}" = "Geen eigenschappen, ongewijzigd overgenomen: {names}"
|
||||
"Not read yet" = "Nog niet gelezen"
|
||||
"Check for updates" = "Controleren op updates"
|
||||
"Not checked yet" = "Nog niet gecontroleerd"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Dit bestand gebruikt teambibliotheken op servers die je hier nog niet hebt gebruikt. Er wordt pas contact mee gemaakt als je ze controleert."
|
||||
"Not shared" = "Niet gedeeld"
|
||||
"Note {n}" = "Notitie {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "Er is niets gewijzigd sinds dit was gemarkeerd als ready for dev."
|
||||
|
||||
@@ -918,6 +918,9 @@ name = "Polski"
|
||||
"Not in this font" = "Brak w tej czcionce"
|
||||
"Not properties, kept as written: {names}" = "Nie są to właściwości, zachowano bez zmian: {names}"
|
||||
"Not read yet" = "Jeszcze nieprzeczytane"
|
||||
"Check for updates" = "Sprawdź aktualizacje"
|
||||
"Not checked yet" = "Jeszcze nie sprawdzono"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Ten plik korzysta z bibliotek zespołu na serwerach, których tu jeszcze nie używano. Nie są kontaktowane, dopóki ich nie sprawdzisz."
|
||||
"Not shared" = "Nieudostępnione"
|
||||
"Note {n}" = "Notatka {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "Nic się nie zmieniło od czasu oznaczenia jako gotowe do wdrożenia."
|
||||
|
||||
@@ -865,6 +865,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "Não existe nesta fonte"
|
||||
"Not properties, kept as written: {names}" = "Não são propriedades, mantidos como escritos: {names}"
|
||||
"Not read yet" = "Ainda não lido"
|
||||
"Check for updates" = "Verificar atualizações"
|
||||
"Not checked yet" = "Ainda não verificada"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Este arquivo usa bibliotecas de equipe em servidores que você não usou aqui. Eles só são contatados quando você os verifica."
|
||||
"Not shared" = "Não compartilhado"
|
||||
"Note {n}" = "Nota {n}"
|
||||
"Nothing changed since this version." = "Nada mudou desde esta versão."
|
||||
|
||||
@@ -918,6 +918,9 @@ name = "Русский"
|
||||
"Not in this font" = "Нет в этом шрифте"
|
||||
"Not properties, kept as written: {names}" = "Не свойства, оставлены как есть: {names}"
|
||||
"Not read yet" = "Ещё не прочитано"
|
||||
"Check for updates" = "Проверить обновления"
|
||||
"Not checked yet" = "Ещё не проверено"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Этот файл использует командные библиотеки на серверах, которые вы здесь не использовали. К ним не обращаются, пока вы их не проверите."
|
||||
"Not shared" = "Не опубликовано"
|
||||
"Note {n}" = "Заметка {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "С момента отметки «Готово к разработке» изменений нет."
|
||||
|
||||
@@ -940,6 +940,9 @@ name = "Svenska"
|
||||
"Not in this font" = "Finns inte i det här typsnittet"
|
||||
"Not properties, kept as written: {names}" = "Inte egenskaper, behölls som de skrevs: {names}"
|
||||
"Not read yet" = "Inte läst än"
|
||||
"Check for updates" = "Sök efter uppdateringar"
|
||||
"Not checked yet" = "Inte kontrollerat än"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Den här filen använder teambibliotek på servrar som du inte har använt här. De kontaktas inte förrän du kontrollerar dem."
|
||||
"Not shared" = "Inte delad"
|
||||
"Note {n}" = "Anteckning {n}"
|
||||
"Nothing changed since this version." = "Ingenting har ändrats sedan den här versionen."
|
||||
|
||||
@@ -910,6 +910,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "ไม่มีในฟอนต์นี้"
|
||||
"Not properties, kept as written: {names}" = "ไม่ใช่พร็อพเพอร์ตี คงไว้ตามที่เขียน: {names}"
|
||||
"Not read yet" = "ยังไม่ได้อ่าน"
|
||||
"Check for updates" = "ตรวจหาอัปเดต"
|
||||
"Not checked yet" = "ยังไม่ได้ตรวจ"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "ไฟล์นี้ใช้ไลบรารีทีมบนเซิร์ฟเวอร์ที่คุณยังไม่เคยใช้ที่นี่ จะไม่มีการติดต่อเซิร์ฟเวอร์เหล่านั้นจนกว่าคุณจะตรวจ"
|
||||
"Not shared" = "ไม่ได้แชร์"
|
||||
"Note {n}" = "โน้ต {n}"
|
||||
"Nothing changed since this version." = "ไม่มีอะไรเปลี่ยนตั้งแต่เวอร์ชันนี้"
|
||||
|
||||
@@ -900,6 +900,9 @@ name = "Türkçe"
|
||||
"Not in this font" = "Bu yazı tipinde yok"
|
||||
"Not properties, kept as written: {names}" = "Özellik değil, yazıldığı gibi korundu: {names}"
|
||||
"Not read yet" = "Henüz okunmadı"
|
||||
"Check for updates" = "Güncellemeleri denetle"
|
||||
"Not checked yet" = "Henüz denetlenmedi"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Bu dosya, burada kullanmadığınız sunuculardaki ekip kitaplıklarını kullanıyor. Siz denetleyene kadar bu sunuculara bağlanılmaz."
|
||||
"Not shared" = "Paylaşılmadı"
|
||||
"Note {n}" = "Not {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "Geliştirmeye hazır olarak işaretlendiğinden beri hiçbir şey değişmedi."
|
||||
|
||||
@@ -885,6 +885,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "Không có trong phông chữ này"
|
||||
"Not properties, kept as written: {names}" = "Không phải thuộc tính, được giữ nguyên như đã viết: {names}"
|
||||
"Not read yet" = "Chưa đọc"
|
||||
"Check for updates" = "Kiểm tra cập nhật"
|
||||
"Not checked yet" = "Chưa kiểm tra"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Tệp này dùng thư viện nhóm trên các máy chủ bạn chưa dùng ở đây. Các máy chủ đó không được liên hệ cho đến khi bạn kiểm tra."
|
||||
"Not shared" = "Chưa chia sẻ"
|
||||
"Note {n}" = "Ghi chú {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "Chưa có gì thay đổi kể từ khi được đánh dấu sẵn sàng cho dev."
|
||||
|
||||
@@ -871,6 +871,9 @@ status = "draft — needs native review"
|
||||
"Not in this font" = "此字体不支持"
|
||||
"Not properties, kept as written: {names}" = "不是属性,按原样保留:{names}"
|
||||
"Not read yet" = "尚未读取"
|
||||
"Check for updates" = "检查更新"
|
||||
"Not checked yet" = "尚未检查"
|
||||
"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "此文件使用了你在此处未用过的服务器上的团队库。在你检查之前,不会连接这些服务器。"
|
||||
"Not shared" = "未分享"
|
||||
"Note {n}" = "备注 {n}"
|
||||
"Nothing changed since this version." = "自此版本以来没有任何更改。"
|
||||
|
||||
@@ -130,6 +130,8 @@ mod layers_keys_tests;
|
||||
#[cfg(test)]
|
||||
mod layers_rename_tests;
|
||||
#[cfg(test)]
|
||||
mod library_beacon_tests;
|
||||
#[cfg(test)]
|
||||
mod live_chat_tests;
|
||||
#[cfg(test)]
|
||||
mod live_docs_tests;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//! The Assets panel showing a file that names a team library on an unknown server doesn't
|
||||
//! contact it (#1097): the library waits until the user clicks Check for updates. That a library
|
||||
//! not allowed is never asked is the engine's `library_beacon_tests`; here, what allows it.
|
||||
|
||||
use figcraft_doc::LibraryLink;
|
||||
use figcraft_engine::Session;
|
||||
|
||||
use crate::layers_drag_tests::Ui;
|
||||
use crate::live_tests::{flat, has_text, text_center};
|
||||
|
||||
const LINK: &str = "ws://tracker.example:9/?file=f&token=per-file-secret";
|
||||
|
||||
#[test]
|
||||
fn the_assets_panel_checks_an_unknown_library_server_only_when_asked() {
|
||||
let mut s = Session::new();
|
||||
s.doc_mut().unwrap().libraries_mut().enabled.push(LibraryLink { id: "lib".into(), name: "Kit".into(), path: LINK.into(), version: 1 });
|
||||
let mut u = Ui::new(s);
|
||||
u.app.ui.left_tab = crate::LeftTab::Assets;
|
||||
u.app.ui.libraries = true;
|
||||
for _ in 0..4 {
|
||||
u.frame(vec![]);
|
||||
u.advance_clock(60.0);
|
||||
}
|
||||
assert!(!u.app.session.library_allowed(LINK), "showing the panel allows nothing");
|
||||
let shapes = flat(&u.frame_output(vec![]).shapes);
|
||||
assert!(has_text(&shapes, "Not checked yet"));
|
||||
let at = text_center(&shapes, "Check for updates");
|
||||
u.button(at, true);
|
||||
u.button(at, false);
|
||||
u.frame(vec![]);
|
||||
assert!(u.app.session.library_allowed(LINK), "allowed once the user checked");
|
||||
}
|
||||
@@ -50,6 +50,7 @@ const PAD: f32 = 16.0;
|
||||
const TO_SERVER: &str = "library.publishToServer";
|
||||
const USAGE: &str = "library.usage";
|
||||
const ADD: &str = "library.add";
|
||||
const CHECK: &str = "library.check";
|
||||
|
||||
/// Where an asset comes from: this file or an enabled library (its id).
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -288,8 +289,10 @@ fn refresh_libraries(app: &mut FigApp, ui: &Ui) {
|
||||
// Team libraries: in the background, at once for one not fetched yet.
|
||||
let sid = Id::unique("asset_server_libraries_checked");
|
||||
let fetched: Option<f64> = ui.data(|d| d.get_temp(sid));
|
||||
let unread = paths.iter().any(|p| is_server_link(p) && app.session.libraries.cached(p).is_none());
|
||||
if paths.iter().any(|p| is_server_link(p)) && (unread || fetched.is_none_or(|l| now - l >= SERVER_REFRESH)) {
|
||||
// Only the ones you added or checked: a file can't make the app contact a server (#1097).
|
||||
let team: Vec<&String> = paths.iter().filter(|p| is_server_link(p) && app.session.library_allowed(p)).collect();
|
||||
let unread = team.iter().any(|p| app.session.libraries.cached(p).is_none());
|
||||
if !team.is_empty() && (unread || fetched.is_none_or(|l| now - l >= SERVER_REFRESH)) {
|
||||
app.session.refresh_server_libraries();
|
||||
ui.data_mut(|d| d.insert_temp(sid, now));
|
||||
}
|
||||
@@ -1054,7 +1057,8 @@ fn enabled_libraries(app: &mut FigApp, ui: &mut Ui, w: f32, doc: &Document) {
|
||||
}
|
||||
Some(Ok(_)) => (tr("The file now holds another library").into(), t.danger),
|
||||
Some(Err(e)) => (e.to_string(), t.danger),
|
||||
None if is_server_link(&l.path) => (tr("Fetching from the server…").into(), t.text_secondary),
|
||||
None if is_server_link(&l.path) && app.session.library_allowed(&l.path) => (tr("Fetching from the server…").into(), t.text_secondary),
|
||||
None if is_server_link(&l.path) => (tr("Not checked yet").into(), t.text_secondary),
|
||||
None => (tr("Not read yet").into(), t.text_secondary),
|
||||
};
|
||||
let text_w = w - PAD - if swappable { 72.0 } else { 44.0 };
|
||||
@@ -1092,6 +1096,17 @@ fn enabled_libraries(app: &mut FigApp, ui: &mut Ui, w: f32, doc: &Document) {
|
||||
});
|
||||
}
|
||||
}
|
||||
// A file names its team libraries' servers: they are contacted only once you say so (#1097).
|
||||
let unchecked = doc.libraries.enabled.iter().any(|l| is_server_link(&l.path) && !app.session.library_allowed(&l.path));
|
||||
if unchecked {
|
||||
hint(ui, tr("This file uses team libraries on servers you haven't used here. They aren't contacted until you check them."));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(PAD);
|
||||
if crate::widgets::button(ui, tr("Check for updates"), false, true).clicked() && !crate::menus::dev_mode_refuses(app, CHECK) {
|
||||
app.run(CHECK, json!({}));
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
let mut add = false;
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Components, variants, styles and variables parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and
|
||||
variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300
|
||||
@@ -78,6 +78,7 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M8, #1097: library updates from a team library a file names are offered, not fetched from its server by themselves: only team libraries you added or checked are checked in the background (**Check for updates** in Assets ▸ Libraries). Numbers unchanged. |
|
||||
| 2026-10-30 | minor | M8, #1114: variables import from an explicit file path (`variable.import`, the `variables` of `file.open` and `file.import`) takes regular files of at most 256 MiB, the limit the sibling `variables.json` already had; anything else is refused before the document changes. Grade unchanged. Tests: `import_export_tests::an_explicit_variables_path_is_capped_like_the_sibling_file`, `variables_io_tests::variables_import_caps_the_file_it_reads` |
|
||||
| 2026-10-30 | minor | M3, #1536 and #1537: component properties are listed in the order they were made in, and renaming one keeps its place (they were sorted by name, so a rename moved the property); that holds for every kind, on every variant of a set and on instances, in `component.properties`, the Design panel and `.fig` export. Files from before keep their properties by name until one is added or renamed. Make slot (the Slot menu of a frame inside a component) asks for the slot's name first: a field under the Slot row with the frame's name selected, Enter or Make slot to make it, Escape to cancel; a name taken keeps the field open. Renaming stays in the component's Properties list. Tests: `engine/src/prop_order_tests.rs`, `ui-egui/src/make_slot_naming_tests.rs`. Left: dragging the shared properties to reorder them, and renaming a slot with only its frame selected. Numbers unchanged |
|
||||
| 2026-10-29 | minor | M3, #1338: the instance-swap property's menu (on an instance, its main component's default and the component playground) and the instance's swap menu open with a search field at the top that has the keyboard; typing filters the components by name (any case, headings without matches drop out), an empty result reads "No matching components", Up and Down step through the matches and Enter picks one (the first without a step), Escape clears the search and then closes the menu. Only the list scrolls; the menu grows back when the search is cleared. Tests: `component_swap_search_tests`. Numbers unchanged |
|
||||
|
||||
+3
-1
@@ -892,7 +892,9 @@ most when it can't be reached).
|
||||
- `library.add {path: <share link>}` enables it (a view link is enough); then `library.place`,
|
||||
`library.applyStyle`, `library.bindVariable`… work as for library files. `library.list` and
|
||||
`library.updates` ask the server for a newer version first (`"server": true` in the listing);
|
||||
`library.acceptUpdates` takes it, overrides kept.
|
||||
`library.acceptUpdates` takes it, overrides kept. The app's own background checks ask only about
|
||||
team libraries added (`library.add`) or checked (`library.check {library?}`) on this computer,
|
||||
so opening a file that names another one contacts nothing (#1097).
|
||||
- `library.usage {library?}` → `{id, name, instances, files: [{name, updated, instances}],
|
||||
components: [{id, name, instances, files}]}`: what the files using it reported (each fetch
|
||||
reports the asking document's name and its instances per component). `library` is an enabled
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
@@ -209,6 +209,12 @@ which other files use without anyone copying the library file around.
|
||||
**Library updates available**; accepting keeps instance overrides. While the server can't be
|
||||
reached, the last version fetched stays in use. The file keeps the link, token included:
|
||||
sharing that file shares read access to the library.
|
||||
- **Only libraries you chose are checked by themselves.** The app asks only about team libraries
|
||||
you added or checked on this computer (kept as hashes of their links in the preferences). A file
|
||||
someone sent you that names another one contacts nothing when you open it: **Assets ▸ Libraries**
|
||||
says it isn't checked yet, and **Check for updates** (`library.check`) asks its server and
|
||||
checks it by itself from then on (#1097). Placing one of its components or applying one of its
|
||||
styles asks the server too, since you did that yourself.
|
||||
- **Usage.** Each time a file's app asks for the library it reports which of its components the
|
||||
file uses and how often. **Show usage** in the library file (`library.usage`) lists the files and
|
||||
the instances of each component. Files are told apart by their names; a file's report changes
|
||||
|
||||
Reference in New Issue
Block a user