mirror of
https://github.com/penpot/penpot.git
synced 2026-11-07 18:40:41 +00:00
Move SPA routing out of the URL fragment into the normal query string. The screen travels in a reserved `screen` key holding the route name (`?screen=workspace&team-id=…`); every other param keeps its name. `rt/nav` and `rt/resolve` keep their signatures. This deletes the fragment-mirroring URL surgery, simplifies link-preview (the server sees everything) and nginx (single path, no SPA fallback rules needed), and migrates OIDC redirects, email links, e2e helpers and plugin test utils to the new format. Legacy `#/…` URLs translate client-side for one Penpot version (`legacy-routes`, marked TODO(next-version)); non-SPA paths are untouched. AI-assisted-by: muse-spark-1.3-contributor
843 lines
31 KiB
Clojure
843 lines
31 KiB
Clojure
;; 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 SUBSIDIARY SL
|
|
|
|
(ns app.main.data.team
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.logging :as log]
|
|
[app.common.schema :as sm]
|
|
[app.common.types.organization :as cto]
|
|
[app.common.types.team :as ctt]
|
|
[app.common.uri :as u]
|
|
[app.config :as cf]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.media :as di]
|
|
[app.main.data.modal :as modal]
|
|
[app.main.data.nitrate-audit :as nitrate-audit]
|
|
[app.main.data.profile :as dp]
|
|
[app.main.features :as features]
|
|
[app.main.repo :as rp]
|
|
[app.main.router :as rt]
|
|
[app.util.clipboard :as clipboard]
|
|
[app.util.i18n :refer [tr]]
|
|
[app.util.storage :as storage]
|
|
[beicon.v2.core :as rx]
|
|
[clojure.string :as str]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
(log/set-level! :warn)
|
|
|
|
(defn get-last-team-id
|
|
"Get last accessed team id"
|
|
[]
|
|
(::current-team-id storage/global))
|
|
|
|
(defn resolve-login-team-id
|
|
"Resolve the team to navigate to after login. Falls back to the
|
|
default team when the candidate requires SSO and the user has no
|
|
valid SSO session for it."
|
|
[{:keys [team-id default-team-id]}]
|
|
(if (or (not (contains? cf/flags :admin-console))
|
|
(= team-id default-team-id))
|
|
(rx/of team-id)
|
|
(->> (rp/cmd! :check-nitrate-sso {:team-id team-id :url (rt/get-current-href)})
|
|
(rx/map (fn [{:keys [authorized]}]
|
|
(if authorized team-id default-team-id))))))
|
|
|
|
(defn teams-fetched
|
|
[teams]
|
|
(ptk/reify ::teams-fetched
|
|
IDeref
|
|
(-deref [_] teams)
|
|
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [team-ids (map :id teams)
|
|
;; Delete old teams from state
|
|
state (update state :teams #(select-keys % team-ids))]
|
|
(reduce (fn [state {:keys [id organization-id] :as team}]
|
|
(let [team-merged (merge (dm/get-in state [:teams id]) team)
|
|
has-org? (or (some? organization-id) (some? (:organization team)))
|
|
team-updated (if has-org?
|
|
team-merged
|
|
(dissoc team-merged
|
|
:organization
|
|
:organization-id
|
|
:organization-name
|
|
:organization-slug
|
|
:organization-owner-id
|
|
:organization-avatar-bg-url
|
|
:organization-permissions))]
|
|
(update state :teams assoc id team-updated)))
|
|
state
|
|
teams)))))
|
|
|
|
(defn fetch-teams
|
|
[]
|
|
(ptk/reify ::fetch-teams
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(->> (rp/cmd! :get-teams)
|
|
(rx/map teams-fetched)))))
|
|
|
|
(defn- update-team-data
|
|
[state team-id f & args]
|
|
(cond
|
|
(contains? (:teams state) team-id)
|
|
(apply update-in state [:teams team-id] f args)
|
|
|
|
(= team-id (dm/get-in state [:current-team :id]))
|
|
(apply update state :current-team f args)
|
|
|
|
:else
|
|
state))
|
|
|
|
(defn with-refreshed-team
|
|
"Fetches fresh team data from the server to ensure up-to-date organization
|
|
permissions, updates the app state, and calls f with the fresh team data.
|
|
Returns an observable of events."
|
|
[team-id f]
|
|
(->> (rp/cmd! :get-teams)
|
|
(rx/mapcat
|
|
(fn [teams]
|
|
(let [team (d/seek #(= (:id %) team-id) teams)]
|
|
(rx/concat
|
|
(rx/of (teams-fetched teams))
|
|
(f team)))))))
|
|
|
|
(defn check-and-create-team
|
|
"Fetches fresh team data from the server to ensure up-to-date organization
|
|
permissions, then shows the team-form modal or a no-permission modal."
|
|
[team-id]
|
|
(ptk/reify ::check-and-create-team
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [profile-id (dm/get-in state [:profile :id])]
|
|
(with-refreshed-team team-id
|
|
(fn [team]
|
|
(let [organization (:organization team)
|
|
in-organization? (and (contains? cf/flags :admin-console) organization)
|
|
can-create? (if in-organization?
|
|
(cto/allowed? :create-team
|
|
{:organization-perms {:owner-id (:owner-id organization)
|
|
:permissions (:permissions organization)}
|
|
:profile-id profile-id
|
|
:team-perms (:permissions team)})
|
|
true)]
|
|
(rx/of (if can-create?
|
|
(modal/show :team-form (if in-organization?
|
|
{:organization-id (:id organization)
|
|
:organization-name (:name organization)}
|
|
{}))
|
|
(modal/show :no-permission-modal {:type :create-team}))))))))))
|
|
|
|
(defn check-and-delete-team
|
|
"Fetches fresh team data from the server to ensure up-to-date organization
|
|
permissions, then shows the confirmation modal or a no-permission modal."
|
|
[{:keys [team-id delete-fn]}]
|
|
(ptk/reify ::check-and-delete-team
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [profile-id (dm/get-in state [:profile :id])]
|
|
(with-refreshed-team team-id
|
|
(fn [team]
|
|
(let [organization (:organization team)
|
|
in-organization? (and (contains? cf/flags :admin-console) organization)
|
|
can-delete? (if in-organization?
|
|
(cto/allowed? :delete-team
|
|
{:organization-perms {:owner-id (:owner-id organization)
|
|
:permissions (:permissions organization)}
|
|
:profile-id profile-id
|
|
:team-perms (:permissions team)})
|
|
(boolean (dm/get-in team [:permissions :is-owner])))
|
|
message (if in-organization?
|
|
(tr "modals.delete-organization-team-confirm.message" (:name organization))
|
|
(tr "modals.delete-team-confirm.message"))]
|
|
(rx/of (if can-delete?
|
|
(modal/show
|
|
{:type :confirm
|
|
:title (tr "modals.delete-team-confirm.title")
|
|
:message message
|
|
:accept-label (tr "modals.delete-team-confirm.accept")
|
|
:on-accept delete-fn})
|
|
(modal/show :no-permission-modal {:type :delete-team}))))))))))
|
|
|
|
(defn- check-new-team-members-permission-and-show-invite-members
|
|
"Receives refreshed team data with up-to-date organization
|
|
permissions, then shows the invite members modal or an appropriate alert."
|
|
[{:keys [team invite-email origin]}]
|
|
(ptk/reify ::check-new-team-members-permission-and-show-invite-members
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [show-invite (rx/of (modal/show {:type :invite-members
|
|
:team team
|
|
:origin (or origin :team)
|
|
:invite-email invite-email}))]
|
|
(if (and (contains? cf/flags :admin-console)
|
|
(not (cto/allowed? :add-anybody-to-team
|
|
{:organization-perms (:organization team)})))
|
|
(->> (rp/cmd! :all-organization-members-in-team
|
|
{:team-id (:id team)
|
|
:organization-id (get-in team [:organization :id])})
|
|
(rx/mapcat
|
|
(fn [all-organization-members-in-team?]
|
|
(if all-organization-members-in-team?
|
|
(rx/of (modal/show
|
|
{:type :alert
|
|
:message (tr "modals.invite-restricted-members.all-organization-members-in-team" (get-in team [:organization :name]))
|
|
:accept-label (tr "labels.accept")
|
|
:accept-style :primary
|
|
:title (tr "modals.invite-team-member.title")}))
|
|
show-invite))))
|
|
show-invite)))))
|
|
|
|
(defn check-and-invite-members
|
|
"Fetches fresh team data from the server to ensure up-to-date organization
|
|
permissions, then shows invite-members modal or a permission error."
|
|
[{:keys [team-id origin invite-email]
|
|
:or {origin :team}}]
|
|
(ptk/reify ::check-and-invite-members
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [profile-id (dm/get-in state [:profile :id])]
|
|
(with-refreshed-team team-id
|
|
(fn [team]
|
|
(let [organization (:organization team)
|
|
can-invite? (cto/can-send-invitations?
|
|
{:organization organization
|
|
:profile-id profile-id
|
|
:team-permissions (:permissions team)})]
|
|
(rx/of (if can-invite?
|
|
(check-new-team-members-permission-and-show-invite-members {:team team
|
|
:origin origin
|
|
:invite-email invite-email})
|
|
(modal/show :no-permission-modal {:type :invite-members}))))))))))
|
|
|
|
;; --- EVENT: fetch-members
|
|
|
|
(defn- members-fetched
|
|
[team-id members]
|
|
(ptk/reify ::members-fetched
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(update-team-data team-id assoc :members members)
|
|
(update :profiles merge (d/index-by :id members))))))
|
|
|
|
(defn fetch-members
|
|
([] (fetch-members nil))
|
|
([team-id]
|
|
(ptk/reify ::fetch-members
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-let [team-id (or team-id (:current-team-id state))]
|
|
(->> (rp/cmd! :get-team-members {:team-id team-id})
|
|
(rx/map (partial members-fetched team-id))
|
|
(rx/catch (fn [cause]
|
|
(let [{:keys [type]} (ex-data cause)]
|
|
(if (= :not-found type)
|
|
(do
|
|
(log/warn :hint "fetch-members: team not found, skipping"
|
|
:team-id (str team-id))
|
|
(rx/empty))
|
|
(rx/throw cause)))))))))))
|
|
|
|
(defn- invitations-fetched
|
|
[team-id invitations]
|
|
(ptk/reify ::invitations-fetched
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update-team-data state team-id assoc :invitations invitations))))
|
|
|
|
(defn fetch-invitations
|
|
[]
|
|
(ptk/reify ::fetch-invitations
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team-id (:current-team-id state)]
|
|
(->> (rp/cmd! :get-team-invitations {:team-id team-id})
|
|
(rx/map (partial invitations-fetched team-id)))))))
|
|
|
|
(defn- team-initialized
|
|
[team-id]
|
|
(ptk/reify ::team-initialized
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team (dsh/lookup-team state team-id)]
|
|
(if team
|
|
(let [permissions (get team :permissions)
|
|
features (get team :features)]
|
|
(rx/of #(-> %
|
|
(assoc :current-team team)
|
|
(assoc :permissions permissions))
|
|
(features/initialize features)
|
|
(fetch-members team-id)))
|
|
(->> (rp/cmd! :get-team {:id team-id})
|
|
(rx/mapcat (fn [team]
|
|
(let [permissions (get team :permissions)
|
|
features (get team :features)]
|
|
(rx/of #(-> %
|
|
(assoc :current-team team)
|
|
(assoc :permissions permissions))
|
|
(features/initialize features)
|
|
(fetch-members team-id)))))))))
|
|
|
|
ptk/EffectEvent
|
|
(effect [_ _ _]
|
|
(swap! storage/global assoc ::current-team-id team-id))))
|
|
|
|
(defn initialize-team
|
|
[team-id]
|
|
(ptk/reify ::initialize-team
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(assoc :current-team-id team-id)
|
|
(dissoc :current-team)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
(let [stopper (rx/filter (ptk/type? ::finalize-team) stream)]
|
|
(->> (rx/merge
|
|
(rx/of (fetch-teams))
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::teams-fetched))
|
|
(rx/map (partial team-initialized team-id))))
|
|
(rx/take-until stopper))))))
|
|
|
|
(defn finalize-team
|
|
[team-id]
|
|
(ptk/reify ::finalize-team
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [team-id' (get state :current-team-id)]
|
|
(if (= team-id' team-id)
|
|
(-> state
|
|
(dissoc :current-team-id)
|
|
(dissoc :current-team)
|
|
(dissoc :shared-files)
|
|
(dissoc :fonts))
|
|
state)))))
|
|
|
|
;; --- ROLES
|
|
|
|
(defn update-member-role
|
|
[{:keys [role member-id] :as params}]
|
|
|
|
(assert (uuid? member-id))
|
|
(assert (contains? ctt/valid-roles role))
|
|
|
|
(ptk/reify ::update-member-role
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team-id (:current-team-id state)
|
|
params (assoc params :team-id team-id)]
|
|
(->> (rp/cmd! :update-team-member-role params)
|
|
(rx/mapcat (fn [_]
|
|
(rx/of (dp/refresh-profile)
|
|
(fetch-members team-id)
|
|
(fetch-teams)
|
|
(ev/event {::ev/name "update-team-member-role"
|
|
:team-id team-id
|
|
:role role
|
|
:member-id member-id})))))))))
|
|
|
|
(defn delete-member
|
|
[{:keys [member-id] :as params}]
|
|
(dm/assert! (uuid? member-id))
|
|
(ptk/reify ::delete-member
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team-id (:current-team-id state)
|
|
params (assoc params :team-id team-id)]
|
|
(->> (rp/cmd! :delete-team-member params)
|
|
(rx/mapcat (fn [_]
|
|
(rx/of (dp/refresh-profile)
|
|
(fetch-members team-id)
|
|
(fetch-teams)
|
|
(ev/event
|
|
{::ev/name "delete-team-member"
|
|
:team-id team-id
|
|
:member-id member-id})))))))))
|
|
|
|
|
|
(defn- stats-fetched
|
|
[team-id stats]
|
|
(ptk/reify ::stats-fetched
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update-team-data state team-id assoc :stats stats))))
|
|
|
|
(defn fetch-stats
|
|
[]
|
|
(ptk/reify ::fetch-stats
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team-id (:current-team-id state)]
|
|
(->> (rp/cmd! :get-team-stats {:team-id team-id})
|
|
(rx/map (partial stats-fetched team-id)))))))
|
|
|
|
(defn- webhooks-fetched
|
|
[team-id webhooks]
|
|
(ptk/reify ::webhooks-fetched
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update-team-data state team-id assoc :webhooks webhooks))))
|
|
|
|
(defn fetch-webhooks
|
|
[]
|
|
(ptk/reify ::fetch-webhooks
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team-id (:current-team-id state)]
|
|
(->> (rp/cmd! :get-webhooks {:team-id team-id})
|
|
(rx/map (partial webhooks-fetched team-id)))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Data Modification
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn update-team-photo
|
|
[file]
|
|
(dm/assert!
|
|
"expected a valid blob for `file` param"
|
|
(di/blob? file))
|
|
(ptk/reify ::update-team-photo
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [on-success di/notify-finished-loading
|
|
on-error #(do (di/notify-finished-loading)
|
|
(di/process-error %))
|
|
team-id (:current-team-id state)
|
|
prepare #(hash-map :file % :team-id team-id)]
|
|
|
|
(di/notify-start-loading)
|
|
(->> (rx/of file)
|
|
(rx/map di/validate-file)
|
|
(rx/map prepare)
|
|
(rx/mapcat #(rp/cmd! :update-team-photo %))
|
|
(rx/tap on-success)
|
|
(rx/mapcat (fn [_]
|
|
(rx/of (fetch-teams)
|
|
(ev/event
|
|
{::ev/name "update-team-photo"
|
|
:team-id team-id}))))
|
|
(rx/catch on-error))))))
|
|
|
|
|
|
;; --- EVENT: create-team
|
|
|
|
(defn- team-created
|
|
[team]
|
|
(ptk/reify ::team-created
|
|
IDeref
|
|
(-deref [_] team)))
|
|
|
|
(defn create-team
|
|
[{:keys [name organization-id] :as params}]
|
|
(dm/assert! (string? name))
|
|
(ptk/reify ::create-team
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [{:keys [on-success on-error]
|
|
:or {on-success identity
|
|
on-error rx/throw}} (meta params)
|
|
features features/global-enabled-features
|
|
organization-team-count-before
|
|
(when organization-id
|
|
(nitrate-audit/organization-team-count
|
|
(vals (:teams state))
|
|
organization-id))
|
|
subscription-status
|
|
(or (dm/get-in state [:profile :subscription :status])
|
|
"active")
|
|
audit-event
|
|
(when organization-id
|
|
(nitrate-audit/add-team-to-organization-event
|
|
{:organization-id organization-id
|
|
:organization-team-count-before organization-team-count-before
|
|
:team-previous-organization-status "no-organization"
|
|
:add-method "create-team-in-organization"
|
|
:subscription-status subscription-status}))
|
|
params (cond-> {:name name :features features}
|
|
organization-id (assoc :organization-id organization-id))]
|
|
(rx/concat
|
|
(when audit-event
|
|
(rx/of audit-event))
|
|
(->> (rp/cmd! :create-team (with-meta params (meta it)))
|
|
(rx/tap on-success)
|
|
(rx/map team-created)
|
|
(rx/catch on-error)))))))
|
|
|
|
;; --- EVENT: create-team-with-invitations
|
|
|
|
(defn create-team-with-invitations
|
|
[{:keys [name emails role] :as params}]
|
|
(ptk/reify ::create-team-with-invitations
|
|
ptk/WatchEvent
|
|
(watch [it _ _]
|
|
(let [{:keys [on-success on-error]
|
|
:or {on-success identity
|
|
on-error rx/throw}} (meta params)
|
|
features features/global-enabled-features
|
|
params {:name name
|
|
:emails emails
|
|
:role role
|
|
:features features}]
|
|
(->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it)))
|
|
(rx/tap on-success)
|
|
(rx/map team-created)
|
|
(rx/catch on-error))))))
|
|
|
|
(defn update-team
|
|
[{:keys [id name] :as params}]
|
|
(ptk/reify ::update-team
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:teams id :name] name))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(->> (rp/cmd! :update-team params)
|
|
(rx/ignore)))))
|
|
|
|
(defn- team-leaved
|
|
[{:keys [id] :as params}]
|
|
(ptk/reify ::team-leaved
|
|
IDeref
|
|
(-deref [_] params)
|
|
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update state :teams dissoc id))
|
|
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(let [teams (get state :teams)]
|
|
(when-let [ctid (::current-team-id storage/user)]
|
|
(when-not (contains? teams ctid)
|
|
(swap! storage/user dissoc ::current-team-id)))))))
|
|
|
|
(defn leave-current-team
|
|
"High-level event for leave team, mainly executed from the
|
|
dashboard. It automatically redirects user to the default team, once
|
|
the team-leave operation succeed"
|
|
[{:keys [reassign-to] :as params}]
|
|
|
|
(when reassign-to
|
|
(assert (uuid? reassign-to) "expect a valid uuid for `reassign-to`"))
|
|
|
|
(ptk/reify ::leave-current-team
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team-id (get state :current-team-id)
|
|
params (assoc params :id team-id)
|
|
|
|
{:keys [on-error on-success]
|
|
:or {on-success rx/empty
|
|
on-error rx/throw}}
|
|
(meta params)]
|
|
|
|
(->> (rp/cmd! :leave-team params)
|
|
(rx/mapcat
|
|
(fn [_]
|
|
(rx/merge
|
|
(rx/of (team-leaved params)
|
|
(fetch-teams)
|
|
(ev/event
|
|
{::ev/name "leave-team"
|
|
:reassign-to reassign-to
|
|
:team-id team-id}))
|
|
(on-success))))
|
|
(rx/catch on-error))))))
|
|
|
|
|
|
(def ^:private schema:create-invitation
|
|
[:and
|
|
[:map
|
|
[:emails {:optional true} [::sm/set ::sm/email]]
|
|
[:invitations {:optional true}
|
|
[:vector
|
|
[:map
|
|
[:email ::sm/email]
|
|
[:role [::sm/one-of ctt/valid-roles]]]]]
|
|
[:team-id ::sm/uuid]
|
|
[:resend? {:optional true} ::sm/boolean]]
|
|
[:fn (fn [attrs]
|
|
(or (contains? attrs :emails)
|
|
(contains? attrs :invitations)))]])
|
|
|
|
(def ^:private check-create-invitations-params
|
|
(sm/check-fn schema:create-invitation))
|
|
|
|
(defn create-invitations
|
|
"Unified function to create invitations. Supports two parameter formats:
|
|
1. {:emails #{...} :role :admin :team-id uuid} - single role for all emails
|
|
2. {:invitations [{:email ... :role ...}] :team-id uuid} - individual roles per email"
|
|
[{:keys [emails role team-id invitations resend?] :as params}]
|
|
(check-create-invitations-params params)
|
|
|
|
(ptk/reify ::create-invitations
|
|
ev/Event
|
|
(-data [_]
|
|
{:role (if invitations
|
|
(->> invitations (map :role) distinct (map name) (str/join ", "))
|
|
(name role))
|
|
:team-id team-id
|
|
:resend (boolean resend?)})
|
|
|
|
ptk/WatchEvent
|
|
(watch [it _ _]
|
|
(let [{:keys [on-success on-error]
|
|
:or {on-success identity
|
|
on-error rx/throw}} (meta params)
|
|
;; Prepare parameters based on format
|
|
rpc-params (cond
|
|
;; Format 1: emails + single role
|
|
(and emails role)
|
|
{:emails emails :role role :team-id team-id}
|
|
|
|
;; Format 2: invitations with individual roles
|
|
invitations
|
|
{:invitations invitations :team-id team-id}
|
|
|
|
:else
|
|
(throw (ex-info " Invalid parameters " params)))]
|
|
(->> (rp/cmd! :create-team-invitations (with-meta rpc-params (meta it)))
|
|
(rx/tap on-success)
|
|
(rx/catch on-error))))))
|
|
|
|
(defn check-and-submit-invite-members
|
|
"Fetches fresh team data from the server to ensure up-to-date organization
|
|
permissions, then submits member invitations or shows a permission error."
|
|
[{:keys [team-id] :as params} origin do-invite-members]
|
|
(ptk/reify ::check-and-submit-invite-members
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [profile-id (dm/get-in state [:profile :id])]
|
|
(if (contains? cf/flags :admin-console)
|
|
(with-refreshed-team team-id
|
|
(fn [team]
|
|
(if (not (cto/can-send-invitations?
|
|
{:organization (:organization team)
|
|
:profile-id profile-id
|
|
:team-permissions (:permissions team)}))
|
|
(rx/of (modal/show :no-permission-modal {:type :invite-members}))
|
|
(if (not (cto/allowed? :add-anybody-to-team
|
|
{:organization-perms (:organization team)}))
|
|
(->> (rp/cmd! :check-organization-members {:organization-id (get-in team [:organization :id])
|
|
:emails (vec (:emails params))})
|
|
(rx/mapcat
|
|
(fn [result]
|
|
(let [blocked (into [] (comp (filter (fn [[_ v]] (not v)))
|
|
(map first))
|
|
result)]
|
|
(cond
|
|
(empty? blocked)
|
|
(do (do-invite-members params origin) (rx/empty))
|
|
|
|
(= (count blocked) (count result))
|
|
(rx/of
|
|
(modal/show
|
|
{:type :alert
|
|
:title (tr "modals.invite-restricted-members.all-blocked-title")
|
|
:message (tr "modals.invite-restricted-members.all-blocked")
|
|
:accept-label (tr "labels.accept")
|
|
:accept-style :primary}))
|
|
|
|
:else
|
|
(rx/of
|
|
(modal/show
|
|
{:type :invite-restricted-members
|
|
:blocked-emails blocked
|
|
:on-accept (fn []
|
|
(let [valid-emails (into #{} (filter (fn [e] (get result e)))
|
|
(:emails params))
|
|
params' (assoc params :emails valid-emails)]
|
|
(do-invite-members params' origin)))})))))))
|
|
(do (do-invite-members params origin)
|
|
(rx/empty))))))
|
|
(do (do-invite-members params origin)
|
|
(rx/empty)))))))
|
|
|
|
(defn copy-invitation-link
|
|
[{:keys [email team-id] :as params}]
|
|
(assert (sm/check-email email))
|
|
(assert (uuid? team-id))
|
|
|
|
(ptk/reify ::copy-invitation-link
|
|
IDeref
|
|
(-deref [_] {:email email :team-id team-id})
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [{:keys [on-success on-error]
|
|
:or {on-success identity
|
|
on-error rx/throw}} (meta params)
|
|
router (:router state)]
|
|
|
|
(->> (rp/cmd! :get-team-invitation-token params)
|
|
(rx/map (fn [params]
|
|
(rt/resolve-uri router :auth-verify-token params)))
|
|
(rx/tap (fn [uri]
|
|
(clipboard/to-clipboard uri)))
|
|
(rx/tap on-success)
|
|
(rx/ignore)
|
|
(rx/catch on-error))))))
|
|
|
|
(defn update-invitation-role
|
|
[{:keys [email team-id role] :as params}]
|
|
(assert (sm/check-email email))
|
|
(assert (uuid? team-id))
|
|
(assert (contains? ctt/valid-roles role))
|
|
|
|
(ptk/reify ::update-invitation-role
|
|
IDeref
|
|
(-deref [_] {:role role})
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [{:keys [on-success on-error]
|
|
:or {on-success identity
|
|
on-error rx/throw}} (meta params)]
|
|
(->> (rp/cmd! :update-team-invitation-role params)
|
|
(rx/tap on-success)
|
|
(rx/catch on-error))))))
|
|
|
|
(defn delete-invitation
|
|
[{:keys [email team-id] :as params}]
|
|
(assert (sm/check-email email))
|
|
(assert (uuid? team-id))
|
|
|
|
(ptk/reify ::delete-invitation
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [{:keys [on-success on-error]
|
|
:or {on-success identity
|
|
on-error rx/throw}} (meta params)]
|
|
(->> (rp/cmd! :delete-team-invitation params)
|
|
(rx/tap on-success)
|
|
(rx/catch on-error))))))
|
|
|
|
(defn- team-deleted
|
|
[id]
|
|
(ptk/reify ::team-deleted
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update state :teams dissoc id))))
|
|
|
|
(defn delete-team
|
|
[{:keys [id] :as params}]
|
|
(ptk/reify ::delete-team
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [{:keys [on-success on-error]
|
|
:or {on-success rx/empty
|
|
on-error rx/throw}}
|
|
(meta params)]
|
|
|
|
(->> (rp/cmd! :delete-team {:id id})
|
|
(rx/mapcat (fn [result]
|
|
(rx/concat
|
|
(rx/of (team-deleted id))
|
|
(on-success result))))
|
|
(rx/catch on-error))))))
|
|
|
|
(defn delete-webhook
|
|
[{:keys [id] :as params}]
|
|
(dm/assert! (uuid? id))
|
|
|
|
(ptk/reify ::delete-webhook
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team-id (:current-team-id state)
|
|
params (assoc params :team-id team-id)
|
|
{:keys [on-success on-error]
|
|
:or {on-success identity
|
|
on-error rx/throw}} (meta params)]
|
|
(->> (rp/cmd! :delete-webhook params)
|
|
(rx/tap on-success)
|
|
(rx/catch on-error))))))
|
|
|
|
(def valid-mtypes
|
|
#{"application/json"
|
|
"application/x-www-form-urlencoded"
|
|
"application/transit+json"})
|
|
|
|
(defn update-webhook
|
|
[{:keys [id uri mtype is-active] :as params}]
|
|
(dm/assert! (uuid? id))
|
|
(dm/assert! (contains? valid-mtypes mtype))
|
|
(dm/assert! (boolean? is-active))
|
|
(dm/assert! (u/uri? uri))
|
|
|
|
(ptk/reify ::update-webhook
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team-id (:current-team-id state)
|
|
params (assoc params :team-id team-id)
|
|
{:keys [on-success on-error]
|
|
:or {on-success rx/empty
|
|
on-error rx/throw}} (meta params)]
|
|
(->> (rp/cmd! :update-webhook params)
|
|
(rx/mapcat (fn [_]
|
|
(rx/concat
|
|
(on-success)
|
|
(rx/of (fetch-webhooks)))))
|
|
(rx/catch on-error))))))
|
|
|
|
(defn create-webhook
|
|
[{:keys [uri mtype is-active] :as params}]
|
|
|
|
(assert (contains? valid-mtypes mtype))
|
|
(assert (boolean? is-active))
|
|
(assert (u/uri? uri))
|
|
|
|
(ptk/reify ::create-webhook
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [team-id (:current-team-id state)
|
|
params (-> params
|
|
(assoc :team-id team-id)
|
|
(update :uri str))
|
|
{:keys [on-success on-error]
|
|
:or {on-success rx/empty
|
|
on-error rx/throw}} (meta params)]
|
|
(->> (rp/cmd! :create-webhook params)
|
|
(rx/mapcat (fn [_]
|
|
(rx/concat
|
|
(on-success)
|
|
(rx/of (fetch-webhooks)))))
|
|
(rx/catch on-error))))))
|
|
|
|
(defn- shared-files-fetched
|
|
[files]
|
|
(ptk/reify ::shared-files-fetched
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [files (d/index-by :id files)]
|
|
(update state :shared-files merge files)))))
|
|
|
|
(defn fetch-shared-files
|
|
"Event mainly used for fetch a list of shared libraries for a team,
|
|
this list does not includes the content of the library per se. It
|
|
is used mainly for show available libraries and a summary of it."
|
|
([] (fetch-shared-files nil))
|
|
([team-id]
|
|
(ptk/reify ::fetch-shared-files
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-let [team-id (or team-id (:current-team-id state))]
|
|
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
|
|
(rx/map shared-files-fetched)))))))
|
|
|
|
|
|
(defn team->organization [team]
|
|
(when-let [organization (:organization team)]
|
|
(assoc organization :default-team-id (:id team))))
|