store-only apps edit their listing on the store (storebound listing live / set: old -> new per field with the checks, a PATCH of only the edited fields; serve listing.set is the one store-only write, its token bound to the store ids; policy.rs STORE_ONLY_WRITES, write_with_confirm for store targets); screenshot captions per language and scene edited as a text edit of the shots copy JSON (JSON files keep their layout); Play content rating, target audience and App content answers in App info; docs/console-parity.md after the editors (73 rows: 55 complete, 18 with the reason) and tests/console-parity.test.ts keeping it honest
This commit is contained in:
parent
3cea2af4e1
commit
7ac028421a
33 changed files with 852 additions and 157 deletions
|
|
@ -193,8 +193,8 @@ pub async fn sidecar_call(window: WebviewWindow, app: AppHandle, state: State<'_
|
|||
if matches!(kind, Some(Kind::Read | Kind::Write | Kind::Owner)) {
|
||||
let id = project_id.ok_or_else(|| Fail::host(format!("{method} needs a project")))?;
|
||||
if id.starts_with("store:") {
|
||||
// a store-only app: store reads only, through the sidecar's generated config
|
||||
if kind != Some(Kind::Read) {
|
||||
// a store-only app: store reads, and the few writes that send only what the call carries (dry runs here)
|
||||
if kind != Some(Kind::Read) && !policy::store_only_write(&method, kind) {
|
||||
return Err(Fail::host("store-only apps are read-only: link a folder to change anything"));
|
||||
}
|
||||
let target = StoreTarget::parse(&id).map_err(Fail::host)?;
|
||||
|
|
@ -205,7 +205,7 @@ pub async fn sidecar_call(window: WebviewWindow, app: AppHandle, state: State<'_
|
|||
o.insert("credentials".into(), Value::Object(creds));
|
||||
}
|
||||
let res = r.call_job(&method, p, job.as_deref()).await?;
|
||||
if readable(&res) {
|
||||
if kind == Some(Kind::Read) && readable(&res) {
|
||||
let _ = state.cache.put(&id, &method, &args, &res, &store::now_iso());
|
||||
}
|
||||
return Ok(res);
|
||||
|
|
@ -400,6 +400,9 @@ pub async fn write_with_confirm(window: WebviewWindow, app: AppHandle, state: St
|
|||
Value::Object(o) => Value::Object(o),
|
||||
_ => return Err(Fail::host("args must be an object")),
|
||||
};
|
||||
if project_id.starts_with("store:") {
|
||||
return store_write_with_confirm(&window, &app, &state, &r, &method, kind, &d, &project_id, args, job.as_deref()).await;
|
||||
}
|
||||
let (proj, creds) = project_params(&state, &project_id)?;
|
||||
let mut dry_params = json!({ "project": proj.path, "args": args, "dryRun": true });
|
||||
if !creds.is_empty() {
|
||||
|
|
@ -431,6 +434,42 @@ pub async fn write_with_confirm(window: WebviewWindow, app: AppHandle, state: St
|
|||
Ok(json!({ "confirmed": true, "stage": "done", "dryRun": dry, "result": result }))
|
||||
}
|
||||
|
||||
/// `write_with_confirm` for a store-only app (no folder): only STORE_ONLY_WRITES, with the store ids (`virtual`) and the
|
||||
/// accounts of the store target; the dry run, the same native dialog, then a token bound to `{ virtual, args }`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn store_write_with_confirm(window: &WebviewWindow, app: &AppHandle, state: &State<'_, AppState>, r: &std::sync::Arc<crate::sidecar::Running>, method: &str, kind: Kind, d: &crate::sidecar::Descriptor, project_id: &str, args: Value, job: Option<&str>) -> Res<Value> {
|
||||
if !policy::store_only_write(method, Some(kind)) {
|
||||
return Err(Fail::host("store-only apps are read-only for this: link a folder to change it"));
|
||||
}
|
||||
let target = StoreTarget::parse(project_id).map_err(Fail::host)?;
|
||||
let creds = target_params(state, &target)?;
|
||||
let virt = target.virtual_project();
|
||||
let name = virt["name"].as_str().unwrap_or("store app").to_string();
|
||||
let mut dry_params = json!({ "virtual": virt, "args": args, "dryRun": true });
|
||||
if !creds.is_empty() {
|
||||
dry_params["credentials"] = Value::Object(creds.clone());
|
||||
}
|
||||
let dry = r.call_job(method, dry_params, job).await?;
|
||||
if dry["ok"] != Value::Bool(true) {
|
||||
return Ok(json!({ "confirmed": false, "stage": "dry-run", "dryRun": dry }));
|
||||
}
|
||||
let summary = summary_for(method, kind, &d.title, &d.description, &name, d.network, d.destructive);
|
||||
let text = format!("{summary}\n\nDry run ({}):\n{}", dry["command"].as_str().unwrap_or_default(), dialog_excerpt(dry["output"].as_str().unwrap_or_default()));
|
||||
let (title, ok) = dialog_labels(kind, &d.title);
|
||||
let home = app.path().home_dir().ok().map(|h| h.to_string_lossy().to_string());
|
||||
if !confirm_dialog_with(window, title, tilde(&text, home.as_deref()), ok).await? {
|
||||
return Ok(json!({ "confirmed": false, "stage": "dialog", "dryRun": dry }));
|
||||
}
|
||||
let bound = json!({ "virtual": virt, "args": args });
|
||||
let token = r.mint(method, &bound).map_err(Fail::host)?;
|
||||
let mut real = json!({ "virtual": virt, "args": args, "dryRun": false, "confirmToken": token });
|
||||
if !creds.is_empty() {
|
||||
real["credentials"] = Value::Object(creds);
|
||||
}
|
||||
let result = r.call_job(method, real, job).await?;
|
||||
Ok(json!({ "confirmed": true, "stage": "done", "dryRun": dry, "result": result }))
|
||||
}
|
||||
|
||||
/// An app folder as the window sees it: `missing` when the folder is gone (moved, renamed, deleted, or an old
|
||||
/// worktree), so the app shows "Folder not found" with Locate / Remove instead of reading it.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
|
|
|
|||
|
|
@ -122,6 +122,15 @@ pub const NEW_FOLDER_METHODS: &[&str] = &["project.init", "import.fastlane"];
|
|||
pub const CONSOLE_FILL: &str = "console.fill";
|
||||
pub const CONSOLE_FILL_NOTE: &str = "Console pages change. This is AI-assisted: review every field in the browser and press the final button (Publish, Submit, Save or Create) yourself. storebound moves through the pages for you and never submits or publishes; App Privacy answers stay a draft until you press Publish.";
|
||||
|
||||
/// Writes a store-only app (no folder) may run: they send only what the call carries (one language's texts), never
|
||||
/// the sidecar's generated config. Dry run from the window like every write; the real run only through
|
||||
/// `write_with_confirm`, its token bound to the store ids (`virtual`) instead of a folder.
|
||||
pub const STORE_ONLY_WRITES: &[&str] = &["listing.set"];
|
||||
|
||||
pub fn store_only_write(method: &str, kind: Option<Kind>) -> bool {
|
||||
kind == Some(Kind::Write) && STORE_ONLY_WRITES.contains(&method)
|
||||
}
|
||||
|
||||
/// Reads that are never kept in the offline cache (big: one rendered image as a data: URL).
|
||||
pub const NO_CACHE_METHODS: &[&str] = &["project.render"];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue