From debfe5490f6caf31341d44d02d5b346faf5d1588 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 23 Apr 2026 13:26:01 +0200 Subject: [PATCH] :bug: Fix switching a team nitrate organization lose the background --- backend/src/app/nitrate.clj | 57 +++++++------------ backend/src/app/rpc/commands/nitrate.clj | 13 ++--- backend/src/app/rpc/management/nitrate.clj | 21 ++----- backend/src/app/rpc/notifications.clj | 7 +-- .../rpc_management_nitrate_test.clj | 28 +++++---- common/src/app/common/organization.cljc | 32 +++++++++++ common/src/app/common/types/team.cljc | 13 +++++ frontend/src/app/main/data/dashboard.cljs | 25 ++++---- frontend/src/app/main/data/nitrate.cljs | 4 +- frontend/src/app/main/ui/dashboard/team.cljs | 3 +- frontend/src/app/main/ui/dashboard/team.scss | 1 - .../main/ui/ds/controls/shared/option.scss | 1 + 12 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 common/src/app/common/organization.cljc diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index d096a4e9d2..ad4318ec6c 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -10,9 +10,11 @@ [app.common.exceptions :as ex] [app.common.json :as json] [app.common.logging :as l] + [app.common.organization :as co] [app.common.schema :as sm] [app.common.schema.generators :as sg] [app.common.time :as ct] + [app.common.types.team :as ctt] [app.config :as cf] [app.http.client :as http] [app.rpc :as-alias rpc] @@ -108,16 +110,6 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def ^:private schema:organization - [:map - [:id ::sm/uuid] - [:name ::sm/text] - [:slug ::sm/text] - [:is-your-penpot :boolean] - [:owner-id ::sm/uuid] - [:avatar-bg-url [::sm/text]] - [:logo-id {:optional true} [:maybe ::sm/uuid]]]) - (def ^:private schema:org-summary [:map [:id ::sm/uuid] @@ -129,12 +121,6 @@ [:id ::sm/uuid] [:is-your-penpot :boolean]]]]]) -(def ^:private schema:team - [:map - [:id ::sm/uuid] - [:organization-id ::sm/uuid] - [:is-your-penpot :boolean]]) - (def ^:private schema:profile-org [:map [:is-member :boolean] @@ -221,7 +207,7 @@ (str baseuri "/api/teams/" team-id) - schema:organization params))) + ctt/schema:team-with-organization params))) (defn- get-org-membership-api [cfg {:keys [profile-id organization-id] :as params}] @@ -261,13 +247,18 @@ [cfg {:keys [organization-id team-id is-default] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) params (assoc params :request-params {:team-id team-id - :is-your-penpot (true? is-default)})] - (request-to-nitrate cfg :post - (str baseuri - "/api/organizations/" - organization-id - "/add-team") - schema:team params))) + :is-your-penpot (true? is-default)}) + team (request-to-nitrate cfg :post + (str baseuri + "/api/organizations/" + organization-id + "/add-team") + ctt/schema:team-with-organization params) + custom-photo (when-let [logo-id (get-in team [:organization :logo-id])] + (str (cf/get :public-uri) "/assets/by-id/" logo-id))] + (cond-> team + custom-photo + (assoc-in [:organization :custom-photo] custom-photo)))) (defn- add-profile-to-org-api [cfg {:keys [profile-id organization-id team-id email] :as params}] @@ -385,18 +376,14 @@ Returns the original team unchanged if the request fails or org data is nil." [cfg team params] (try - (let [params (assoc (or params {}) :team-id (:id team)) - org (call cfg :get-team-org params)] + (let [params (assoc (or params {}) :team-id (:id team)) + team-with-org (call cfg :get-team-org params) + org (:organization team-with-org)] (if (some? org) - (assoc team - :organization-id (:id org) - :organization-name (:name org) - :organization-slug (:slug org) - :organization-owner-id (:owner-id org) - :organization-avatar-bg-url (:avatar-bg-url org) - :organization-custom-photo (when-let [logo-id (:logo-id org)] - (str (cf/get :public-uri) "/assets/by-id/" logo-id)) - :is-default (or (:is-default team) (true? (:is-your-penpot org)))) + (-> (co/apply-organization team (assoc org :custom-photo + (when-let [logo-id (:logo-id org)] + (str (cf/get :public-uri) "/assets/by-id/" logo-id)))) + (assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-org))))) team)) (catch Throwable cause (l/error :hint "failed to get team organization info" diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index db552ae070..ddabb9bb65 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -249,22 +249,21 @@ (nitrate/call cfg :remove-team-from-org {:team-id team-id :organization-id organization-id}) ;; Notify connected users - (notifications/notify-team-change cfg team-id nil nil organization-name "dashboard.team-no-longer-belong-org") + (notifications/notify-team-change cfg {:id team-id :organization {:name organization-name}} "dashboard.team-no-longer-belong-org") nil) (def ^:private schema:add-team-to-org [:map [:team-id ::sm/uuid] - [:organization-id ::sm/uuid] - [:organization-name ::sm/text]]) + [:organization-id ::sm/uuid]]) (sv/defmethod ::add-team-to-org {::rpc/auth true ::doc/added "2.17" ::sm/params schema:add-team-to-org ::db/transaction true} - [cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}] + [cfg {:keys [::rpc/profile-id team-id organization-id]}] (assert-is-owner cfg profile-id team-id) (assert-not-default-team cfg team-id) @@ -277,8 +276,8 @@ (teams/initialize-user-in-nitrate-org cfg member-id organization-id))) ;; Api call to nitrate - (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false}) + (let [team (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false})] - ;; Notify connected users - (notifications/notify-team-change cfg team-id nil organization-id organization-name "dashboard.team-belong-org") + ;; Notify connected users + (notifications/notify-team-change cfg team "dashboard.team-belong-org")) nil) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 6762de6e80..f07451abc5 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -12,7 +12,7 @@ [app.common.exceptions :as ex] [app.common.schema :as sm] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] - [app.common.types.team :refer [schema:team]] + [app.common.types.team :refer [schema:team schema:team-with-organization]] [app.config :as cf] [app.db :as db] [app.media :as media] @@ -117,22 +117,13 @@ ;; ---- API: notify-team-change -(def ^:private schema:notify-team-change - [:map - [:id ::sm/uuid] - [:organization-id ::sm/uuid] - [:organization-name ::sm/text]]) - - - - (sv/defmethod ::notify-team-change "Notify to Penpot a team change from nitrate" {::doc/added "2.14" - ::sm/params schema:notify-team-change + ::sm/params schema:team-with-organization ::rpc/auth false} - [cfg {:keys [id organization-id organization-name]}] - (notifications/notify-team-change cfg id nil organization-id organization-name nil) + [cfg team] + (notifications/notify-team-change cfg (select-keys team [:id :is-your-penpot :organization]) nil) nil) ;; ---- API: notify-user-added-to-organization @@ -143,8 +134,6 @@ [:organization-id ::sm/uuid] [:role ::sm/text]]) - - (sv/defmethod ::notify-user-added-to-organization "Notify to Penpot that an user has joined an org from nitrate" {::doc/added "2.14" @@ -271,7 +260,7 @@ RETURNING id, name;") ;; Notify users (doseq [team updated-teams] - (notifications/notify-team-change cfg (:id team) (:name team) nil organization-name "dashboard.org-deleted")))))))) + (notifications/notify-team-change cfg {:id (:id team) :name (:name team) :organization {:name organization-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 index 3bdb2c8a3b..10d0d9f134 100644 --- a/backend/src/app/rpc/notifications.clj +++ b/backend/src/app/rpc/notifications.clj @@ -10,17 +10,14 @@ [app.msgbus :as mbus])) (defn notify-team-change - [cfg team-id team-name organization-id organization-name notification] + [cfg team 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 + :team team :notification notification}))) diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index 99cef73858..382264bd66 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -74,24 +74,32 @@ (t/deftest notify-team-change-publishes-event (let [team-id (uuid/random) organization-id (uuid/random) + organization {:id organization-id + :name "Acme Inc" + :slug "acme-inc" + :owner-id (uuid/random) + :avatar-bg-url "http://example.com/avatar.svg"} calls (atom []) out (with-redefs [mbus/pub! (fn [_cfg & {:keys [topic message]}] (swap! calls conj {:topic topic :message message}))] (management-command-with-nitrate! {::th/type :notify-team-change :id team-id - :organization-id organization-id - :organization-name "Acme Inc"}))] + :is-your-penpot false + :organization organization}))] (t/is (th/success? out)) (t/is (= 1 (count @calls))) (t/is (= uuid/zero (-> @calls first :topic))) - (t/is (= {:type :team-org-change - :team-id team-id - :team-name nil - :organization-id organization-id - :organization-name "Acme Inc" - :notification nil} - (-> @calls first :message))))) + (let [msg (-> @calls first :message)] + (t/is (= :team-org-change (:type msg))) + (t/is (= nil (:notification msg))) + (t/is (= team-id (-> msg :team :id))) + (t/is (= false (-> msg :team :is-your-penpot))) + (t/is (= (:id organization) (-> msg :team :organization :id))) + (t/is (= (:name organization) (-> msg :team :organization :name))) + (t/is (= (:slug organization) (-> msg :team :organization :slug))) + (t/is (= (:owner-id organization) (-> msg :team :organization :owner-id))) + (t/is (= (:avatar-bg-url organization) (str (-> msg :team :organization :avatar-bg-url))))))) (t/deftest notify-user-added-to-organization-creates-default-org-team (let [profile (th/create-profile* 1 {:is-active true}) @@ -181,7 +189,7 @@ (doseq [call @calls] (t/is (= uuid/zero (:topic call))) (t/is (= :team-org-change (-> call :message :type))) - (t/is (= organization-name (-> call :message :organization-name))) + (t/is (= organization-name (-> call :message :team :organization :name))) (t/is (= "dashboard.org-deleted" (-> call :message :notification)))))) (t/deftest get-profile-by-email-success-and-not-found diff --git a/common/src/app/common/organization.cljc b/common/src/app/common/organization.cljc new file mode 100644 index 0000000000..e7e5bc49b1 --- /dev/null +++ b/common/src/app/common/organization.cljc @@ -0,0 +1,32 @@ +;; 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.common.organization) + +(def organization->team-keys + "Mapping from organization field keys to their corresponding :organization-* team keys." + [[:id :organization-id] + [:name :organization-name] + [:custom-photo :organization-custom-photo] + [:slug :organization-slug] + [:avatar-bg-url :organization-avatar-bg-url] + [:owner-id :organization-owner-id]]) + +(defn apply-organization + "Updates a team map with organization fields sourced from org. + Associates each org field to the corresponding :organization-* team key when + the value is non-nil; dissociates the key otherwise. This correctly handles + both attaching an org (all values present) and detaching one (org is nil or + all fields absent)." + [team organization] + (let [id (:id organization)] + (reduce (fn [acc [org-k team-k]] + (let [v (get organization org-k)] + (if (and id (some? v)) + (assoc acc team-k v) + (dissoc acc team-k)))) + team + organization->team-keys))) \ No newline at end of file diff --git a/common/src/app/common/types/team.cljc b/common/src/app/common/types/team.cljc index ad9bac999c..c8707f8b0f 100644 --- a/common/src/app/common/types/team.cljc +++ b/common/src/app/common/types/team.cljc @@ -26,3 +26,16 @@ [:id ::sm/uuid] [:name :string]]) +(def schema:team-with-organization + [:map + [:id ::sm/uuid] + [:is-your-penpot :boolean] + [:organization + [:map + [:id ::sm/uuid] + [:name ::sm/text] + [:slug ::sm/text] + [:owner-id ::sm/uuid] + [:avatar-bg-url ::sm/uri] + [:logo-id {:optional true} [:maybe ::sm/uuid]]]]]) + diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index eddcb5a503..8ef722de9f 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -11,6 +11,7 @@ [app.common.features :as cfeat] [app.common.files.helpers :as cfh] [app.common.logging :as log] + [app.common.organization :as co] [app.common.schema :as sm] [app.common.time :as ct] [app.common.types.project :refer [valid-project?]] @@ -686,29 +687,29 @@ (modal/hide))))) (defn handle-change-team-org - [{:keys [team-id team-name organization-id organization-name notification]}] + [{:keys [team notification]}] (ptk/reify ::handle-change-team-org ptk/WatchEvent (watch [_ state _] - (let [current-team-id (:current-team-id state)] + (let [current-team-id (:current-team-id state) + organization (:organization team)] (when (and (contains? cf/flags :nitrate) notification - (= team-id current-team-id)) - (rx/of (ntf/show {:content (tr notification organization-name) + (= (:id team) current-team-id)) + (rx/of (ntf/show {:content (tr notification (:name organization)) :type :toast :level :info :timeout nil}))))) ptk/UpdateEvent (update [_ state] (if (contains? cf/flags :nitrate) - (d/update-in-when state [:teams team-id] - (fn [team] - (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)))) + (let [team-id (:id team) + team-name (:name team) + organization (:organization team)] + (d/update-in-when state [:teams team-id] + (fn [team] + (cond-> (co/apply-organization team organization) + team-name (assoc :name team-name))))) state)))) (defn- handle-user-org-change diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index dd2619d37e..0741fddbac 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -126,11 +126,11 @@ (defn add-team-to-org - [{:keys [team-id organization-id organization-name] :as params}] + [{:keys [team-id organization-id] :as params}] (ptk/reify ::add-team-to-org ptk/WatchEvent (watch [_ _ _] - (->> (rp/cmd! ::add-team-to-org {:team-id team-id :organization-id organization-id :organization-name organization-name}) + (->> (rp/cmd! ::add-team-to-org {:team-id team-id :organization-id organization-id}) (rx/mapcat (fn [_] (rx/of (modal/hide)))))))) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 82a2c103d6..47a96575cf 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -1437,8 +1437,7 @@ (let [organization (d/seek #(= organization-id (:id %)) organizations)] (when organization (st/emit! (dnt/add-team-to-org {:team-id (:id team) - :organization-id organization-id - :organization-name (:name organization)})))))) + :organization-id organization-id})))))) on-add-team-to-org (mf/use-fn diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index 73e8b56f6a..a4e24c88b9 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -874,7 +874,6 @@ // SELECT ORGANIZATION MODAL .modal-select-org-container { - overflow: hidden; display: flex; flex-direction: column; width: $sz-512; diff --git a/frontend/src/app/main/ui/ds/controls/shared/option.scss b/frontend/src/app/main/ui/ds/controls/shared/option.scss index 2b3e749770..978110d1f3 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/option.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/option.scss @@ -25,6 +25,7 @@ outline-offset: calc(-1 * $b-1); background-color: var(--options-bg-color); color: var(--options-fg-color); + cursor: default; &:hover, &[aria-selected="true"] {