From eec81f57183ab7fcfd53517ad467f29bc10efc1d Mon Sep 17 00:00:00 2001 From: trisua Date: Fri, 15 Aug 2025 19:09:37 -0400 Subject: [PATCH] add: stack clones --- crates/app/src/langs/en-US.toml | 1 + crates/app/src/public/html/stacks/feed.lisp | 25 +++++++++++++++++++++ crates/app/src/routes/api/v1/mod.rs | 1 + crates/app/src/routes/api/v1/stacks.rs | 21 +++++++++++++++++ crates/core/src/database/stacks.rs | 7 ++++++ 5 files changed, 55 insertions(+) diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index e06e6ed..a464014 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -256,6 +256,7 @@ version = "1.0.0" "stacks:label.remove" = "Remove" "stacks:label.block_all" = "Block all" "stacks:label.unblock_all" = "Unblock all" +"stacks:label.clone" = "Clone" "forge:label.forges" = "Forges" "forge:label.my_forges" = "My forges" diff --git a/crates/app/src/public/html/stacks/feed.lisp b/crates/app/src/public/html/stacks/feed.lisp index 2d1ca4a..392417e 100644 --- a/crates/app/src/public/html/stacks/feed.lisp +++ b/crates/app/src/public/html/stacks/feed.lisp @@ -37,6 +37,14 @@ (text "{{ icon \"pencil\" }}") (span (text "{{ text \"general:action.manage\" }}"))) + (text "{% elif user -%}") + ; clone button for non-owner users + (button + ("class" "lowered small") + ("onclick" "clone_stack()") + (icon (text "book-copy")) + (span + (str (text "stacks:label.clone")))) (text "{%- endif %}"))) (div ("class" "card w_full flex flex_col gap_2") @@ -114,5 +122,22 @@ window.location.href = \"/settings#/account/blocks\"; } }); + } + + async function clone_stack() { + fetch(\"/api/v1/stacks/{{ stack.id }}/clone\", { + method: \"POST\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + window.location.href = `/stacks/${res.payload}`; + } + }); }")) (text "{% endblock %}") diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 294ebb9..4b33b93 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -665,6 +665,7 @@ pub fn routes() -> Router { .route("/stacks", get(stacks::list_request)) .route("/stacks", post(stacks::create_request)) .route("/stacks/{id}", get(stacks::get_request)) + .route("/stacks/{id}/clone", post(stacks::clone_request)) .route("/stacks/{id}/name", post(stacks::update_name_request)) .route("/stacks/{id}/privacy", post(stacks::update_privacy_request)) .route("/stacks/{id}/mode", post(stacks::update_mode_request)) diff --git a/crates/app/src/routes/api/v1/stacks.rs b/crates/app/src/routes/api/v1/stacks.rs index e46cfdc..2e777b1 100644 --- a/crates/app/src/routes/api/v1/stacks.rs +++ b/crates/app/src/routes/api/v1/stacks.rs @@ -91,6 +91,27 @@ pub async fn create_request( } } +pub async fn clone_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateStacks) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.clone_stack(user.id, id).await { + Ok(s) => Json(ApiReturn { + ok: true, + message: "Stack created".to_string(), + payload: s.id.to_string(), + }), + Err(e) => Json(e.into()), + } +} + pub async fn update_name_request( jar: CookieJar, Extension(data): Extension, diff --git a/crates/core/src/database/stacks.rs b/crates/core/src/database/stacks.rs index 604c415..5d102c2 100644 --- a/crates/core/src/database/stacks.rs +++ b/crates/core/src/database/stacks.rs @@ -247,6 +247,13 @@ impl DataManager { Ok(()) } + /// Clone the given stack. + pub async fn clone_stack(&self, owner: usize, stack: usize) -> Result { + let stack = self.get_stack_by_id(stack).await?; + self.create_stack(UserStack::new(stack.name, owner, stack.users)) + .await + } + auto_method!(update_stack_name(&str)@get_stack_by_id:FinePermission::MANAGE_STACKS; -> "UPDATE stacks SET name = $1 WHERE id = $2" --cache-key-tmpl="atto.stack:{}"); auto_method!(update_stack_users(Vec)@get_stack_by_id:FinePermission::MANAGE_STACKS; -> "UPDATE stacks SET users = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}");