From 8f35e451e6209aff2d23855fbf625419f334dc04 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 16 Mar 2026 10:36:32 +0100 Subject: [PATCH] :sparkles: Add notification for nitrate when creating a team inside an organization (#8639) --- backend/src/app/nitrate.clj | 74 +++++++++++++++---- backend/src/app/rpc/commands/teams.clj | 7 +- backend/src/app/rpc/management/nitrate.clj | 6 +- frontend/src/app/main/data/team.cljs | 5 +- .../src/app/main/ui/dashboard/sidebar.cljs | 16 +++- .../src/app/main/ui/dashboard/team_form.cljs | 24 ++++-- 6 files changed, 102 insertions(+), 30 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index cfa0ff9014..dfd034026e 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -18,16 +18,16 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- request-builder - [cfg method uri shared-key profile-id] + [cfg method uri shared-key profile-id request-params] (fn [] - (http/req! cfg {:method method - :headers {"content-type" "application/json" - "accept" "application/json" - "x-shared-key" shared-key - "x-profile-id" (str profile-id)} - :uri uri - :version :http1.1}))) - + (http/req! cfg (cond-> {:method method + :headers {"content-type" "application/json" + "accept" "application/json" + "x-shared-key" shared-key + "x-profile-id" (str profile-id)} + :uri uri + :version :http1.1} + (= method :post) (assoc :body (json/encode request-params)))))) (defn- with-retries [handler max-retries] @@ -60,9 +60,9 @@ nil))))) (defn- request-to-nitrate - [cfg method uri schema {:keys [::rpc/profile-id] :as params}] + [cfg method uri schema {:keys [::rpc/profile-id request-params] :as params}] (let [shared-key (-> cfg ::setup/shared-keys :nitrate) - full-http-call (-> (request-builder cfg method uri shared-key profile-id) + full-http-call (-> (request-builder cfg method uri shared-key profile-id request-params) (with-retries 3) (with-validate uri schema))] (full-http-call))) @@ -86,6 +86,12 @@ [:name ::sm/text] [:slug ::sm/text]]) +(def ^:private schema:team + [:map + [:id ::sm/uuid] + [:organizationId ::sm/uuid] + [:yourPenpot :boolean]]) + ;; TODO Unify with schemas on backend/src/app/http/management.clj (def ^:private schema:timestamp (sm/type-schema @@ -161,17 +167,40 @@ (defn- get-team-org [cfg {:keys [team-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] - (request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params))) + (request-to-nitrate cfg :get + (str baseuri + "/api/teams/" + team-id) + schema:organization params))) + +(defn- set-team-org + [cfg {:keys [organization-id team-id is-default] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri) + params (assoc params :request-params {:teamId team-id + :yourPenpot (true? is-default)})] + (request-to-nitrate cfg :post + (str baseuri + "/api/organizations/" + organization-id + "/addTeam") + schema:team params))) (defn- get-subscription [cfg {:keys [profile-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] - (request-to-nitrate cfg :get (str baseuri "/api/subscriptions/" (str profile-id)) schema:subscription params))) + (request-to-nitrate cfg :get + (str baseuri + "/api/subscriptions/" + profile-id) + schema:subscription params))) (defn- get-connectivity [cfg params] (let [baseuri (cf/get :nitrate-backend-uri)] - (request-to-nitrate cfg :get (str baseuri "/api/connectivity") schema:connectivity params))) + (request-to-nitrate cfg :get + (str baseuri + "/api/connectivity") + schema:connectivity params))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INITIALIZATION @@ -181,6 +210,7 @@ [_ cfg] (when (contains? cf/flags :nitrate) {:get-team-org (partial get-team-org cfg) + :set-team-org (partial set-team-org cfg) :get-subscription (partial get-subscription cfg) :connectivity (partial get-connectivity cfg)})) @@ -224,6 +254,22 @@ :cause cause) team))) +(defn set-team-organization + "Associates a team with an organization in Nitrate. + Requires organization-id and is-default in params. + Throws an exception if the request fails." + [cfg team params] + (let [params (assoc (or params {}) + :team-id (:id team) + :organization-id (:organization-id params) + :is-default (:is-default params)) + result (call cfg :set-team-org params)] + (when (nil? result) + (throw (ex-info "Failed to set team organization" + {:team-id (:id team) + :organization-id (:organization-id params)}))) + team)) + (defn connectivity [cfg] (call cfg :connectivity {})) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 220602d4e9..55836cb5b6 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -499,7 +499,9 @@ [:map {:title "create-team"} [:name [:string {:max 250}]] [:features {:optional true} ::cfeat/features] - [:id {:optional true} ::sm/uuid]]) + [:id {:optional true} ::sm/uuid] + [:organization-id {:optional true} ::sm/uuid] + [:is-default {:optional true} :boolean]]) (sv/defmethod ::create-team {::doc/added "1.17" @@ -531,6 +533,9 @@ :role :owner) project (create-team-default-project conn params)] (create-team-role conn params) + ;; Set team organization in Nitrate if organization-id is provided + (when (and (contains? cf/flags :nitrate) (:organization-id params)) + (nitrate/set-team-organization cfg-or-conn team params)) (assoc team :default-project-id (:id project)))) (defn- create-team* diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 455b96705b..c70b617c7c 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -113,7 +113,7 @@ {::doc/added "2.14" ::sm/params schema:notify-user-added-to-organization ::rpc/auth false} - [cfg {:keys [profile-id]}] + [cfg {:keys [profile-id organization-id]}] (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile ::quotes/profile-id profile-id}) @@ -122,7 +122,9 @@ (set/difference cfeat/no-team-inheritable-features)) params {:profile-id profile-id :name "Default" - :features features} + :features features + :organization-id organization-id + :is-default true} team (db/tx-run! cfg teams/create-team params)] (select-keys team [:id]))) diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index 60846d88bd..2f6c03f68e 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -255,7 +255,7 @@ (-deref [_] team))) (defn create-team - [{:keys [name] :as params}] + [{:keys [name organization-id] :as params}] (dm/assert! (string? name)) (ptk/reify ::create-team ptk/WatchEvent @@ -264,7 +264,8 @@ :or {on-success identity on-error rx/throw}} (meta params) features features/global-enabled-features - params {:name name :features features}] + params (cond-> {:name name :features features} + organization-id (assoc :organization-id organization-id))] (->> (rp/cmd! :create-team (with-meta params (meta it))) (rx/tap on-success) (rx/map team-created) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index c0d9b46496..491b0d9800 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -314,7 +314,9 @@ (mf/use-fn (mf/deps organization) (fn [] - (dnt/go-to-nitrate-cc organization))) + (if (:organization-id organization) + (dnt/go-to-nitrate-cc organization) + (dnt/go-to-nitrate-cc)))) default-team-id (or (->> organizations vals @@ -371,7 +373,13 @@ teams (dissoc teams default-team-id) on-create-team-click - (mf/use-fn #(st/emit! (modal/show :team-form {}))) + (mf/use-fn + (mf/deps team) + (fn [] + (let [params (if (and (contains? cf/flags :nitrate) (:organization-id team)) + {:organization-id (:organization-id team)} + {})] + (st/emit! (modal/show :team-form params))))) on-team-click (mf/use-fn @@ -383,12 +391,12 @@ [:> dropdown-menu* props [:> dropdown-menu-item* {:on-click on-team-click - :data-value (:default-team-id profile) + :data-value default-team-id :class (stl/css :team-dropdown-item)} [:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon] [:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")] - (when (= (:default-team-id profile) (:id team)) + (when (= default-team-id (:id team)) tick-icon)] (for [team-item (remove :is-default (vals teams))] diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs index 5f6fdaae90..9856a3ec1e 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -24,7 +24,8 @@ (def ^:private schema:team-form [:map {:title "TeamForm"} - [:name [::sm/text {:max 250}]]]) + [:name [::sm/text {:max 250}]] + [:organization-id {:optional true} [:maybe ::sm/uuid]]]) (defn- on-create-success [_form response] @@ -50,7 +51,9 @@ [form] (let [mdata {:on-success (partial on-create-success form) :on-error (partial on-error form)} - params {:name (get-in @form [:clean-data :name])}] + data (:clean-data @form) + params (cond-> {:name (:name data)} + (:organization-id data) (assoc :organization-id (:organization-id data)))] (st/emit! (-> (dtm/create-team (with-meta params mdata)) (with-meta {::ev/origin :dashboard}))))) @@ -58,7 +61,8 @@ [form] (let [mdata {:on-success (partial on-update-success form) :on-error (partial on-error form)} - team (get @form :clean-data)] + data (:clean-data @form) + team (select-keys data [:id :name])] ;; Only send name and id for updates (st/emit! (dtm/update-team (with-meta team mdata)) (modal/hide)))) @@ -72,10 +76,16 @@ (mf/defc team-form-modal {::mf/register modal/components ::mf/register-as :team-form} - [{:keys [team] :as props}] - (let [initial (mf/use-memo (fn [] - (or (some-> team (select-keys [:name :id])) - {}))) + [{:keys [team organization-id] :as props}] + (let [initial (mf/use-memo + (mf/deps team organization-id) + (fn [] + (if team + ;; For existing teams, only include name and id (no organization changes) + (select-keys team [:name :id]) + ;; For new teams, include organization-id if provided + (cond-> {} + organization-id (assoc :organization-id organization-id))))) form (fm/use-form :schema schema:team-form :initial initial) handle-keydown