mirror of
https://github.com/penpot/penpot.git
synced 2026-05-30 04:08:08 +00:00
🐛 Fix nitrate delete and leave org flow
This commit is contained in:
parent
6b3d4e38b0
commit
87384aaccd
@ -112,12 +112,48 @@
|
||||
AND t.id = ANY(?)
|
||||
AND t.deleted_at IS NULL")
|
||||
|
||||
(def sql:get-team-files-count
|
||||
"SELECT count(*) AS total
|
||||
(def ^:private sql:get-teams-files-counts
|
||||
"SELECT p.team_id, count(*) AS total
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE p.team_id = ?
|
||||
AND f.deleted_at IS NULL")
|
||||
WHERE p.team_id = ANY(?)
|
||||
AND f.deleted_at IS NULL
|
||||
GROUP BY p.team_id")
|
||||
|
||||
(defn- get-team-files-counts
|
||||
[conn team-ids]
|
||||
(if (seq team-ids)
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(->> (db/exec! conn [sql:get-teams-files-counts ids-array])
|
||||
(reduce (fn [acc {:keys [team-id total]}]
|
||||
(assoc acc team-id (long total)))
|
||||
{})))
|
||||
{}))
|
||||
|
||||
(defn- build-leave-org-plan
|
||||
[{:keys [::db/conn]} default-team-id teams-to-delete keep-default-team-requested?]
|
||||
(let [all-teams (cond-> (set teams-to-delete) default-team-id (conj default-team-id))
|
||||
files-counts (get-team-files-counts conn all-teams)
|
||||
has-files? (fn [id] (pos? (long (get files-counts id 0))))
|
||||
deletable (remove has-files? teams-to-delete)
|
||||
keep-default? (or keep-default-team-requested?
|
||||
(and default-team-id (has-files? default-team-id)))
|
||||
to-detach (cond-> (into [] (remove (set deletable) teams-to-delete))
|
||||
(and default-team-id keep-default?) (conj default-team-id))]
|
||||
{:deletable-team-ids deletable
|
||||
:keep-default-team? keep-default?
|
||||
:delete-default-team? (boolean (and default-team-id (not keep-default?)))
|
||||
:detach-from-org-team-ids to-detach}))
|
||||
|
||||
(defn get-leave-org-summary
|
||||
[cfg default-team-id teams-to-delete teams-to-transfer-count teams-to-exit-count]
|
||||
(let [{:keys [deletable-team-ids delete-default-team? detach-from-org-team-ids]}
|
||||
(build-leave-org-plan cfg default-team-id teams-to-delete nil)]
|
||||
{:teams-to-delete (+ (count deletable-team-ids)
|
||||
(if delete-default-team? 1 0))
|
||||
:teams-to-transfer teams-to-transfer-count
|
||||
:teams-to-exit teams-to-exit-count
|
||||
:teams-to-detach (count detach-from-org-team-ids)}))
|
||||
|
||||
(def ^:private schema:leave-org
|
||||
[:map
|
||||
@ -132,6 +168,18 @@
|
||||
[:id ::sm/uuid]
|
||||
[:reassign-to {:optional true} ::sm/uuid]]]]])
|
||||
|
||||
(def ^:private schema:get-leave-org-summary-result
|
||||
[:map
|
||||
[:teams-to-delete ::sm/int]
|
||||
[:teams-to-transfer ::sm/int]
|
||||
[:teams-to-exit ::sm/int]
|
||||
[:teams-to-detach ::sm/int]])
|
||||
|
||||
(def ^:private schema:get-leave-org-summary
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:default-team-id ::sm/uuid]])
|
||||
|
||||
|
||||
(defn- get-organization-teams-for-user
|
||||
[{:keys [::db/conn] :as cfg} org-summary profile-id]
|
||||
@ -221,16 +269,14 @@
|
||||
:code :not-valid-teams))))
|
||||
|
||||
|
||||
|
||||
(defn leave-org
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id id name default-team-id teams-to-delete teams-to-leave skip-validation] :as params}]
|
||||
(let [org-prefix (str "[" (d/sanitize-string name) "] ")
|
||||
|
||||
default-team-files-count (-> (db/exec-one! conn [sql:get-team-files-count default-team-id])
|
||||
:total)
|
||||
delete-default-team? (= default-team-files-count 0)]
|
||||
|
||||
|
||||
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [profile-id id name default-team-id teams-to-delete teams-to-leave skip-validation keep-default-team-requested?]}]
|
||||
(let [org-prefix (str "[" (d/sanitize-string name) "] ")
|
||||
{:keys [deletable-team-ids
|
||||
keep-default-team?
|
||||
detach-from-org-team-ids]} (build-leave-org-plan cfg default-team-id teams-to-delete keep-default-team-requested?)]
|
||||
|
||||
;; assert that the received teams are valid, checking the different constraints
|
||||
(when-not skip-validation
|
||||
@ -238,20 +284,27 @@
|
||||
|
||||
(assert-membership cfg profile-id id)
|
||||
|
||||
;; delete the teams-to-delete
|
||||
(doseq [id teams-to-delete]
|
||||
(teams/delete-team cfg {:profile-id profile-id :team-id id}))
|
||||
;; delete only eligible teams (non-protected and without files)
|
||||
(doseq [id deletable-team-ids]
|
||||
(teams/delete-team cfg {:profile-id profile-id
|
||||
:team-id id}))
|
||||
|
||||
;; leave the teams-to-leave
|
||||
(doseq [{:keys [id reassign-to]} teams-to-leave]
|
||||
(teams/leave-team cfg {:profile-id profile-id :id id :reassign-to reassign-to}))
|
||||
|
||||
;; Delete default-team-id if empty; otherwise keep it and prefix the name.
|
||||
(if delete-default-team?
|
||||
(do
|
||||
(db/update! conn :team {:is-default false} {:id default-team-id})
|
||||
(teams/delete-team cfg {:profile-id profile-id :team-id default-team-id}))
|
||||
(db/exec! conn [sql:prefix-team-name-and-unset-default org-prefix default-team-id]))
|
||||
;; Process org "Your Penpot" team: keep with prefix if needed, otherwise delete.
|
||||
(when default-team-id
|
||||
(if keep-default-team?
|
||||
(db/exec! conn [sql:prefix-team-name-and-unset-default org-prefix default-team-id])
|
||||
(teams/delete-team cfg {:profile-id profile-id
|
||||
:team-id default-team-id})))
|
||||
|
||||
;; Detach retained owned teams from the organization in Nitrate.
|
||||
;; Nitrate will rehome them to its fallback/default org.
|
||||
(doseq [team-id detach-from-org-team-ids]
|
||||
(nitrate/call cfg :remove-team-from-org {:team-id team-id
|
||||
:organization-id id}))
|
||||
|
||||
;; Api call to nitrate
|
||||
(nitrate/call cfg :remove-profile-from-org {:profile-id profile-id :organization-id id})
|
||||
@ -268,6 +321,25 @@
|
||||
(leave-org cfg (assoc params :profile-id profile-id)))
|
||||
|
||||
|
||||
(sv/defmethod ::get-leave-org-summary
|
||||
{::rpc/auth true
|
||||
::doc/added "2.18"
|
||||
::sm/params schema:get-leave-org-summary
|
||||
::sm/result schema:get-leave-org-summary-result
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id id default-team-id]}]
|
||||
(let [{:keys [valid-teams-to-delete-ids
|
||||
valid-teams-to-transfer
|
||||
valid-teams-to-exit
|
||||
valid-default-team]} (get-valid-teams cfg id profile-id default-team-id)
|
||||
teams-to-transfer-count (count valid-teams-to-transfer)
|
||||
teams-to-exit-count (count valid-teams-to-exit)]
|
||||
(when-not valid-default-team
|
||||
(ex/raise :type :validation
|
||||
:code :not-valid-teams))
|
||||
(get-leave-org-summary cfg default-team-id valid-teams-to-delete-ids teams-to-transfer-count teams-to-exit-count)))
|
||||
|
||||
|
||||
(def ^:private schema:remove-team-from-org
|
||||
[:map
|
||||
[:team-id ::sm/uuid]
|
||||
|
||||
@ -487,9 +487,10 @@
|
||||
|
||||
;; Delete owned organizations on the fly (no grace period).
|
||||
;; Nitrate iterates the user's owned orgs and, per org, calls
|
||||
;; Penpot back via ::notify-organization-deletion which renames
|
||||
;; the org's teams (prefixed with "[OrgName] ", including the
|
||||
;; user's "Your Penpot" team) and soft-deletes empty ones.
|
||||
;; Penpot back through two paths: ::notify-user-organizations-deletion
|
||||
;; (during delete-owned-orgs) and ::notify-organization-deletion.
|
||||
;; Both preserve org teams unchanged and only prefix or delete
|
||||
;; imported "Your Penpot" teams according to whether they still have files.
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(nitrate/call cfg :delete-owned-orgs {:profile-id profile-id})
|
||||
;; Remove the user from any remaining org memberships.
|
||||
|
||||
@ -791,16 +791,16 @@
|
||||
{:org-perms {:owner-id (dm/get-in team [:organization :owner-id])
|
||||
:permissions (dm/get-in team [:organization :permissions])}
|
||||
:profile-id profile-id
|
||||
:team-perms perms
|
||||
;; `onlyMe` is for a future org-level flow.
|
||||
:allow-org-owner-delete? false})
|
||||
:team-perms perms})
|
||||
(boolean (:is-owner perms)))]
|
||||
|
||||
(when-not can-delete?
|
||||
(ex/raise :type :validation
|
||||
:code :only-owner-can-delete-team))
|
||||
|
||||
(when (:is-default team)
|
||||
;; Protect the user's personal default team from deletion.
|
||||
;; Org-scoped default teams ("Your Penpot") are allowed to be deleted when they have no files.
|
||||
(when (and (:is-default team) (not in-org?))
|
||||
(ex/raise :type :validation
|
||||
:code :non-deletable-team
|
||||
:hint "impossible to delete default team"))
|
||||
|
||||
@ -296,46 +296,61 @@ RETURNING id, deleted_at;")
|
||||
nil)
|
||||
|
||||
(defn manage-deleted-organization-teams
|
||||
"For a list of teams, rename those with files and delete those without, then notify users."
|
||||
[cfg {:keys [teams organization-name]}]
|
||||
(let [teams (->> teams (filter uuid?) distinct (into []))]
|
||||
(when (seq teams)
|
||||
"For a deleted organization, preserve org teams unchanged and only prefix or
|
||||
delete member Your Penpot teams depending on whether they still contain files."
|
||||
[cfg {:keys [organization-id organization-name teams]}]
|
||||
(let [all-team-ids (->> teams
|
||||
(map :id)
|
||||
(filter uuid?)
|
||||
distinct
|
||||
(into []))
|
||||
your-penpot-team-ids (->> teams
|
||||
(filter :is-your-penpot)
|
||||
(map :id)
|
||||
(filter uuid?)
|
||||
distinct
|
||||
(into []))]
|
||||
(when (seq all-team-ids)
|
||||
(let [org-prefix (str "[" (d/sanitize-string organization-name) "] ")]
|
||||
(db/tx-run!
|
||||
cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [teams-array (db/create-array conn "uuid" teams)
|
||||
teams-with-files (->> (db/exec! conn [sql:get-teams-files-counts teams-array])
|
||||
(filter (fn [{:keys [total]}] (pos? total)))
|
||||
(map :team-id)
|
||||
(into #{}))
|
||||
teams-to-keep (->> teams (filter teams-with-files) (into []))
|
||||
teams-to-delete (->> teams (remove teams-with-files) (into []))]
|
||||
(let [teams-with-files (if (seq your-penpot-team-ids)
|
||||
(->> (db/exec! conn [sql:get-teams-files-counts
|
||||
(db/create-array conn "uuid" your-penpot-team-ids)])
|
||||
(filter (fn [{:keys [total]}] (pos? total)))
|
||||
(map :team-id)
|
||||
(into #{}))
|
||||
#{})
|
||||
teams-to-prefix (->> your-penpot-team-ids (filter teams-with-files) (into []))
|
||||
teams-to-delete (->> your-penpot-team-ids (remove teams-with-files) (into []))]
|
||||
|
||||
;; Rename teams that have files in one go
|
||||
(when (seq teams-to-keep)
|
||||
;; Org teams move to the fallback org unchanged. Only imported
|
||||
;; Your Penpot teams keep the org prefix when they still have files.
|
||||
(when (seq teams-to-prefix)
|
||||
(db/exec! conn [sql:prefix-teams-name-and-unset-default
|
||||
org-prefix
|
||||
(db/create-array conn "uuid" teams-to-keep)]))
|
||||
(db/create-array conn "uuid" teams-to-prefix)]))
|
||||
|
||||
;; Soft-delete empty teams in one go
|
||||
;; Empty imported Your Penpot teams disappear entirely.
|
||||
(soft-delete-teams! cfg teams-to-delete)
|
||||
|
||||
(notifications/notify-organization-deletion cfg organization-name teams teams-to-delete)
|
||||
(notifications/notify-organization-deletion cfg organization-id organization-name all-team-ids teams-to-delete)
|
||||
nil)))))))
|
||||
|
||||
|
||||
(sv/defmethod ::notify-organization-deletion
|
||||
"For a list of teams, rename them with the name of the deleted org, and notify
|
||||
of the deletion to the connected users"
|
||||
"For a deleted organization, preserve org teams and only prefix or delete
|
||||
imported Your Penpot teams before notifying connected users."
|
||||
{::doc/added "2.15"
|
||||
::sm/params schema:notify-organization-deletion
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
teams (->> (:teams org-summary)
|
||||
(map :id))]
|
||||
(manage-deleted-organization-teams cfg {:teams teams :organization-name (:name org-summary)})
|
||||
teams (:teams org-summary)]
|
||||
(manage-deleted-organization-teams cfg {:organization-name (:name org-summary)
|
||||
:organization-id (:id org-summary)
|
||||
:teams teams})
|
||||
nil))
|
||||
|
||||
;; ---- API: notify-user-organizations-deletion
|
||||
@ -345,15 +360,18 @@ RETURNING id, deleted_at;")
|
||||
[:profile-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::notify-user-organizations-deletion
|
||||
"For a given user, find all owned organizations and rename or delete their teams."
|
||||
"For a given user, find all owned organizations and apply the deleted-org
|
||||
transfer rules to their imported Your Penpot teams."
|
||||
{::doc/added "2.18"
|
||||
::sm/params schema:notify-user-organizations-deletion}
|
||||
[cfg {:keys [profile-id]}]
|
||||
(let [owned-orgs (nitrate/call cfg :get-owned-orgs {:profile-id profile-id})]
|
||||
(doseq [org owned-orgs]
|
||||
(let [organization-name (:name org)
|
||||
teams (map :id (:teams org))]
|
||||
(manage-deleted-organization-teams cfg {:teams teams :organization-name organization-name}))))
|
||||
teams (:teams org)]
|
||||
(manage-deleted-organization-teams cfg {:organization-name organization-name
|
||||
:organization-id (:id org)
|
||||
:teams teams}))))
|
||||
nil)
|
||||
|
||||
|
||||
@ -630,7 +648,8 @@ LEFT JOIN profile AS p
|
||||
[:map
|
||||
[:teams-to-delete ::sm/int]
|
||||
[:teams-to-transfer ::sm/int]
|
||||
[:teams-to-exit ::sm/int]])
|
||||
[:teams-to-exit ::sm/int]
|
||||
[:teams-to-detach ::sm/int]])
|
||||
|
||||
(sv/defmethod ::get-remove-from-org-summary
|
||||
"Get a summary of the teams that would be deleted, transferred, or exited
|
||||
@ -650,9 +669,11 @@ LEFT JOIN profile AS p
|
||||
(when-not valid-default-team
|
||||
(ex/raise :type :validation
|
||||
:code :not-valid-teams))
|
||||
{:teams-to-delete (count valid-teams-to-delete-ids)
|
||||
:teams-to-transfer (count valid-teams-to-transfer)
|
||||
:teams-to-exit (count valid-teams-to-exit)}))
|
||||
(cnit/get-leave-org-summary cfg
|
||||
default-team-id
|
||||
valid-teams-to-delete-ids
|
||||
(count valid-teams-to-transfer)
|
||||
(count valid-teams-to-exit))))
|
||||
|
||||
;; API: cleanup-org-team-invitations
|
||||
|
||||
|
||||
@ -34,11 +34,12 @@
|
||||
|
||||
|
||||
(defn notify-organization-deletion
|
||||
[cfg organization-name teams deleted-teams]
|
||||
[cfg organization-id organization-name teams deleted-teams]
|
||||
(let [msgbus (::mbus/msgbus cfg)]
|
||||
(mbus/pub! msgbus
|
||||
:topic uuid/zero
|
||||
:message {:type :organization-deleted
|
||||
:organization-id organization-id
|
||||
:organization-name organization-name
|
||||
:teams teams
|
||||
:deleted-teams deleted-teams})))
|
||||
:deleted-teams deleted-teams})))
|
||||
|
||||
@ -186,8 +186,10 @@
|
||||
expected-start (str "[" (d/sanitize-string organization-name) "] ")
|
||||
org-summary {:id organization-id
|
||||
:name organization-name
|
||||
:teams [{:id (:id team-with-files)}
|
||||
{:id (:id empty-team)}]}
|
||||
:teams [{:id (:id team-with-files)
|
||||
:is-your-penpot true}
|
||||
{:id (:id empty-team)
|
||||
:is-your-penpot true}]}
|
||||
calls (atom [])
|
||||
submitted (atom [])
|
||||
out (with-redefs [nitrate/call (fn [_cfg method params]
|
||||
@ -222,6 +224,7 @@
|
||||
(let [{:keys [topic message]} (first @calls)]
|
||||
(t/is (= uuid/zero topic))
|
||||
(t/is (= :organization-deleted (:type message)))
|
||||
(t/is (= organization-id (:organization-id message)))
|
||||
(t/is (= organization-name (:organization-name message)))
|
||||
(t/is (= #{(:id team-with-files) (:id empty-team)}
|
||||
(set (:teams message))))
|
||||
@ -254,12 +257,16 @@
|
||||
org-2-prefix (str "[" (d/sanitize-string org-2-name) "] ")
|
||||
owned-orgs [{:id org-1-id
|
||||
:name org-1-name
|
||||
:teams [{:id (:id org-1-team-files)}
|
||||
{:id (:id org-1-team-empty)}]}
|
||||
:teams [{:id (:id org-1-team-files)
|
||||
:is-your-penpot true}
|
||||
{:id (:id org-1-team-empty)
|
||||
:is-your-penpot true}]}
|
||||
{:id org-2-id
|
||||
:name org-2-name
|
||||
:teams [{:id (:id org-2-team-files)}
|
||||
{:id (:id org-2-team-empty)}]}]
|
||||
:teams [{:id (:id org-2-team-files)
|
||||
:is-your-penpot true}
|
||||
{:id (:id org-2-team-empty)
|
||||
:is-your-penpot true}]}]
|
||||
calls (atom [])
|
||||
submitted (atom [])
|
||||
out (with-redefs [nitrate/call (fn [_cfg method params]
|
||||
@ -313,6 +320,8 @@
|
||||
m2 (org-msg org-2-name)]
|
||||
(t/is (some? m1))
|
||||
(t/is (some? m2))
|
||||
(t/is (= org-1-id (:organization-id m1)))
|
||||
(t/is (= org-2-id (:organization-id m2)))
|
||||
(t/is (= #{(:id org-1-team-files) (:id org-1-team-empty)}
|
||||
(set (:teams m1))))
|
||||
(t/is (= #{(:id org-1-team-empty)}
|
||||
@ -1016,9 +1025,10 @@
|
||||
:organization-id organization-id
|
||||
:default-team-id (:id org-team)}))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= {:teams-to-delete 0
|
||||
(t/is (= {:teams-to-delete 1
|
||||
:teams-to-transfer 0
|
||||
:teams-to-exit 0}
|
||||
:teams-to-exit 0
|
||||
:teams-to-detach 0}
|
||||
(:result out)))))
|
||||
|
||||
(t/deftest get-remove-from-org-summary-with-teams-to-delete
|
||||
@ -1042,9 +1052,10 @@
|
||||
:organization-id organization-id
|
||||
:default-team-id (:id org-team)}))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= {:teams-to-delete 1
|
||||
(t/is (= {:teams-to-delete 2
|
||||
:teams-to-transfer 0
|
||||
:teams-to-exit 0}
|
||||
:teams-to-exit 0
|
||||
:teams-to-detach 0}
|
||||
(:result out)))))
|
||||
|
||||
(t/deftest get-remove-from-org-summary-with-teams-to-transfer
|
||||
@ -1072,9 +1083,10 @@
|
||||
:organization-id organization-id
|
||||
:default-team-id (:id org-team)}))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= {:teams-to-delete 0
|
||||
(t/is (= {:teams-to-delete 1
|
||||
:teams-to-transfer 1
|
||||
:teams-to-exit 0}
|
||||
:teams-to-exit 0
|
||||
:teams-to-detach 0}
|
||||
(:result out)))))
|
||||
|
||||
(t/deftest get-remove-from-org-summary-with-teams-to-exit
|
||||
@ -1101,9 +1113,10 @@
|
||||
:organization-id organization-id
|
||||
:default-team-id (:id org-team)}))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= {:teams-to-delete 0
|
||||
(t/is (= {:teams-to-delete 1
|
||||
:teams-to-transfer 0
|
||||
:teams-to-exit 1}
|
||||
:teams-to-exit 1
|
||||
:teams-to-detach 0}
|
||||
(:result out)))))
|
||||
|
||||
(t/deftest get-remove-from-org-summary-does-not-mutate
|
||||
|
||||
@ -44,6 +44,13 @@
|
||||
:organization-id (:id org-summary)}
|
||||
nil)))
|
||||
|
||||
(defn- nitrate-org-summary-only-mock
|
||||
[org-summary]
|
||||
(fn [_cfg method _params]
|
||||
(case method
|
||||
:get-org-summary org-summary
|
||||
nil)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Tests
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@ -279,6 +286,64 @@
|
||||
(let [team (th/db-get :team {:id (:id team1)})]
|
||||
(t/is (nil? (:deleted-at team))))))))
|
||||
|
||||
(t/deftest get-leave-org-summary-counts-default-team-as-delete-when-empty
|
||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
||||
profile-user (th/create-profile* 2 {:is-active true})
|
||||
org-default-team (th/create-team* 97 {:profile-id (:id profile-user)})
|
||||
|
||||
organization-id (uuid/random)
|
||||
your-penpot-id (:id org-default-team)
|
||||
org-summary (make-org-summary
|
||||
:organization-id organization-id
|
||||
:organization-name "Test Org"
|
||||
:owner-id (:id profile-owner)
|
||||
:your-penpot-teams [your-penpot-id]
|
||||
:org-teams [])]
|
||||
|
||||
(with-redefs [nitrate/call (nitrate-org-summary-only-mock org-summary)]
|
||||
(let [out (th/command! {::th/type :get-leave-org-summary
|
||||
::rpc/profile-id (:id profile-user)
|
||||
:id organization-id
|
||||
:default-team-id your-penpot-id})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= {:teams-to-delete 1
|
||||
:teams-to-transfer 0
|
||||
:teams-to-exit 0
|
||||
:teams-to-detach 0}
|
||||
(:result out)))))))
|
||||
|
||||
(t/deftest get-leave-org-summary-counts-default-team-as-keep-when-has-files
|
||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
||||
profile-user (th/create-profile* 2 {:is-active true})
|
||||
org-default-team (th/create-team* 96 {:profile-id (:id profile-user)})
|
||||
project (th/create-project* 96 {:profile-id (:id profile-user)
|
||||
:team-id (:id org-default-team)})
|
||||
_ (th/create-file* 96 {:profile-id (:id profile-user)
|
||||
:project-id (:id project)})
|
||||
extra-team (th/create-team* 95 {:profile-id (:id profile-user)})
|
||||
|
||||
organization-id (uuid/random)
|
||||
your-penpot-id (:id org-default-team)
|
||||
org-summary (make-org-summary
|
||||
:organization-id organization-id
|
||||
:organization-name "Test Org"
|
||||
:owner-id (:id profile-owner)
|
||||
:your-penpot-teams [your-penpot-id]
|
||||
:org-teams [(:id extra-team)])]
|
||||
|
||||
(with-redefs [nitrate/call (nitrate-org-summary-only-mock org-summary)]
|
||||
(let [out (th/command! {::th/type :get-leave-org-summary
|
||||
::rpc/profile-id (:id profile-user)
|
||||
:id organization-id
|
||||
:default-team-id your-penpot-id})]
|
||||
(t/is (th/success? out))
|
||||
;; extra-team is deletable, default team has files and is preserved.
|
||||
(t/is (= {:teams-to-delete 1
|
||||
:teams-to-transfer 0
|
||||
:teams-to-exit 0
|
||||
:teams-to-detach 1}
|
||||
(:result out)))))))
|
||||
|
||||
(t/deftest leave-org-error-org-owner-cannot-leave
|
||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-owner)})
|
||||
|
||||
@ -19,10 +19,11 @@
|
||||
(= permission-value "any")))
|
||||
|
||||
(defn- can-delete-team?
|
||||
[{:keys [is-org-owner? permission-value team-perms allow-org-owner-delete?]}]
|
||||
[{:keys [is-org-owner? permission-value team-perms]}]
|
||||
(cond
|
||||
(= permission-value "onlyMe")
|
||||
(and allow-org-owner-delete? is-org-owner?)
|
||||
;; Org owners can always delete teams inside their organizations.
|
||||
is-org-owner?
|
||||
true
|
||||
(= permission-value "onlyOwners")
|
||||
(boolean (:is-owner team-perms))
|
||||
:else false))
|
||||
@ -76,7 +77,7 @@
|
||||
|
||||
(defn allowed?
|
||||
"Returns true only for explicitly allowed actions (fail-closed)."
|
||||
[action {:keys [org-perms profile-id team-perms allow-org-owner-delete? target-org-same-owner?]}]
|
||||
[action {:keys [org-perms profile-id team-perms target-org-same-owner?]}]
|
||||
(let [{:keys [permission-key check-fn] :as rule}
|
||||
(get action-rules action)
|
||||
permissions (normalize-org-permissions org-perms)
|
||||
@ -87,7 +88,6 @@
|
||||
:else (boolean (check-fn {:is-org-owner? is-org-owner?
|
||||
:permission-value permission-value
|
||||
:team-perms team-perms
|
||||
:allow-org-owner-delete? allow-org-owner-delete?
|
||||
:target-org-same-owner? target-org-same-owner?})))))
|
||||
|
||||
(defn can-send-invitations?
|
||||
|
||||
@ -21,15 +21,15 @@
|
||||
:profile-id :member
|
||||
:team-perms {:is-admin true}}))))
|
||||
|
||||
(t/deftest org-owner-is-allowed-for-create
|
||||
(t/deftest org-owner-is-allowed-for-create-and-delete
|
||||
(t/is (true? (nitrate-perms/allowed? :create-team
|
||||
{:org-perms org-perms
|
||||
:profile-id :owner
|
||||
:team-perms {:is-admin false}})))
|
||||
(t/is (false? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms org-perms
|
||||
:profile-id :owner
|
||||
:team-perms {:is-admin false}}))))
|
||||
(t/is (true? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms org-perms
|
||||
:profile-id :owner
|
||||
:team-perms {:is-admin false}}))))
|
||||
|
||||
(t/deftest create-team-permission-rules
|
||||
(t/is (true? (nitrate-perms/allowed? :create-team
|
||||
@ -57,16 +57,11 @@
|
||||
:profile-id :member
|
||||
:team-perms {:is-admin true}}))))
|
||||
|
||||
(t/deftest delete-team-onlyme-is-gated-for-future-org-flow
|
||||
(t/deftest delete-team-onlyme-still-allows-org-owner
|
||||
(let [only-me-org (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyMe"})]
|
||||
(t/is (false? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms only-me-org
|
||||
:profile-id :owner
|
||||
:team-perms {:is-owner false :is-admin false}})))
|
||||
(t/is (true? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms only-me-org
|
||||
:allow-org-owner-delete? true
|
||||
:profile-id :owner
|
||||
:team-perms {:is-owner false :is-admin false}})))
|
||||
(t/is (false? (nitrate-perms/allowed? :delete-team
|
||||
|
||||
@ -731,16 +731,19 @@
|
||||
|
||||
|
||||
(defn- handle-organization-deleted
|
||||
[{:keys [organization-name teams deleted-teams]}]
|
||||
[{:keys [organization-id organization-name teams deleted-teams]}]
|
||||
(ptk/reify ::handle-organization-deleted
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [team-id (:current-team-id state)
|
||||
current-team (dm/get-in state [:teams team-id])
|
||||
current-org-id (dm/get-in current-team [:organization :id])
|
||||
teams-set (set teams)
|
||||
notify? (contains? teams-set team-id)
|
||||
fetch? (some (:teams state) teams)
|
||||
go-to-default? (some #{team-id} deleted-teams)]
|
||||
go-to-default? (or (some #{team-id} deleted-teams)
|
||||
(= organization-id current-org-id))]
|
||||
(rx/concat
|
||||
(when go-to-default? ;; If the user is currently on one of the deleted teams
|
||||
(rx/of (dcm/go-to-dashboard-recent {:team-id :default})))
|
||||
|
||||
@ -163,6 +163,49 @@
|
||||
:level :success}))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn show-leave-org-modal
|
||||
[{:keys [organization profile default-team-id leave-fn teams-to-transfer on-error]}]
|
||||
(ptk/reify ::show-leave-org-modal
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! ::get-leave-org-summary {:id (:id organization)
|
||||
:default-team-id default-team-id})
|
||||
(rx/mapcat
|
||||
(fn [summary]
|
||||
(let [num-teams-to-delete (:teams-to-delete summary)
|
||||
num-teams-to-transfer (:teams-to-transfer summary)
|
||||
num-teams-to-exit (:teams-to-exit summary)
|
||||
num-teams-to-detach (:teams-to-detach summary)]
|
||||
(cond
|
||||
(pos? num-teams-to-transfer)
|
||||
(rx/of
|
||||
(modal/show
|
||||
{:type :leave-and-reassign-org
|
||||
:profile profile
|
||||
:teams-to-transfer teams-to-transfer
|
||||
:num-teams-to-delete num-teams-to-delete
|
||||
:accept leave-fn}))
|
||||
|
||||
(or (pos? num-teams-to-delete)
|
||||
(pos? num-teams-to-exit)
|
||||
(pos? num-teams-to-detach))
|
||||
(rx/of (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.before-leave-org.title" (:name organization))
|
||||
:message (tr "modals.before-leave-org.message")
|
||||
:accept-label (tr "modals.leave-org-confirm.accept")
|
||||
:on-accept leave-fn
|
||||
:error-msg (tr "modals.before-leave-org.warning")}))
|
||||
|
||||
:else
|
||||
(rx/of (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.leave-org-confirm.title" (:name organization))
|
||||
:message (tr "modals.leave-org-confirm.message")
|
||||
:accept-label (tr "modals.leave-org-confirm.accept")
|
||||
:on-accept leave-fn}))))))
|
||||
(rx/catch on-error)))))
|
||||
|
||||
|
||||
(defn remove-team-from-org
|
||||
[{:keys [team-id organization-id organization-name] :as params}]
|
||||
|
||||
@ -599,13 +599,11 @@
|
||||
(filter #(dm/get-in % [:permissions :is-owner]) non-default-teams))
|
||||
not-owned-teams (mf/with-memo [non-default-teams]
|
||||
(remove #(dm/get-in % [:permissions :is-owner]) non-default-teams))
|
||||
teams-to-delete (mf/with-memo [owned-teams]
|
||||
(filter #(= (count (:members %)) 1) owned-teams))
|
||||
owned-teams-members-loaded?
|
||||
(mf/with-memo [owned-teams]
|
||||
(every? #(contains? % :members) owned-teams))
|
||||
teams-to-transfer (mf/with-memo [owned-teams]
|
||||
(filter #(> (count (:members %)) 1) owned-teams))
|
||||
num-teams-to-leave (+ (count teams-to-transfer) (count not-owned-teams))
|
||||
num-teams-to-delete (count teams-to-delete)
|
||||
num-teams-to-transfer (count teams-to-transfer)
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
@ -627,14 +625,16 @@
|
||||
|
||||
leave-fn
|
||||
(mf/use-fn
|
||||
(mf/deps on-error organization default-team-id not-owned-teams teams-to-delete)
|
||||
(mf/deps on-error organization default-team-id not-owned-teams owned-teams)
|
||||
(fn [{:keys [teams-to-transfer]}]
|
||||
(let [teams-to-leave (cond->> not-owned-teams
|
||||
:always
|
||||
(map #(select-keys % [:id]))
|
||||
(seq teams-to-transfer)
|
||||
(concat teams-to-transfer))
|
||||
teams-to-delete (map :id teams-to-delete)]
|
||||
teams-to-delete (->> owned-teams
|
||||
(filter #(= (count (:members %)) 1))
|
||||
(map :id))]
|
||||
|
||||
|
||||
(st/emit! (dnt/leave-org {:id (:id organization)
|
||||
@ -646,41 +646,22 @@
|
||||
|
||||
on-leave-clicked
|
||||
(mf/use-fn
|
||||
(mf/deps leave-fn profile organization teams-to-transfer num-teams-to-leave num-teams-to-delete num-teams-to-transfer)
|
||||
(mf/deps leave-fn profile organization default-team-id teams-to-transfer on-error owned-teams-members-loaded?)
|
||||
(fn []
|
||||
(cond
|
||||
(and (pos? num-teams-to-delete)
|
||||
(zero? num-teams-to-transfer))
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.before-leave-org.title" (:name organization))
|
||||
:message (tr "modals.before-leave-org.message")
|
||||
:accept-label (tr "modals.leave-org-confirm.accept")
|
||||
:on-accept leave-fn
|
||||
:error-msg (tr "modals.before-leave-org.warning")}))
|
||||
(pos? num-teams-to-transfer)
|
||||
(st/emit!
|
||||
(modal/show
|
||||
{:type :leave-and-reassign-org
|
||||
:profile profile
|
||||
:teams-to-transfer teams-to-transfer
|
||||
:num-teams-to-delete num-teams-to-delete
|
||||
:accept leave-fn}))
|
||||
|
||||
:else
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.leave-org-confirm.title" (:name organization))
|
||||
:message (tr "modals.leave-org-confirm.message")
|
||||
:accept-label (tr "modals.leave-org-confirm.accept")
|
||||
:on-accept leave-fn})))))]
|
||||
(when owned-teams-members-loaded?
|
||||
(st/emit! (dnt/show-leave-org-modal {:organization organization
|
||||
:profile profile
|
||||
:default-team-id default-team-id
|
||||
:leave-fn leave-fn
|
||||
:teams-to-transfer teams-to-transfer
|
||||
:on-error on-error})))))]
|
||||
(mf/use-effect
|
||||
(mf/deps owned-teams)
|
||||
(fn []
|
||||
;; We need all the team members of the owned teams
|
||||
;; TODO this will re-render once for each owned team, not very performance-wise
|
||||
(do
|
||||
(doseq [team owned-teams]
|
||||
(st/emit! (dtm/fetch-members (:id team)))))))
|
||||
;; Fetch members for any owned team that doesn't have them yet.
|
||||
(doseq [team owned-teams
|
||||
:when (not (contains? team :members))]
|
||||
(st/emit! (dtm/fetch-members (:id team))))))
|
||||
[:> dropdown-menu* props
|
||||
|
||||
[:> dropdown-menu-item* {:on-click on-leave-clicked
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user