From 39f4c13493522bfa73dd78b657f8fcbe469ce050 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 16 Apr 2026 11:46:05 +0200 Subject: [PATCH] :sparkles: Add nitrate remove team from org --- backend/src/app/nitrate.clj | 12 +++ backend/src/app/rpc/commands/nitrate.clj | 28 +++++++ backend/src/app/rpc/commands/teams.clj | 4 +- backend/src/app/rpc/management/nitrate.clj | 22 ++---- backend/src/app/rpc/notifications.clj | 24 ++++++ frontend/src/app/main/data/dashboard.cljs | 8 +- frontend/src/app/main/data/nitrate.cljs | 12 +++ frontend/src/app/main/ui/confirm.cljs | 5 +- frontend/src/app/main/ui/dashboard/team.cljs | 76 ++++++++++++++++++- frontend/src/app/main/ui/dashboard/team.scss | 79 ++++++++++++++++++++ frontend/translations/en.po | 27 +++++++ frontend/translations/es.po | 27 +++++++ 12 files changed, 298 insertions(+), 26 deletions(-) create mode 100644 backend/src/app/rpc/notifications.clj diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 7f27ca0240..48374660e0 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -286,6 +286,17 @@ "/remove-user") nil params))) +(defn- remove-team-from-org-api + [cfg {:keys [team-id organization-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri) + params (assoc params :request-params {:team-id team-id})] + (request-to-nitrate cfg :post + (str baseuri + "/api/organizations/" + organization-id + "/remove-team") + nil params))) + (defn- delete-team-api [cfg {:keys [team-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] @@ -327,6 +338,7 @@ :add-profile-to-org (partial add-profile-to-org-api cfg) :remove-profile-from-org (partial remove-profile-from-org-api cfg) :delete-team (partial delete-team-api cfg) + :remove-team-from-org (partial remove-team-from-org-api cfg) :get-subscription (partial get-subscription-api cfg) :connectivity (partial get-connectivity-api cfg)})) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index b5a4c6064d..71eaedb2b0 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -8,6 +8,7 @@ [app.rpc :as-alias rpc] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] + [app.rpc.notifications :as notifications] [app.util.services :as sv])) @@ -169,3 +170,30 @@ (nitrate/call cfg :remove-profile-from-org {:profile-id profile-id :org-id org-id}) nil)) + + +(def ^:private schema:remove-team-from-org + [:map + [:team-id ::sm/uuid] + [:organization-id ::sm/uuid]]) + +(sv/defmethod ::remove-team-from-org + {::doc/added "2.16" + ::sm/params schema:remove-team-from-org} + [cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}] + (let [perms (teams/get-permissions cfg profile-id team-id) + team (teams/get-team-info cfg {:id team-id})] + + (when-not (:is-owner perms) + (ex/raise :type :validation + :code :insufficient-permissions)) + + (when (:is-default team) + (ex/raise :type :validation + :code :cant-remove-default-team)) + + ;; Api call to nitrate + (nitrate/call cfg :remove-team-from-org {:team-id team-id :organization-id organization-id}) + + (notifications/notify-team-change cfg team-id nil nil organization-name "dashboard.team-no-longer-belong-org") + nil)) \ No newline at end of file diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 242178a37b..9b089e3b2c 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -471,8 +471,8 @@ ;; --- COMMAND QUERY: get-team-info (defn get-team-info - [{:keys [::db/conn] :as cfg} {:keys [id] :as params}] - (-> (db/get* conn :team + [cfg {:keys [id] :as params}] + (-> (db/get* cfg :team {:id id} {::sql/columns [:id :is-default :features]}) (decode-row))) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index fc4459e865..59a442e9c7 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -13,16 +13,15 @@ [app.common.schema :as sm] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] [app.common.types.team :refer [schema:team]] - [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] - [app.msgbus :as mbus] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.commands.teams-invitations :as ti] [app.rpc.doc :as doc] + [app.rpc.notifications :as notifications] [app.util.services :as sv])) @@ -89,19 +88,7 @@ [:organization-id ::sm/uuid] [:organization-name ::sm/text]]) -(defn notify-team-change - [cfg team-id team-name organization-id organization-name notification] - (let [msgbus (::mbus/msgbus cfg)] - (mbus/pub! msgbus - ;;TODO There is a bug on dashboard with teams notifications. - ;;For now we send it to uuid/zero instead of team-id - :topic uuid/zero - :message {:type :team-org-change - :team-id team-id - :team-name team-name - :organization-id organization-id - :organization-name organization-name - :notification notification}))) + (sv/defmethod ::notify-team-change @@ -110,7 +97,8 @@ ::sm/params schema:notify-team-change ::rpc/auth false} [cfg {:keys [id organization-id organization-name]}] - (notify-team-change cfg id nil organization-id organization-name nil)) + (notifications/notify-team-change cfg id nil organization-id organization-name nil) + nil) ;; ---- API: notify-user-added-to-organization @@ -248,7 +236,7 @@ RETURNING id, name;") ;; Notify users (doseq [team updated-teams] - (notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted")))))))) + (notifications/notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted")))))))) ;; ---- API: get-profile-by-email diff --git a/backend/src/app/rpc/notifications.clj b/backend/src/app/rpc/notifications.clj new file mode 100644 index 0000000000..fc3b4b1752 --- /dev/null +++ b/backend/src/app/rpc/notifications.clj @@ -0,0 +1,24 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.rpc.notifications + (:require + [app.common.uuid :as uuid] + [app.msgbus :as mbus])) + +(defn notify-team-change + [cfg team-id team-name organization-id organization-name notification] + (let [msgbus (::mbus/msgbus cfg)] + (mbus/pub! msgbus + ;;TODO There is a bug on dashboard with teams notifications. + ;;For now we send it to uuid/zero instead of team-id + :topic uuid/zero + :message {:type :team-org-change + :team-id team-id + :team-name team-name + :organization-id organization-id + :organization-name organization-name + :notification notification}))) \ No newline at end of file diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 6a1d8d1646..7e98a0eaa4 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -702,9 +702,11 @@ (if (contains? cf/flags :nitrate) (d/update-in-when state [:teams team-id] (fn [team] - (cond-> (assoc team - :organization-id organization-id - :organization-name organization-name) + (cond-> team + (some? organization-id) (assoc :organization-id organization-id) + (nil? organization-id) (dissoc :organization-id) + (some? organization-name) (assoc :organization-name organization-name) + (nil? organization-name) (dissoc :organization-name) team-name (assoc :name team-name)))) state)))) diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index 2697cbf1c1..ba7124a5a2 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -111,3 +111,15 @@ :type :toast :level :success})))) (rx/catch on-error)))))) + + +(defn remove-team-from-org + [{:keys [team-id organization-id organization-name] :as params}] + (ptk/reify ::remove-team-from-org + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/cmd! ::remove-team-from-org {:team-id team-id :organization-id organization-id :organization-name organization-name}) + (rx/mapcat + (fn [_] + (rx/of + (modal/hide)))))))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs index f9ba97b7e2..522641e93c 100644 --- a/frontend/src/app/main/ui/confirm.cljs +++ b/frontend/src/app/main/ui/confirm.cljs @@ -34,7 +34,8 @@ items cancel-label accept-label - accept-style] :as props}] + accept-style + hint-level] :as props}] (let [on-accept (or on-accept identity) on-cancel (or on-cancel identity) message (or message (tr "ds.confirm-title")) @@ -84,7 +85,7 @@ (when (and (string? scd-message) (not= scd-message "")) [:h3 {:class (stl/css :modal-scd-msg)} scd-message]) (when (string? hint) - [:> context-notification* {:level :info + [:> context-notification* {:level (or hint-level :info) :appearance :ghost} hint]) (when (string? error-msg) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 96afda2563..6dc642c40d 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -14,6 +14,7 @@ [app.main.data.common :as dcm] [app.main.data.event :as ev] [app.main.data.modal :as modal] + [app.main.data.nitrate :as dnt] [app.main.data.notifications :as ntf] [app.main.data.team :as dtm] [app.main.refs :as refs] @@ -21,6 +22,7 @@ [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.forms :as fm] + [app.main.ui.components.org-avatar :refer [org-avatar*]] [app.main.ui.dashboard.change-owner] [app.main.ui.dashboard.subscription :refer [members-cta* show-subscription-members-banner? @@ -44,6 +46,9 @@ (def ^:private menu-icon (deprecated-icon/icon-xref :menu (stl/css :menu-icon))) +(def ^:private org-menu-icon + (deprecated-icon/icon-xref :menu (stl/css :org-menu-icon))) + (def ^:private warning-icon (deprecated-icon/icon-xref :msg-warning (stl/css :warning-icon))) @@ -1274,7 +1279,8 @@ (mf/defc team-settings-page* {::mf/props :obj} [{:keys [team]}] - (let [finput (mf/use-ref) + (let [nitrate? (contains? cfg/flags :nitrate) + finput (mf/use-ref) members (get team :members) stats (get team :stats) @@ -1285,12 +1291,49 @@ can-edit (or (:is-owner permissions) (:is-admin permissions)) + show-org-options-menu* + (mf/use-state false) + + show-org-options-menu? + (deref show-org-options-menu*) + + on-show-options-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (swap! show-org-options-menu* not))) + + close-org-options-menu + (mf/use-fn #(reset! show-org-options-menu* false)) + on-image-click (mf/use-fn #(dom/click (mf/ref-val finput))) on-file-selected (fn [file] - (st/emit! (dtm/update-team-photo file)))] + (st/emit! (dtm/update-team-photo file))) + + remove-team-from-org-fn + (mf/use-fn + (mf/deps team) + (fn [] + (st/emit! (dnt/remove-team-from-org {:team-id (:id team) + :organization-id (:organization-id team) + :organization-name (:organization-name team)})))) + + on-remove-team-from-org + (mf/use-fn + (mf/deps team) + (fn [] + (let [params {:type :confirm + :title (tr "modals.remove-team-org.title") + :message (tr "modals.remove-team-org.text" (:name team) (:organization-name team)) + :hint (tr "modals.remove-team-org.info") + :hint-level :default + :accept-label (tr "modals.remove-team-org.accept") + :on-accept remove-team-from-org-fn + :accept-style :danger}] + (st/emit! (modal/show params)))))] (mf/with-effect [team] (dom/set-html-title (tr "title.team-settings" @@ -1324,6 +1367,35 @@ [:div {:class (stl/css :block-text)} (:name team)]] + (when nitrate? + [:div {:class (stl/css :block)} + [:div {:class (stl/css :block-label)} + (tr "dashboard.team-organization")] + (if (:organization-id team) + [:div {:class (stl/css :block-content)} + [:div {:class (stl/css :org-block-content)} + [:> org-avatar* {:org team :size "xxxl"}] + [:span {:class (stl/css :block-text)} + (:organization-name team)] + + (when (and (:is-owner permissions) (not (:is-default team))) + [:* + [:> button* {:variant "ghost" + :type "button" + :class (stl/css-case :org-options-btn (not show-org-options-menu?) :org-options-btn-open show-org-options-menu?) + :on-click on-show-options-click} + org-menu-icon + + [:& dropdown {:show show-org-options-menu? :on-close close-org-options-menu :dropdown-id "org-options"} + [:ul {:class (stl/css :org-dropdown) + :role "listbox"} + [:li {:on-click on-remove-team-from-org + :class (stl/css :org-dropdown-item)} + (tr "dashboard.team-organization.remove")]]]]])]] + [:div {:class (stl/css :block-content)} + [:span {:class (stl/css :block-text)} + (tr "dashboard.team-organization.none")]])]) + [:div {:class (stl/css :block)} [:div {:class (stl/css :block-label)} (tr "dashboard.team-members")] diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index 6d6d0a369b..b04c895bdd 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -51,6 +51,7 @@ .block-text { color: var(--color-foreground-primary); + text-wrap: nowrap; } .block-content { @@ -869,3 +870,81 @@ margin-block-start: var(--sp-xxxl); gap: var(--sp-s); } + +.org-block-content { + display: grid; + grid-template-columns: var(--sp-xxxl) 1fr var(--sp-xxxl); + align-items: center; + gap: var(--sp-m); + width: max-content; +} + +.org-options-btn { + padding: 0; + justify-content: center; + + --stroke-color: var(--color-foreground-primary); + + &:hover { + --stroke-color: var(--color-accent-primary); + } +} + +.org-options-btn-open { + padding: 0; + justify-content: center; + + --stroke-color: var(--color-accent-primary); + + background-color: var(--color-background-tertiary); + position: relative; +} + +.org-menu-icon { + display: flex; + justify-content: center; + align-items: center; + height: $sz-16; + width: $sz-16; + color: transparent; + fill: none; + stroke-width: $b-1; + stroke: var(--stroke-color); +} + +.org-dropdown { + box-shadow: var(--el-shadow-dark); + display: flex; + flex-direction: column; + gap: var(--sp-xs); + position: absolute; + padding: var(--sp-xs); + border-radius: $br-8; + z-index: var(--z-index-dropdown); + color: var(--color-foreground-primary); + background-color: var(--color-background-tertiary); + border: $b-2 solid var(--color-background-quaternary); + margin: 0; + top: var(--sp-xxxl); + width: fit-content; + min-width: $sz-160; +} + +.org-dropdown-item { + @include t.use-typography("body-small"); + + display: flex; + align-items: center; + justify-content: space-between; + height: $sz-28; + width: 100%; + padding: px2rem(6); + border-radius: $br-8; + cursor: pointer; + text-transform: none; + white-space: nowrap; + + &:hover { + background-color: var(--color-background-quaternary); + } +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index fda3a9a5c1..b9484bebf8 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -344,6 +344,9 @@ msgstr "Restore file" msgid "dashboard.org-deleted" msgstr "The %s organization has been deleted." +msgid "dashboard.team-no-longer-belong-org" +msgstr "This team no longer belongs to the organization %s" + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Add file" @@ -1126,6 +1129,18 @@ msgstr "Team info" msgid "dashboard.team-members" msgstr "Team members" +msgid "dashboard.team-organization" +msgstr "Team organization" + +msgid "dashboard.team-organization.none" +msgstr "This team is not part of any organization" + +msgid "dashboard.team-organization.change" +msgstr "Change team organization" + +msgid "dashboard.team-organization.remove" +msgstr "Remove team from organization" + #: src/app/main/ui/dashboard/team.cljs:1344 msgid "dashboard.team-projects" msgstr "Team projects" @@ -9089,6 +9104,18 @@ msgstr "Go to dashboard" msgid "webgl.modals.webgl-unavailable.cta-troubleshooting" msgstr "Troubleshooting guide" +msgid "modals.remove-team-org.title" +msgstr "REMOVE TEAM FROM THE ORGANIZATION" + +msgid "modals.remove-team-org.text" +msgstr "Are you sure you want to remove the '%s' team from the '%s' organization?" + +msgid "modals.remove-team-org.info" +msgstr "Projects and files will remain available to team members, but the organization's settings will no longer apply." + +msgid "modals.remove-team-org.accept" +msgstr "Remove from organization" + #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 16379e8e5d..d040d050f0 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -353,6 +353,9 @@ msgstr "Restaurar archivo" msgid "dashboard.org-deleted" msgstr "La organización %s se ha borrado." +msgid "dashboard.team-no-longer-belong-org" +msgstr "Este equipo ya no pertenece a la organización %s" + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Añadir archivo" @@ -1130,6 +1133,18 @@ msgstr "Información del equipo" msgid "dashboard.team-members" msgstr "Integrantes del equipo" +msgid "dashboard.team-organization" +msgstr "Organización del equipo" + +msgid "dashboard.team-organization.none" +msgstr "Este equipo no pertenece a ninguna organización" + +msgid "dashboard.team-organization.change" +msgstr "Cambiar el equipo de organización" + +msgid "dashboard.team-organization.remove" +msgstr "Eliminar equipo de la organización" + #: src/app/main/ui/dashboard/team.cljs:1344 msgid "dashboard.team-projects" msgstr "Proyectos del equipo" @@ -8867,6 +8882,18 @@ msgstr "Ir al panel" msgid "webgl.modals.webgl-unavailable.cta-troubleshooting" msgstr "Guía de solución de problemas" +msgid "modals.remove-team-org.title" +msgstr "ELIMINAR EQUIPO DE LA ORGANIZACIÓN" + +msgid "modals.remove-team-org.text" +msgstr "¿Estás seguro de que quieres eliminar el equipo %s de la organización %s?" + +msgid "modals.remove-team-org.info" +msgstr "Los proyectos y archivos seguirán estando disponibles para los miembros del equipo, pero la configuración de la organización dejará de aplicarse." + +msgid "modals.remove-team-org.accept" +msgstr "Eliminar de la organización" + #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta"