mirror of
https://github.com/penpot/penpot.git
synced 2025-04-13 23:41:24 -05:00
♻️ Refactor dashboard bootstrap
This commit is contained in:
parent
b31afcfb47
commit
b17d7c0289
45 changed files with 1684 additions and 1500 deletions
|
@ -575,7 +575,7 @@
|
|||
(if-let [media-id (:media-id row)]
|
||||
(-> row
|
||||
(dissoc :media-id)
|
||||
(assoc :thumbnail-uri (resolve-public-uri media-id)))
|
||||
(assoc :thumbnail-id media-id))
|
||||
(dissoc row :media-id))))
|
||||
(map #(assoc % :library-summary (get-library-summary cfg %)))
|
||||
(map #(dissoc % :data))))))
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
[app.common.schema :as sm]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :refer [resolve-public-uri]]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
|
@ -61,7 +60,7 @@
|
|||
(if-let [media-id (:media-id row)]
|
||||
(-> row
|
||||
(dissoc :media-id)
|
||||
(assoc :thumbnail-uri (resolve-public-uri media-id)))
|
||||
(assoc :thumbnail-id media-id))
|
||||
(dissoc row :media-id))))))
|
||||
|
||||
(def ^:private schema:search-files
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"~:modified-at": "~m1714045654874",
|
||||
"~:name": "New File 2",
|
||||
"~:revn": 1,
|
||||
"~:thumbnail-id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe",
|
||||
"~:is-shared": false
|
||||
},
|
||||
{
|
||||
|
@ -15,6 +16,7 @@
|
|||
"~:modified-at": "~m1713519762931",
|
||||
"~:name": "New File 1",
|
||||
"~:revn": 1,
|
||||
"~:thumbnail-id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe",
|
||||
"~:is-shared": false
|
||||
}
|
||||
]
|
||||
|
|
BIN
frontend/playwright/data/dashboard/thumbnail.png
Normal file
BIN
frontend/playwright/data/dashboard/thumbnail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -110,6 +110,10 @@ export class DashboardPage extends BaseWebSocketPage {
|
|||
"get-project-files?project-id=*",
|
||||
"dashboard/get-project-files.json",
|
||||
);
|
||||
|
||||
await this.mockRPC(/assets\/by-id/gi, "dashboard/thumbnail.png", {
|
||||
contentType: "image/png",
|
||||
});
|
||||
}
|
||||
|
||||
async setupNewProject() {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[app.common.logging :as log]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.websocket :as ws]
|
||||
|
@ -68,12 +69,12 @@
|
|||
(rx/merge
|
||||
(rx/of (du/fetch-profile))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::profile-fetched))
|
||||
(rx/filter du/profile-fetched?)
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/mapcat (fn [profile]
|
||||
(if (du/is-authenticated? profile)
|
||||
(rx/of (du/fetch-teams))
|
||||
(rx/of (du/initialize-profile profile))
|
||||
(rx/empty))))
|
||||
(rx/observe-on :async))))
|
||||
|
||||
|
@ -92,6 +93,11 @@
|
|||
|
||||
(initialize-profile stream)
|
||||
|
||||
;; Watch for profile deletion events
|
||||
(->> stream
|
||||
(rx/filter du/profile-deleted?)
|
||||
(rx/map da/logged-out))
|
||||
|
||||
;; Once profile is fetched, initialize all penpot application
|
||||
;; routes
|
||||
(->> stream
|
||||
|
|
317
frontend/src/app/main/data/auth.cljs
Normal file
317
frontend/src/app/main/data/auth.cljs
Normal file
|
@ -0,0 +1,317 @@
|
|||
;; 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.main.data.auth
|
||||
"Auth related data events"
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.websocket :as ws]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :as storage]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(defn is-authenticated?
|
||||
[{:keys [id]}]
|
||||
(and (uuid? id) (not= id uuid/zero)))
|
||||
|
||||
;; --- EVENT: login
|
||||
|
||||
(defn- logged-in
|
||||
"This is the main event that is executed once we have logged in
|
||||
profile. The profile can proceed from standard login or from
|
||||
accepting invitation, or third party auth signup or singin."
|
||||
[{:keys [props] :as profile}]
|
||||
(letfn [(get-redirect-events [teams]
|
||||
(if-let [redirect-href (:login-redirect storage/session)]
|
||||
(binding [storage/*sync* true]
|
||||
(swap! storage/session dissoc :login-redirect)
|
||||
(if (= redirect-href (rt/get-current-href))
|
||||
(rx/of (rt/reload true))
|
||||
(rx/of (rt/nav-raw :href redirect-href))))
|
||||
(if-let [file-id (get props :welcome-file-id)]
|
||||
(rx/of (rt/nav' :workspace {:project-id (:default-project-id profile)
|
||||
:file-id file-id})
|
||||
(du/update-profile-props {:welcome-file-id nil}))
|
||||
|
||||
(let [teams (into #{} (map :id) teams)
|
||||
team-id (dtm/get-last-team-id)
|
||||
team-id (if (and team-id (contains? teams team-id))
|
||||
team-id
|
||||
(:default-team-id profile))]
|
||||
(rx/of (rt/nav' :dashboard-projects {:team-id team-id}))))))]
|
||||
|
||||
(ptk/reify ::logged-in
|
||||
ev/Event
|
||||
(-data [_]
|
||||
{::ev/name "signin"
|
||||
::ev/type "identify"
|
||||
:email (:email profile)
|
||||
:auth-backend (:auth-backend profile)
|
||||
:fullname (:fullname profile)
|
||||
:is-muted (:is-muted profile)
|
||||
:default-team-id (:default-team-id profile)
|
||||
:default-project-id (:default-project-id profile)})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(->> (rx/merge
|
||||
(rx/of (du/initialize-profile profile)
|
||||
(ws/initialize)
|
||||
(dtm/fetch-teams))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dtm/teams-fetched))
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/mapcat get-redirect-events)))
|
||||
|
||||
(rx/observe-on :async))))))
|
||||
|
||||
(declare login-from-register)
|
||||
|
||||
(defn login
|
||||
[{:keys [email password invitation-token] :as data}]
|
||||
(ptk/reify ::login
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [{:keys [on-error on-success]
|
||||
:or {on-error rx/throw
|
||||
on-success identity}} (meta data)
|
||||
|
||||
params {:email email
|
||||
:password password
|
||||
:invitation-token invitation-token}]
|
||||
|
||||
;; NOTE: We can't take the profile value from login because
|
||||
;; there are cases when login is successful but the cookie is
|
||||
;; not set properly (because of possible misconfiguration).
|
||||
;; So, we proceed to make an additional call to fetch the
|
||||
;; profile, and ensure that cookie is set correctly. If
|
||||
;; profile fetch is successful, we mark the user logged in, if
|
||||
;; the returned profile is an NOT authenticated profile, we
|
||||
;; proceed to logout and show an error message.
|
||||
|
||||
(->> (rp/cmd! :login-with-password (d/without-nils params))
|
||||
(rx/merge-map (fn [data]
|
||||
(rx/merge
|
||||
(rx/of (du/fetch-profile))
|
||||
(->> stream
|
||||
(rx/filter du/profile-fetched?)
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/filter (complement is-authenticated?))
|
||||
(rx/tap on-error)
|
||||
(rx/map #(ex/raise :type :authentication))
|
||||
(rx/observe-on :async))
|
||||
|
||||
(->> stream
|
||||
(rx/filter du/profile-fetched?)
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/filter is-authenticated?)
|
||||
(rx/map (fn [profile]
|
||||
(with-meta (merge data profile)
|
||||
{::ev/source "login"})))
|
||||
(rx/tap on-success)
|
||||
(rx/map logged-in)
|
||||
(rx/observe-on :async)))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(def ^:private schema:login-with-ldap
|
||||
[:map {:title "login-with-ldap"}
|
||||
[:email ::sm/email]
|
||||
[:password :string]])
|
||||
|
||||
(defn login-with-ldap
|
||||
[params]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid params"
|
||||
(sm/check schema:login-with-ldap params))
|
||||
|
||||
(ptk/reify ::login-with-ldap
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-error on-success]
|
||||
:or {on-error rx/throw
|
||||
on-success identity}} (meta params)]
|
||||
(->> (rp/cmd! :login-with-ldap params)
|
||||
(rx/tap on-success)
|
||||
(rx/map (fn [profile]
|
||||
(-> profile
|
||||
(with-meta {::ev/source "login-with-ldap"})
|
||||
(logged-in))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn login-from-token
|
||||
"Used mainly as flow continuation after token validation."
|
||||
[{:keys [profile] :as tdata}]
|
||||
(ptk/reify ::login-from-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))
|
||||
;; NOTE: we need this to be asynchronous because the effect
|
||||
;; should be called before proceed with the login process
|
||||
(rx/observe-on :async)))))
|
||||
|
||||
(defn login-from-register
|
||||
"Event used mainly for mark current session as logged-in in after the
|
||||
user successfully registered using third party auth provider (in this
|
||||
case we dont need to verify the email)."
|
||||
[]
|
||||
(ptk/reify ::login-from-register
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(rx/merge
|
||||
(rx/of (du/fetch-profile))
|
||||
(->> stream
|
||||
(rx/filter du/profile-fetched?)
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/filter is-authenticated?)
|
||||
(rx/map (fn [profile]
|
||||
(with-meta profile
|
||||
{::ev/source "register"})))
|
||||
(rx/map logged-in)
|
||||
(rx/observe-on :async))))))
|
||||
|
||||
;; --- EVENT: logout
|
||||
|
||||
(defn logged-out
|
||||
[]
|
||||
(ptk/reify ::logged-out
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(select-keys state [:route :router :session-id :history]))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/merge
|
||||
;; NOTE: We need the `effect` of the current event to be
|
||||
;; executed before the redirect.
|
||||
(->> (rx/of (rt/nav :auth-login))
|
||||
(rx/observe-on :async))
|
||||
(rx/of (ws/finalize))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
|
||||
(swap! storage/user (constantly {})))))
|
||||
|
||||
(defn logout
|
||||
[]
|
||||
(ptk/reify ::logout
|
||||
ev/Event
|
||||
(-data [_] {})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile-id (:profile-id state)]
|
||||
(->> (rx/interval 500)
|
||||
(rx/take 1)
|
||||
(rx/mapcat (fn [_]
|
||||
(->> (rp/cmd! :logout {:profile-id profile-id})
|
||||
(rx/delay-at-least 300)
|
||||
(rx/catch (constantly (rx/of 1))))))
|
||||
(rx/map logged-out))))))
|
||||
|
||||
;; --- Update Profile
|
||||
|
||||
(def ^:private
|
||||
schema:request-profile-recovery
|
||||
[:map {:title "request-profile-recovery" :closed true}
|
||||
[:email ::sm/email]])
|
||||
|
||||
(defn request-profile-recovery
|
||||
[data]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid parameters"
|
||||
(sm/check schema:request-profile-recovery data))
|
||||
|
||||
(ptk/reify ::request-profile-recovery
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-error on-success]
|
||||
:or {on-error rx/throw
|
||||
on-success identity}} (meta data)]
|
||||
|
||||
(->> (rp/cmd! :request-profile-recovery data)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: recover-profile (Password)
|
||||
|
||||
(def ^:private
|
||||
schema:recover-profile
|
||||
[:map {:title "recover-profile" :closed true}
|
||||
[:password :string]
|
||||
[:token :string]])
|
||||
|
||||
(defn recover-profile
|
||||
[data]
|
||||
(dm/assert!
|
||||
"expected valid arguments"
|
||||
(sm/check schema:recover-profile data))
|
||||
|
||||
(ptk/reify ::recover-profile
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-error on-success]
|
||||
:or {on-error rx/throw
|
||||
on-success identity}} (meta data)]
|
||||
(->> (rp/cmd! :recover-profile data)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: crete-demo-profile
|
||||
|
||||
(defn create-demo-profile
|
||||
[]
|
||||
(ptk/reify ::create-demo-profile
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :create-demo-profile {})
|
||||
(rx/map login)))))
|
||||
|
||||
(defn show-redirect-error
|
||||
"A helper event that interprets the OIDC redirect errors on the URI
|
||||
and shows an appropriate error message using the notification
|
||||
banners."
|
||||
[error]
|
||||
(ptk/reify ::show-redirect-error
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when-let [hint (case error
|
||||
"registration-disabled"
|
||||
(tr "errors.registration-disabled")
|
||||
"profile-blocked"
|
||||
(tr "errors.profile-blocked")
|
||||
"auth-provider-not-allowed"
|
||||
(tr "errors.auth-provider-not-allowed")
|
||||
"email-domain-not-allowed"
|
||||
(tr "errors.email-domain-not-allowed")
|
||||
|
||||
;; We explicitly do not show any error here, it a explicit user operation.
|
||||
"unable-to-auth"
|
||||
nil
|
||||
|
||||
(tr "errors.generic"))]
|
||||
|
||||
(rx/of (ntf/warn hint))))))
|
|
@ -12,16 +12,11 @@
|
|||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.team :as ctt]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dc]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.websocket :as dws]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
|
@ -29,9 +24,8 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.sse :as sse]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.time :as dt]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.set :as set]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
@ -43,143 +37,38 @@
|
|||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare fetch-projects)
|
||||
(declare fetch-team-members)
|
||||
(declare process-message)
|
||||
|
||||
(defn initialize
|
||||
[{:keys [id]}]
|
||||
(dm/assert! (uuid? id))
|
||||
[]
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [prev-team-id (:current-team-id state)]
|
||||
(cond-> state
|
||||
(not= prev-team-id id)
|
||||
(-> (dissoc :current-team-initialized)
|
||||
(dissoc :dashboard-files)
|
||||
(dissoc :dashboard-projects)
|
||||
(dissoc :dashboard-shared-files)
|
||||
(dissoc :dashboard-recent-files)
|
||||
(dissoc :dashboard-team-members)
|
||||
(dissoc :dashboard-team-stats)
|
||||
(assoc :current-team-id id)
|
||||
(update :workspace-global dissoc :default-font)))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stopper (rx/filter (ptk/type? ::finalize) stream)
|
||||
(let [stopper (rx/filter (ptk/type? ::finalize) stream)
|
||||
profile-id (:profile-id state)]
|
||||
|
||||
(->> (rx/merge
|
||||
;; fetch teams must be first in case the team doesn't exist
|
||||
(ptk/watch (du/fetch-teams) state stream)
|
||||
(ptk/watch (df/load-team-fonts id) state stream)
|
||||
(ptk/watch (fetch-projects) state stream)
|
||||
(ptk/watch (fetch-team-members) state stream)
|
||||
(ptk/watch (du/fetch-users) state stream)
|
||||
|
||||
(rx/of (fetch-projects)
|
||||
(df/fetch-fonts))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dws/message))
|
||||
(rx/map deref)
|
||||
(rx/filter (fn [{:keys [topic] :as msg}]
|
||||
(or (= topic uuid/zero)
|
||||
(= topic profile-id))))
|
||||
(rx/map process-message))
|
||||
|
||||
;; Once the teams are fecthed, initialize features related
|
||||
;; to currently active team
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::du/teams-fetched))
|
||||
(rx/observe-on :async)
|
||||
(rx/mapcat deref)
|
||||
(rx/filter #(= id (:id %)))
|
||||
(rx/mapcat (fn [team]
|
||||
(rx/of (du/set-current-team team)
|
||||
#(assoc % :current-team-initialized true))))))
|
||||
(rx/map process-message)
|
||||
(rx/ignore)))
|
||||
|
||||
(rx/take-until stopper))))))
|
||||
|
||||
(defn finalize
|
||||
[params]
|
||||
(ptk/data-event ::finalize params))
|
||||
[]
|
||||
(ptk/data-event ::finalize {}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Fetching (context aware: current team)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; --- EVENT: fetch-team-members
|
||||
|
||||
(defn team-members-fetched
|
||||
[members]
|
||||
(ptk/reify ::team-members-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :dashboard-team-members (d/index-by :id members)))))
|
||||
|
||||
(defn fetch-team-members
|
||||
([] (fetch-team-members nil))
|
||||
([team-id]
|
||||
(ptk/reify ::fetch-team-members
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (or team-id (:current-team-id state))]
|
||||
(assert (uuid? team-id) "expected team-id to be resolved")
|
||||
(->> (rp/cmd! :get-team-members {:team-id team-id})
|
||||
(rx/map team-members-fetched)))))))
|
||||
|
||||
;; --- EVENT: fetch-team-stats
|
||||
|
||||
(defn team-stats-fetched
|
||||
[stats]
|
||||
(ptk/reify ::team-stats-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :dashboard-team-stats stats))))
|
||||
|
||||
(defn fetch-team-stats
|
||||
[team-id]
|
||||
(ptk/reify ::fetch-team-stats
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-team-stats {:team-id team-id})
|
||||
(rx/map team-stats-fetched)))))
|
||||
|
||||
;; --- EVENT: fetch-team-invitations
|
||||
|
||||
(defn team-invitations-fetched
|
||||
[invitations]
|
||||
(ptk/reify ::team-invitations-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :dashboard-team-invitations invitations))))
|
||||
|
||||
(defn fetch-team-invitations
|
||||
[]
|
||||
(ptk/reify ::fetch-team-invitations
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-team-invitations {:team-id team-id})
|
||||
(rx/map team-invitations-fetched))))))
|
||||
|
||||
;; --- EVENT: fetch-team-webhooks
|
||||
|
||||
(defn team-webhooks-fetched
|
||||
[webhooks]
|
||||
(ptk/reify ::team-webhooks-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :dashboard-team-webhooks webhooks))))
|
||||
|
||||
(defn fetch-team-webhooks
|
||||
[]
|
||||
(ptk/reify ::fetch-team-webhooks
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-webhooks {:team-id team-id})
|
||||
(rx/map team-webhooks-fetched))))))
|
||||
|
||||
;; --- EVENT: fetch-projects
|
||||
|
||||
(defn projects-fetched
|
||||
|
@ -187,8 +76,10 @@
|
|||
(ptk/reify ::projects-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [projects (d/index-by :id projects)]
|
||||
(assoc state :dashboard-projects projects)))))
|
||||
(reduce (fn [state {:keys [id] :as project}]
|
||||
(update-in state [:projects id] merge project))
|
||||
state
|
||||
projects))))
|
||||
|
||||
(defn fetch-projects
|
||||
[]
|
||||
|
@ -201,31 +92,28 @@
|
|||
|
||||
;; --- EVENT: search
|
||||
|
||||
(defn search-result-fetched
|
||||
[result]
|
||||
(ptk/reify ::search-result-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :dashboard-search-result result))))
|
||||
|
||||
(def schema:search-params
|
||||
(def ^:private schema:search-params
|
||||
[:map {:closed true}
|
||||
[:search-term [:maybe :string]]])
|
||||
|
||||
(def ^:private check-search-params
|
||||
(sm/check-fn schema:search-params))
|
||||
|
||||
(defn search
|
||||
[params]
|
||||
(dm/assert! schema:search-params params)
|
||||
(ptk/reify ::search
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(dissoc state :dashboard-search-result))
|
||||
(let [params (check-search-params params)]
|
||||
(ptk/reify ::search
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(dissoc state :search-result))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)
|
||||
params (assoc params :team-id team-id)]
|
||||
(->> (rp/cmd! :search-files params)
|
||||
(rx/map search-result-fetched))))))
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)
|
||||
params (assoc params :team-id team-id)]
|
||||
(->> (rp/cmd! :search-files params)
|
||||
(rx/map (fn [result]
|
||||
#(assoc % :search-result result)))))))))
|
||||
|
||||
;; --- EVENT: files
|
||||
|
||||
|
@ -241,11 +129,12 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :dashboard-files
|
||||
(fn [state]
|
||||
(let [state (remove-project-files state)]
|
||||
(reduce #(assoc %1 (:id %2) %2) state files))))
|
||||
(assoc-in [:dashboard-projects project-id :count] (count files)))))))
|
||||
(update :files
|
||||
(fn [files']
|
||||
(reduce #(assoc %1 (:id %2) %2)
|
||||
(remove-project-files files')
|
||||
files)))
|
||||
(assoc-in [:projects project-id :count] (count files)))))))
|
||||
|
||||
(defn fetch-files
|
||||
[{:keys [project-id] :as params}]
|
||||
|
@ -264,19 +153,16 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [files (d/index-by :id files)]
|
||||
(-> state
|
||||
(assoc :dashboard-shared-files files)
|
||||
(update :dashboard-files d/merge files))))))
|
||||
(assoc state :shared-files files)))))
|
||||
|
||||
(defn fetch-shared-files
|
||||
([] (fetch-shared-files nil))
|
||||
([team-id]
|
||||
(ptk/reify ::fetch-shared-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(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)))))))
|
||||
[]
|
||||
(ptk/reify ::fetch-shared-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
|
||||
(rx/map shared-files-fetched))))))
|
||||
|
||||
;; --- EVENT: recent-files
|
||||
|
||||
|
@ -287,8 +173,8 @@
|
|||
(update [_ state]
|
||||
(let [files (d/index-by :id files)]
|
||||
(-> state
|
||||
(assoc :dashboard-recent-files files)
|
||||
(update :dashboard-files d/merge files))))))
|
||||
(assoc :recent-files files)
|
||||
(update :files d/merge files))))))
|
||||
|
||||
(defn fetch-recent-files
|
||||
[]
|
||||
|
@ -325,27 +211,22 @@
|
|||
(ptk/reify ::clear-file-select
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :dashboard-local
|
||||
assoc :selected-files #{}
|
||||
:selected-project nil
|
||||
:menu-open false
|
||||
:menu-pos nil))))
|
||||
(-> state
|
||||
(dissoc :selected-files)
|
||||
(dissoc :selected-project)
|
||||
(update :dashboard-local dissoc :menu-open :menu-pos)))))
|
||||
|
||||
(defn toggle-file-select
|
||||
[{:keys [id project-id] :as file}]
|
||||
(ptk/reify ::toggle-file-select
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [selected-project-id (get-in state [:dashboard-local :selected-project])]
|
||||
(let [selected-project-id (get state :selected-project)]
|
||||
(if (or (nil? selected-project-id)
|
||||
(= selected-project-id project-id))
|
||||
(update state :dashboard-local
|
||||
(fn [local]
|
||||
(-> local
|
||||
(update :selected-files #(if (contains? % id)
|
||||
(disj % id)
|
||||
(conj % id)))
|
||||
(assoc :selected-project project-id))))
|
||||
(-> state
|
||||
(update :selected-files #(if (contains? % id) (disj % id) (conj % id)))
|
||||
(assoc :selected-project project-id))
|
||||
state)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -399,320 +280,6 @@
|
|||
;; Data Modification
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; --- EVENT: create-team
|
||||
|
||||
(defn team-created
|
||||
[team]
|
||||
(ptk/reify ::team-created
|
||||
IDeref
|
||||
(-deref [_] team)))
|
||||
|
||||
(defn create-team
|
||||
[{:keys [name] :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/get-enabled-features state)
|
||||
params {:name name :features features}]
|
||||
(->> (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 state _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
features (features/get-enabled-features state)
|
||||
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))))))
|
||||
|
||||
;; --- EVENT: update-team
|
||||
|
||||
(defn update-team
|
||||
[{:keys [id name] :as params}]
|
||||
(ptk/reify ::update-team
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:teams id :name] name)
|
||||
(assoc-in [:team :name] name)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :update-team params)
|
||||
(rx/ignore)))))
|
||||
|
||||
(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 (du/fetch-teams)
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "update-team-photo"
|
||||
:team-id team-id}))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn update-team-member-role
|
||||
[{:keys [role member-id] :as params}]
|
||||
(dm/assert! (uuid? member-id))
|
||||
(dm/assert! (contains? ctt/valid-roles role))
|
||||
|
||||
(ptk/reify ::update-team-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 (fetch-team-members)
|
||||
(du/fetch-teams)
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "update-team-member-role"
|
||||
:team-id team-id
|
||||
:role role
|
||||
:member-id member-id})))))))))
|
||||
|
||||
(defn delete-team-member
|
||||
[{:keys [member-id] :as params}]
|
||||
(dm/assert! (uuid? member-id))
|
||||
(ptk/reify ::delete-team-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 (fetch-team-members)
|
||||
(du/fetch-teams)
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "delete-team-member"
|
||||
:team-id team-id
|
||||
:member-id member-id})))))))))
|
||||
|
||||
(defn leave-team
|
||||
[{:keys [reassign-to] :as params}]
|
||||
(dm/assert! (or (nil? reassign-to)
|
||||
(uuid? reassign-to)))
|
||||
|
||||
(ptk/reify ::leave-team
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
team-id (:current-team-id state)
|
||||
params (cond-> {:id team-id}
|
||||
(uuid? reassign-to)
|
||||
(assoc :reassign-to reassign-to))]
|
||||
(->> (rp/cmd! :leave-team params)
|
||||
(rx/tap #(tm/schedule on-success))
|
||||
(rx/map (fn [_]
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "leave-team"
|
||||
:reassign-to reassign-to
|
||||
:team-id team-id})))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn invite-team-members
|
||||
[{:keys [emails role team-id resend?] :as params}]
|
||||
(dm/assert! (keyword? role))
|
||||
(dm/assert! (uuid? team-id))
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid set of emails"
|
||||
(sm/check-set-of-emails! emails))
|
||||
|
||||
(ptk/reify ::invite-team-members
|
||||
ev/Event
|
||||
(-data [_]
|
||||
{:role role
|
||||
:team-id team-id
|
||||
:resend resend?})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
params (dissoc params :resend?)]
|
||||
(->> (rp/cmd! :create-team-invitations (with-meta params (meta it)))
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
(defn copy-invitation-link
|
||||
[{:keys [email team-id] :as params}]
|
||||
(dm/assert!
|
||||
"expected a valid email"
|
||||
(sm/check-email! email))
|
||||
|
||||
(dm/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 router :auth-verify-token {} params)))
|
||||
(rx/map (fn [fragment]
|
||||
(assoc cf/public-uri :fragment fragment)))
|
||||
(rx/tap (fn [uri]
|
||||
(wapi/write-to-clipboard (str uri))))
|
||||
(rx/tap on-success)
|
||||
(rx/ignore)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
(defn update-team-invitation-role
|
||||
[{:keys [email team-id role] :as params}]
|
||||
(dm/assert!
|
||||
"expected a valid email"
|
||||
(sm/check-email! email))
|
||||
|
||||
(dm/assert! (uuid? team-id))
|
||||
(dm/assert! (contains? ctt/valid-roles role))
|
||||
|
||||
(ptk/reify ::update-team-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-team-invitation
|
||||
[{:keys [email team-id] :as params}]
|
||||
(dm/assert! (sm/check-email! email))
|
||||
(dm/assert! (uuid? team-id))
|
||||
(ptk/reify ::delete-team-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 delete-team-webhook
|
||||
[{:keys [id] :as params}]
|
||||
(dm/assert! (uuid? id))
|
||||
(ptk/reify ::delete-team-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-team-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-team-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! :update-webhook params)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn create-team-webhook
|
||||
[{:keys [uri mtype is-active] :as params}]
|
||||
(dm/assert! (contains? valid-mtypes mtype))
|
||||
(dm/assert! (boolean? is-active))
|
||||
(dm/assert! (u/uri? uri))
|
||||
|
||||
(ptk/reify ::create-team-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 identity
|
||||
on-error rx/throw}} (meta params)]
|
||||
(->> (rp/cmd! :create-webhook params)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: delete-team
|
||||
|
||||
(defn delete-team
|
||||
[{:keys [id] :as params}]
|
||||
(ptk/reify ::delete-team
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)]
|
||||
(->> (rp/cmd! :delete-team {:id id})
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: create-project
|
||||
|
||||
(defn- project-created
|
||||
|
@ -724,7 +291,7 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:dashboard-projects id] project)
|
||||
(assoc-in [:projects id] project)
|
||||
(assoc-in [:dashboard-local :project-for-edit] id)))))
|
||||
|
||||
(defn create-project
|
||||
|
@ -732,7 +299,7 @@
|
|||
(ptk/reify ::create-project
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [projects (get state :dashboard-projects)
|
||||
(let [projects (get state :projects)
|
||||
unames (cfh/get-used-names projects)
|
||||
name (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))
|
||||
team-id (:current-team-id state)
|
||||
|
@ -753,7 +320,7 @@
|
|||
(ptk/reify ::project-duplicated
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:dashboard-projects id] project))))
|
||||
(assoc-in state [:projects id] project))))
|
||||
|
||||
(defn duplicate-project
|
||||
[{:keys [id name] :as params}]
|
||||
|
@ -803,11 +370,11 @@
|
|||
(ptk/reify ::toggle-project-pin
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:dashboard-projects id :is-pinned] (not is-pinned)))
|
||||
(assoc-in state [:projects id :is-pinned] (not is-pinned)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [project (get-in state [:dashboard-projects id])
|
||||
(let [project (get-in state [:projects id])
|
||||
params (select-keys project [:id :is-pinned :team-id])]
|
||||
(->> (rp/cmd! :update-project-pin params)
|
||||
(rx/ignore))))))
|
||||
|
@ -820,7 +387,7 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update-in [:dashboard-projects id :name] (constantly name))
|
||||
(update-in [:projects id :name] (constantly name))
|
||||
(update :dashboard-local dissoc :project-for-edit)))
|
||||
|
||||
ptk/WatchEvent
|
||||
|
@ -836,7 +403,7 @@
|
|||
(ptk/reify ::delete-project
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :dashboard-projects dissoc id))
|
||||
(update state :projects dissoc id))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
|
@ -850,7 +417,7 @@
|
|||
(ptk/reify ::file-deleted
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:dashboard-projects project-id :count] dec))))
|
||||
(update-in state [:projects project-id :count] dec))))
|
||||
|
||||
(defn delete-file
|
||||
[{:keys [id project-id] :as params}]
|
||||
|
@ -858,9 +425,9 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(d/update-when :dashboard-files dissoc id)
|
||||
(d/update-when :dashboard-shared-files dissoc id)
|
||||
(d/update-when :dashboard-recent-files dissoc id)))
|
||||
(d/update-when :files dissoc id)
|
||||
(d/update-when :shared-files dissoc id)
|
||||
(d/update-when :recent-files dissoc id)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
|
@ -882,9 +449,9 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(d/update-in-when [:dashboard-files id :name] (constantly name))
|
||||
(d/update-in-when [:dashboard-shared-files id :name] (constantly name))
|
||||
(d/update-in-when [:dashboard-recent-files id :name] (constantly name))))
|
||||
(d/update-in-when [:files id :name] (constantly name))
|
||||
(d/update-in-when [:shared-files id :name] (constantly name))
|
||||
(d/update-in-when [:recent-files id :name] (constantly name))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
|
@ -906,10 +473,10 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared))
|
||||
(d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared))
|
||||
(d/update-in-when [:files id :is-shared] (constantly is-shared))
|
||||
(d/update-in-when [:recent-files id :is-shared] (constantly is-shared))
|
||||
(cond-> (not is-shared)
|
||||
(d/update-when :dashboard-shared-files dissoc id))))
|
||||
(d/update-when :shared-files dissoc id))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
|
@ -928,8 +495,8 @@
|
|||
(= file-id (:id %))
|
||||
(assoc :thumbnail-id thumbnail-id)))))]
|
||||
(-> state
|
||||
(d/update-in-when [:dashboard-files file-id] assoc :thumbnail-id thumbnail-id)
|
||||
(d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-id thumbnail-id)
|
||||
(d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id)
|
||||
(d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
|
||||
(d/update-when :dashboard-search-result update-search-files))))))
|
||||
|
||||
;; --- EVENT: create-file
|
||||
|
@ -946,9 +513,9 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:dashboard-files id] file)
|
||||
(assoc-in [:dashboard-recent-files id] file)
|
||||
(update-in [:dashboard-projects project-id :count] inc)))))
|
||||
(assoc-in [:files id] file)
|
||||
(assoc-in [:recent-files id] file)
|
||||
(update-in [:projects project-id :count] inc)))))
|
||||
|
||||
(defn create-file
|
||||
[{:keys [project-id name] :as params}]
|
||||
|
@ -963,7 +530,7 @@
|
|||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
|
||||
files (get state :dashboard-files)
|
||||
files (get state :files)
|
||||
unames (cfh/get-used-names files)
|
||||
name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
|
@ -1015,14 +582,14 @@
|
|||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [origin-project (get-in state [:dashboard-files (first ids) :project-id])
|
||||
(let [origin-project (get-in state [:files (first ids) :project-id])
|
||||
update-project (fn [project delta op]
|
||||
(-> project
|
||||
(update :count #(op % (count ids)))
|
||||
(assoc :modified-at (dt/plus (dt/now) {:milliseconds delta}))))]
|
||||
(-> state
|
||||
(d/update-in-when [:dashboard-projects origin-project] update-project 0 -)
|
||||
(d/update-in-when [:dashboard-projects project-id] update-project 10 +))))
|
||||
(d/update-in-when [:projects origin-project] update-project 0 -)
|
||||
(d/update-in-when [:projects project-id] update-project 10 +))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
|
@ -1077,19 +644,14 @@
|
|||
|
||||
|
||||
(defn go-to-files
|
||||
([project-id]
|
||||
(ptk/reify ::go-to-files-1
|
||||
([project-id] (go-to-files project-id nil))
|
||||
([project-id team-id]
|
||||
(ptk/reify ::go-to-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(let [team-id (or team-id (:current-team-id state))]
|
||||
(rx/of (rt/nav :dashboard-files {:team-id team-id
|
||||
:project-id project-id}))))))
|
||||
([team-id project-id]
|
||||
(ptk/reify ::go-to-files-2
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (rt/nav :dashboard-files {:team-id team-id
|
||||
:project-id project-id}))))))
|
||||
:project-id project-id})))))))
|
||||
|
||||
(defn go-to-search
|
||||
([] (go-to-search nil))
|
||||
|
@ -1112,21 +674,36 @@
|
|||
(dom/focus! (dom/get-element "search-input"))))))
|
||||
|
||||
(defn go-to-projects
|
||||
([]
|
||||
(ptk/reify ::go-to-projects-0
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
|
||||
([team-id]
|
||||
(ptk/reify ::go-to-projects-1
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; FIXME: Remove on 2.5
|
||||
(assoc state :current-team-id team-id))
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
|
||||
[team-id]
|
||||
(ptk/reify ::go-to-projects
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (or team-id (:current-team-id state))]
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
|
||||
|
||||
(defn go-to-default-team
|
||||
"High-level component for redirect to the current profile default
|
||||
team and hide all modals"
|
||||
[]
|
||||
(ptk/reify ::go-to-default-team
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (dm/get-in state [:profile :default-team-id])]
|
||||
(rx/of (go-to-projects team-id)
|
||||
(modal/hide))))))
|
||||
|
||||
|
||||
(defn go-to-current-team
|
||||
"High-level component for redirect to the current profile default
|
||||
team and hide all modals"
|
||||
[]
|
||||
(ptk/reify ::go-to-current-team
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (or (::current-team-id storage/user)
|
||||
(dm/get-in state [:profile :default-team-id]))]
|
||||
(rx/of (go-to-projects team-id)
|
||||
(modal/hide))))))
|
||||
|
||||
(defn go-to-team-members
|
||||
[]
|
||||
|
@ -1166,7 +743,7 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)
|
||||
projects (:dashboard-projects state)
|
||||
projects (:projects state)
|
||||
default-project (d/seek :is-default (vals projects))]
|
||||
(when default-project
|
||||
(rx/of (rt/nav :dashboard-files {:team-id team-id
|
||||
|
@ -1190,10 +767,10 @@
|
|||
pparams (:path-params route)
|
||||
in-project? (contains? pparams :project-id)
|
||||
name (if in-project?
|
||||
(let [files (get state :dashboard-files)
|
||||
(let [files (get state :files)
|
||||
unames (cfh/get-used-names files)]
|
||||
(cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
|
||||
(let [projects (get state :dashboard-projects)
|
||||
(let [projects (get state :projects)
|
||||
unames (cfh/get-used-names projects)]
|
||||
(cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))))
|
||||
params (if in-project?
|
||||
|
@ -1203,7 +780,7 @@
|
|||
:team-id team-id})
|
||||
action-name (if in-project? :create-file :create-project)
|
||||
action (if in-project? file-created project-created)
|
||||
can-edit? (dm/get-in state [:permissions :can-edit])]
|
||||
can-edit? (dm/get-in state [:teams team-id :permissions :can-edit])]
|
||||
|
||||
(when can-edit?
|
||||
(->> (rp/cmd! action-name params)
|
||||
|
@ -1214,9 +791,9 @@
|
|||
(ptk/reify ::open-selected-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [files (get-in state [:dashboard-local :selected-files])]
|
||||
(let [[file-id :as files] (get state :selected-files)]
|
||||
(if (= 1 (count files))
|
||||
(let [file (get-in state [:dashboard-files (first files)])]
|
||||
(let [file (dm/get-in state [files file-id])]
|
||||
(rx/of (go-to-workspace file)))
|
||||
(rx/empty))))))
|
||||
|
||||
|
|
|
@ -59,10 +59,10 @@
|
|||
(adapt-font-id [variant]
|
||||
(update variant :font-id #(str "custom-" %)))]
|
||||
|
||||
(ptk/reify ::team-fonts-loaded
|
||||
(ptk/reify ::fonts-loaded
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :dashboard-fonts (d/index-by :id fonts)))
|
||||
(assoc state :fonts (d/index-by :id fonts)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
|
@ -72,13 +72,14 @@
|
|||
(mapv prepare-font))]
|
||||
(fonts/register! :custom fonts))))))
|
||||
|
||||
(defn load-team-fonts
|
||||
[team-id]
|
||||
(defn fetch-fonts
|
||||
[]
|
||||
(ptk/reify ::load-team-fonts
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-font-variants {:team-id team-id})
|
||||
(rx/map fonts-fetched)))))
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-font-variants {:team-id team-id})
|
||||
(rx/map fonts-fetched))))))
|
||||
|
||||
(defn process-upload
|
||||
"Given a seq of blobs and the team id, creates a ready-to-use fonts
|
||||
|
@ -90,12 +91,15 @@
|
|||
variant (or (.getEnglishName ^js font "preferredSubfamily")
|
||||
(.getEnglishName ^js font "fontSubfamily"))
|
||||
|
||||
;; Vertical metrics determine the baseline in a text and the space between lines of text.
|
||||
;; For historical reasons, there are three pairs of ascender/descender values, known as hhea, OS/2 and uSWin metrics.
|
||||
;; Depending on the font, operating system and application a different set will be used to render text on the screen.
|
||||
;; On Mac, Safari and Chrome use the hhea values to render text. Firefox will respect the useTypoMetrics setting and will use the OS/2 if it is set.
|
||||
;; If the useTypoMetrics is not set, Firefox will also use metrics from the hhea table.
|
||||
;; On Windows, all browsers use the usWin metrics, but respect the useTypoMetrics setting and if set will use the OS/2 values.
|
||||
;; Vertical metrics determine the baseline in a text and the space between lines of
|
||||
;; text. For historical reasons, there are three pairs of ascender/descender
|
||||
;; values, known as hhea, OS/2 and uSWin metrics. Depending on the font, operating
|
||||
;; system and application a different set will be used to render text on the
|
||||
;; screen. On Mac, Safari and Chrome use the hhea values to render text. Firefox
|
||||
;; will respect the useTypoMetrics setting and will use the OS/2 if it is set. If
|
||||
;; the useTypoMetrics is not set, Firefox will also use metrics from the hhea
|
||||
;; table. On Windows, all browsers use the usWin metrics, but respect the
|
||||
;; useTypoMetrics setting and if set will use the OS/2 values.
|
||||
|
||||
hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender))
|
||||
hhea-descender (abs (-> ^js font .-tables .-hhea .-descender))
|
||||
|
@ -239,7 +243,7 @@
|
|||
(ptk/reify ::add-font
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :dashboard-fonts assoc (:id font) font))
|
||||
(update state :fonts assoc (:id font) font))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
|
@ -260,13 +264,10 @@
|
|||
(update [_ state]
|
||||
;; Update all variants that has the same font-id with the new
|
||||
;; name in the local state.
|
||||
(update state :dashboard-fonts
|
||||
(fn [fonts]
|
||||
(d/mapm (fn [_ font]
|
||||
(cond-> font
|
||||
(= id (:font-id font))
|
||||
(assoc :font-family name)))
|
||||
fonts))))
|
||||
(update state :fonts update-vals (fn [font]
|
||||
(cond-> font
|
||||
(= id (:font-id font))
|
||||
(assoc :font-family name)))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
|
@ -285,10 +286,11 @@
|
|||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :dashboard-fonts
|
||||
(update state :fonts
|
||||
(fn [variants]
|
||||
(d/removem (fn [[_id variant]]
|
||||
(= (:font-id variant) font-id)) variants))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
|
@ -305,7 +307,7 @@
|
|||
(ptk/reify ::delete-font-variants
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :dashboard-fonts
|
||||
(update state :fonts
|
||||
(fn [variants]
|
||||
(d/removem (fn [[_ variant]]
|
||||
(= (:id variant) id))
|
||||
|
|
534
frontend/src/app/main/data/team.cljs
Normal file
534
frontend/src/app/main/data/team.cljs
Normal file
|
@ -0,0 +1,534 @@
|
|||
;; 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.main.data.team
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.team :as ctt]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.media :as di]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[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 teams-fetched
|
||||
[teams]
|
||||
(ptk/reify ::teams-fetched
|
||||
IDeref
|
||||
(-deref [_] teams)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(reduce (fn [state {:keys [id] :as team}]
|
||||
(update-in state [:teams id] merge team))
|
||||
state
|
||||
teams))))
|
||||
|
||||
(defn fetch-teams
|
||||
[]
|
||||
(ptk/reify ::fetch-teams
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-teams)
|
||||
(rx/map teams-fetched)))))
|
||||
|
||||
;; --- EVENT: fetch-members
|
||||
|
||||
(defn- members-fetched
|
||||
[team-id members]
|
||||
(ptk/reify ::members-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:teams team-id] assoc :members members))))
|
||||
|
||||
(defn fetch-members
|
||||
[]
|
||||
(ptk/reify ::fetch-members
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-team-members {:team-id team-id})
|
||||
(rx/map (partial members-fetched team-id)))))))
|
||||
|
||||
(defn- invitations-fetched
|
||||
[team-id invitations]
|
||||
(ptk/reify ::invitations-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:teams 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 set-current-team
|
||||
[{:keys [id permissions features] :as team}]
|
||||
(ptk/reify ::set-current-team
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
;; FIXME: redundant operation, only necessary on workspace
|
||||
;; until workspace initialization is refactored
|
||||
(update-in [:teams id] merge team)
|
||||
(assoc :permissions permissions)
|
||||
;; FIXME: this is a redundant operation that only needed by
|
||||
;; workspace; ti will not be needed after workspace
|
||||
;; bootstrap & urls refactor
|
||||
(assoc :current-team-id id)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (features/initialize (or features #{}))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(swap! storage/global assoc ::current-team-id id))))
|
||||
|
||||
(defn- team-initialized
|
||||
[]
|
||||
(ptk/reify ::team-initialized
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)
|
||||
teams (get state :teams)
|
||||
team (get teams team-id)]
|
||||
(rx/of (set-current-team team)
|
||||
(fetch-members))))))
|
||||
|
||||
(defn initialize-team
|
||||
[team-id]
|
||||
(ptk/reify ::initialize-team
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :current-team-id team-id))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [stopper (rx/filter (ptk/type? ::finalize) stream)]
|
||||
(->> (rx/merge
|
||||
(rx/of (fetch-teams))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::teams-fetched))
|
||||
(rx/observe-on :async)
|
||||
(rx/map team-initialized)))
|
||||
(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 :fonts))
|
||||
state)))))
|
||||
|
||||
;; --- ROLES
|
||||
|
||||
(defn update-member-role
|
||||
[{:keys [role member-id] :as params}]
|
||||
(dm/assert! (uuid? member-id))
|
||||
(dm/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 (fetch-members)
|
||||
(fetch-teams)
|
||||
(ptk/data-event ::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 (fetch-members)
|
||||
(fetch-teams)
|
||||
(ptk/data-event ::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-in state [:teams 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-in state [:team-id 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)
|
||||
(ptk/data-event ::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] :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/get-enabled-features state)
|
||||
params {:name name :features features}]
|
||||
(->> (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 state _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
features (features/get-enabled-features state)
|
||||
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-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)
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "leave-team"
|
||||
:reassign-to reassign-to
|
||||
:team-id team-id}))
|
||||
(on-success))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn create-invitations
|
||||
[{:keys [emails role team-id resend?] :as params}]
|
||||
(dm/assert! (keyword? role))
|
||||
(dm/assert! (uuid? team-id))
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid set of emails"
|
||||
(sm/check-set-of-emails! emails))
|
||||
|
||||
(ptk/reify ::create-invitations
|
||||
ev/Event
|
||||
(-data [_]
|
||||
{:role role
|
||||
:team-id team-id
|
||||
:resend resend?})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
params (dissoc params :resend?)]
|
||||
(->> (rp/cmd! :create-team-invitations (with-meta params (meta it)))
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn copy-invitation-link
|
||||
[{:keys [email team-id] :as params}]
|
||||
(dm/assert!
|
||||
"expected a valid email"
|
||||
(sm/check-email! email))
|
||||
|
||||
(dm/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 router :auth-verify-token {} params)))
|
||||
(rx/map (fn [fragment]
|
||||
(assoc cf/public-uri :fragment fragment)))
|
||||
(rx/tap (fn [uri]
|
||||
(wapi/write-to-clipboard (str uri))))
|
||||
(rx/tap on-success)
|
||||
(rx/ignore)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn update-invitation-role
|
||||
[{:keys [email team-id role] :as params}]
|
||||
(dm/assert!
|
||||
"expected a valid email"
|
||||
(sm/check-email! email))
|
||||
|
||||
(dm/assert! (uuid? team-id))
|
||||
(dm/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}]
|
||||
(dm/assert! (sm/check-email! email))
|
||||
(dm/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 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 on-success)
|
||||
(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}]
|
||||
(dm/assert! (contains? valid-mtypes mtype))
|
||||
(dm/assert! (boolean? is-active))
|
||||
(dm/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))))))
|
||||
|
||||
|
||||
|
|
@ -8,19 +8,16 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.websocket :as ws]
|
||||
[app.main.features :as features]
|
||||
[app.main.data.team :as-alias dtm]
|
||||
[app.main.repo :as rp]
|
||||
[app.plugins.register :as register]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.plugins.register :as plugins.register]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :as storage]
|
||||
[beicon.v2.core :as rx]
|
||||
|
@ -40,7 +37,7 @@
|
|||
[:lang {:optional true} :string]
|
||||
[:theme {:optional true} :string]])
|
||||
|
||||
(def check-profile!
|
||||
(def check-profile
|
||||
(sm/check-fn schema:profile))
|
||||
|
||||
;; --- HELPERS
|
||||
|
@ -49,98 +46,31 @@
|
|||
[{:keys [id]}]
|
||||
(and (uuid? id) (not= id uuid/zero)))
|
||||
|
||||
(defn get-current-team-id
|
||||
[profile]
|
||||
(let [team-id (::current-team-id storage/user)]
|
||||
(or team-id (:default-team-id profile))))
|
||||
|
||||
(defn set-current-team!
|
||||
[team-id]
|
||||
(if (nil? team-id)
|
||||
(swap! storage/user dissoc ::current-team-id)
|
||||
(swap! storage/user assoc ::current-team-id team-id)))
|
||||
|
||||
;; --- EVENT: fetch-teams
|
||||
|
||||
(defn teams-fetched
|
||||
[teams]
|
||||
(ptk/reify ::teams-fetched
|
||||
IDeref
|
||||
(-deref [_] teams)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :teams (d/index-by :id teams)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; Check if current team-id is part of available teams
|
||||
;; if not, dissoc it from storage.
|
||||
|
||||
(let [ids (into #{} (map :id) teams)]
|
||||
(when-let [ctid (::current-team-id storage/user)]
|
||||
(when-not (contains? ids ctid)
|
||||
(swap! storage/user dissoc ::current-team-id)))))))
|
||||
|
||||
(defn fetch-teams
|
||||
[]
|
||||
(ptk/reify ::fetch-teams
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-teams)
|
||||
(rx/map teams-fetched)))))
|
||||
|
||||
(defn set-current-team
|
||||
[team]
|
||||
(ptk/reify ::set-current-team
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :team team)
|
||||
(assoc :permissions (:permissions team))
|
||||
(assoc :current-team-id (:id team))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (features/initialize (:features team #{}))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(set-current-team! (:id team)))))
|
||||
|
||||
;; --- EVENT: fetch-profile
|
||||
|
||||
(declare logout)
|
||||
|
||||
(def profile-fetched?
|
||||
(ptk/type? ::profile-fetched))
|
||||
|
||||
(defn profile-fetched
|
||||
(defn initialize-profile
|
||||
"Initialize profile state, only logged-in profile data should be
|
||||
passed to this event"
|
||||
[{:keys [id] :as profile}]
|
||||
(ptk/reify ::profile-fetched
|
||||
(ptk/reify ::initialize-profile
|
||||
IDeref
|
||||
(-deref [_] profile)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(cond-> state
|
||||
(is-authenticated? profile)
|
||||
(-> (assoc :profile-id id)
|
||||
(assoc :profile profile))))
|
||||
(-> state
|
||||
(assoc :profile-id id)
|
||||
(assoc :profile profile)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [profile (:profile state)
|
||||
email (:email profile)
|
||||
previous-profile (:profile storage/user)
|
||||
previous-email (:email previous-profile)]
|
||||
(when profile
|
||||
(swap! storage/user assoc :profile profile)
|
||||
(i18n/set-locale! (:lang profile))
|
||||
(when (not= previous-email email)
|
||||
(set-current-team! nil))
|
||||
(let [profile (:profile state)]
|
||||
(swap! storage/user assoc :profile profile)
|
||||
(i18n/set-locale! (:lang profile))
|
||||
(plugins.register/init)))))
|
||||
|
||||
(register/init))))))
|
||||
(def profile-fetched?
|
||||
(ptk/type? ::profile-fetched))
|
||||
|
||||
(defn- on-fetch-profile-exception
|
||||
[cause]
|
||||
|
@ -160,213 +90,9 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-profile)
|
||||
(rx/map profile-fetched)
|
||||
(rx/map (partial ptk/data-event ::profile-fetched))
|
||||
(rx/catch on-fetch-profile-exception)))))
|
||||
|
||||
;; --- EVENT: login
|
||||
|
||||
(defn- logged-in
|
||||
"This is the main event that is executed once we have logged in
|
||||
profile. The profile can proceed from standard login or from
|
||||
accepting invitation, or third party auth signup or singin."
|
||||
[profile]
|
||||
(letfn [(get-redirect-events []
|
||||
(let [team-id (get-current-team-id profile)
|
||||
welcome-file-id (dm/get-in profile [:props :welcome-file-id])
|
||||
redirect-href (:login-redirect @storage/session)
|
||||
current-href (rt/get-current-href)]
|
||||
|
||||
(cond
|
||||
(some? redirect-href)
|
||||
(binding [storage/*sync* true]
|
||||
(swap! storage/session dissoc :login-redirect)
|
||||
(if (= current-href redirect-href)
|
||||
(rx/of (rt/reload true))
|
||||
(rx/of (rt/nav-raw :href redirect-href))))
|
||||
|
||||
(some? welcome-file-id)
|
||||
(rx/of (rt/nav' :workspace {:project-id (:default-project-id profile)
|
||||
:file-id welcome-file-id})
|
||||
(update-profile-props {:welcome-file-id nil}))
|
||||
|
||||
:else
|
||||
(rx/of (rt/nav' :dashboard-projects {:team-id team-id})))))]
|
||||
|
||||
(ptk/reify ::logged-in
|
||||
ev/Event
|
||||
(-data [_]
|
||||
{::ev/name "signin"
|
||||
::ev/type "identify"
|
||||
:email (:email profile)
|
||||
:auth-backend (:auth-backend profile)
|
||||
:fullname (:fullname profile)
|
||||
:is-muted (:is-muted profile)
|
||||
:default-team-id (:default-team-id profile)
|
||||
:default-project-id (:default-project-id profile)})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when (is-authenticated? profile)
|
||||
(->> (rx/concat
|
||||
(rx/of (profile-fetched profile)
|
||||
(fetch-teams)
|
||||
(ws/initialize))
|
||||
(get-redirect-events))
|
||||
(rx/observe-on :async)))))))
|
||||
|
||||
(declare login-from-register)
|
||||
|
||||
(defn login
|
||||
[{:keys [email password invitation-token] :as data}]
|
||||
(ptk/reify ::login
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [{:keys [on-error on-success]
|
||||
:or {on-error rx/throw
|
||||
on-success identity}} (meta data)
|
||||
|
||||
params {:email email
|
||||
:password password
|
||||
:invitation-token invitation-token}]
|
||||
|
||||
;; NOTE: We can't take the profile value from login because
|
||||
;; there are cases when login is successful but the cookie is
|
||||
;; not set properly (because of possible misconfiguration).
|
||||
;; So, we proceed to make an additional call to fetch the
|
||||
;; profile, and ensure that cookie is set correctly. If
|
||||
;; profile fetch is successful, we mark the user logged in, if
|
||||
;; the returned profile is an NOT authenticated profile, we
|
||||
;; proceed to logout and show an error message.
|
||||
|
||||
(->> (rp/cmd! :login-with-password (d/without-nils params))
|
||||
(rx/merge-map (fn [data]
|
||||
(rx/merge
|
||||
(rx/of (fetch-profile))
|
||||
(->> stream
|
||||
(rx/filter profile-fetched?)
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/filter (complement is-authenticated?))
|
||||
(rx/tap on-error)
|
||||
(rx/map #(ex/raise :type :authentication))
|
||||
(rx/observe-on :async))
|
||||
|
||||
(->> stream
|
||||
(rx/filter profile-fetched?)
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/filter is-authenticated?)
|
||||
(rx/map (fn [profile]
|
||||
(with-meta (merge data profile)
|
||||
{::ev/source "login"})))
|
||||
(rx/tap on-success)
|
||||
(rx/map logged-in)
|
||||
(rx/observe-on :async)))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(def ^:private schema:login-with-ldap
|
||||
[:map {:title "login-with-ldap"}
|
||||
[:email ::sm/email]
|
||||
[:password :string]])
|
||||
|
||||
(defn login-with-ldap
|
||||
[params]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid params"
|
||||
(sm/check schema:login-with-ldap params))
|
||||
|
||||
(ptk/reify ::login-with-ldap
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-error on-success]
|
||||
:or {on-error rx/throw
|
||||
on-success identity}} (meta params)]
|
||||
(->> (rp/cmd! :login-with-ldap params)
|
||||
(rx/tap on-success)
|
||||
(rx/map (fn [profile]
|
||||
(-> profile
|
||||
(with-meta {::ev/source "login-with-ldap"})
|
||||
(logged-in))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn login-from-token
|
||||
"Used mainly as flow continuation after token validation."
|
||||
[{:keys [profile] :as tdata}]
|
||||
(ptk/reify ::login-from-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))
|
||||
;; NOTE: we need this to be asynchronous because the effect
|
||||
;; should be called before proceed with the login process
|
||||
(rx/observe-on :async)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(set-current-team! nil))))
|
||||
|
||||
(defn login-from-register
|
||||
"Event used mainly for mark current session as logged-in in after the
|
||||
user successfully registered using third party auth provider (in this
|
||||
case we dont need to verify the email)."
|
||||
[]
|
||||
(ptk/reify ::login-from-register
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(rx/merge
|
||||
(rx/of (fetch-profile))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::profile-fetched))
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/map (fn [profile]
|
||||
(with-meta profile
|
||||
{::ev/source "register"})))
|
||||
(rx/map logged-in)
|
||||
(rx/observe-on :async))))))
|
||||
|
||||
;; --- EVENT: logout
|
||||
|
||||
(defn logged-out
|
||||
([] (logged-out {}))
|
||||
([_params]
|
||||
(ptk/reify ::logged-out
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(select-keys state [:route :router :session-id :history]))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/merge
|
||||
;; NOTE: We need the `effect` of the current event to be
|
||||
;; executed before the redirect.
|
||||
(->> (rx/of (rt/nav :auth-login))
|
||||
(rx/observe-on :async))
|
||||
(rx/of (ws/finalize))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
|
||||
(swap! storage/user (constantly {}))))))
|
||||
|
||||
(defn logout
|
||||
([] (logout {}))
|
||||
([params]
|
||||
(ptk/reify ::logout
|
||||
ev/Event
|
||||
(-data [_] {})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile-id (:profile-id state)]
|
||||
(->> (rx/interval 500)
|
||||
(rx/take 1)
|
||||
(rx/mapcat (fn [_]
|
||||
(->> (rp/cmd! :logout {:profile-id profile-id})
|
||||
(rx/delay-at-least 300)
|
||||
(rx/catch (constantly (rx/of 1))))))
|
||||
(rx/map #(logged-out params))))))))
|
||||
|
||||
;; --- Update Profile
|
||||
|
||||
(defn persist-profile
|
||||
|
@ -386,7 +112,7 @@
|
|||
[data]
|
||||
(dm/assert!
|
||||
"expected valid profile data"
|
||||
(check-profile! data))
|
||||
(check-profile data))
|
||||
|
||||
(ptk/reify ::update-profile
|
||||
ptk/WatchEvent
|
||||
|
@ -589,6 +315,9 @@
|
|||
|
||||
;; --- EVENT: request-account-deletion
|
||||
|
||||
(def profile-deleted?
|
||||
(ptk/type? ::profile-deleted))
|
||||
|
||||
(defn request-account-deletion
|
||||
[params]
|
||||
(ptk/reify ::request-account-deletion
|
||||
|
@ -599,7 +328,8 @@
|
|||
on-success identity}} (meta params)]
|
||||
(->> (rp/cmd! :delete-profile {})
|
||||
(rx/tap on-success)
|
||||
(rx/map logged-out)
|
||||
(rx/map (fn [_]
|
||||
(ptk/data-event ::profile-deleted params)))
|
||||
(rx/catch on-error)
|
||||
(rx/delay-at-least 300))))))
|
||||
|
||||
|
@ -652,16 +382,6 @@
|
|||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: crete-demo-profile
|
||||
|
||||
(defn create-demo-profile
|
||||
[]
|
||||
(ptk/reify ::create-demo-profile
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :create-demo-profile {})
|
||||
(rx/map login)))))
|
||||
|
||||
;; --- EVENT: fetch-team-webhooks
|
||||
|
||||
(defn access-tokens-fetched
|
||||
|
@ -716,28 +436,3 @@
|
|||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn show-redirect-error
|
||||
"A helper event that interprets the OIDC redirect errors on the URI
|
||||
and shows an appropriate error message using the notification
|
||||
banners."
|
||||
[error]
|
||||
(ptk/reify ::show-redirect-error
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when-let [hint (case error
|
||||
"registration-disabled"
|
||||
(tr "errors.registration-disabled")
|
||||
"profile-blocked"
|
||||
(tr "errors.profile-blocked")
|
||||
"auth-provider-not-allowed"
|
||||
(tr "errors.auth-provider-not-allowed")
|
||||
"email-domain-not-allowed"
|
||||
(tr "errors.email-domain-not-allowed")
|
||||
|
||||
;; We explicitly do not show any error here, it a explicit user operation.
|
||||
"unable-to-auth"
|
||||
nil
|
||||
|
||||
(tr "errors.generic"))]
|
||||
|
||||
(rx/of (ntf/warn hint))))))
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.persistence :as dps]
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.workspace.bool :as dwb]
|
||||
[app.main.data.workspace.collapse :as dwco]
|
||||
|
@ -167,6 +168,7 @@
|
|||
|
||||
(->> (rx/concat
|
||||
;; Initialize notifications
|
||||
;; FIXME: this should not be initialized here looks like
|
||||
(rx/of (dwn/initialize team-id file-id)
|
||||
(dwsl/initialize))
|
||||
|
||||
|
@ -174,11 +176,11 @@
|
|||
;; fully loadad before mark workspace as initialized
|
||||
(rx/merge
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::df/team-fonts-loaded))
|
||||
(rx/filter (ptk/type? ::df/fonts-loaded))
|
||||
(rx/take 1)
|
||||
(rx/ignore))
|
||||
|
||||
(rx/of (df/load-team-fonts team-id))
|
||||
(rx/of (df/fetch-fonts))
|
||||
|
||||
;; FIXME: move to bundle fetch stages
|
||||
|
||||
|
@ -275,20 +277,29 @@
|
|||
;; steps
|
||||
(->> (rx/from wasm/module)
|
||||
(rx/ignore))
|
||||
|
||||
(->> (rp/cmd! :get-team {:id (:team-id project)})
|
||||
(rx/mapcat (fn [team]
|
||||
(let [bundle {:team team
|
||||
:project project
|
||||
:file-id file-id
|
||||
:project-id project-id}]
|
||||
(rx/of (du/set-current-team team)
|
||||
;; FIXME: this should not be handled here, pending
|
||||
;; refactor of urls and team initialization
|
||||
;; normalization
|
||||
(rx/of (dtm/set-current-team team)
|
||||
(ptk/data-event ::bundle-stage-1 bundle)))))))))
|
||||
(rx/take-until
|
||||
(rx/filter (ptk/type? ::fetch-bundle) stream))))))
|
||||
|
||||
(defn- fetch-bundle-stage-2
|
||||
[{:keys [file-id project-id] :as bundle}]
|
||||
[{:keys [file-id project-id project] :as bundle}]
|
||||
(ptk/reify ::fetch-bundle-stage-2
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :projects assoc project-id project)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [features (features/get-team-enabled-features state)
|
||||
|
@ -419,7 +430,6 @@
|
|||
:workspace-media-objects
|
||||
:workspace-persistence
|
||||
:workspace-presence
|
||||
:workspace-project
|
||||
:workspace-ready?
|
||||
:workspace-undo)
|
||||
(update :workspace-global dissoc :read-only?)
|
||||
|
@ -1148,9 +1158,9 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [project-id (get-in state [:workspace-project :id])
|
||||
file-id (get-in state [:workspace-file :id])
|
||||
page-id (get state :current-page-id)
|
||||
(let [project-id (:current-project-id state)
|
||||
file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
pparams {:file-id file-id :project-id project-id}
|
||||
qparams {:page-id page-id :layout (name layout)}]
|
||||
(rx/of (rt/nav :workspace pparams qparams))))))
|
||||
|
@ -1300,9 +1310,10 @@
|
|||
(let [components-v2 (features/active-feature? state "components/v2")]
|
||||
(if components-v2
|
||||
(rx/of (go-to-main-instance nil component-id))
|
||||
(let [project-id (get-in state [:workspace-project :id])
|
||||
file-id (get-in state [:workspace-file :id])
|
||||
page-id (get state :current-page-id)
|
||||
(let [file-id (:current-file-id state)
|
||||
project-id (:current-project-id state)
|
||||
page-id (:current-page-id state)
|
||||
|
||||
pparams {:file-id file-id :project-id project-id}
|
||||
qparams {:page-id page-id :layout :assets}]
|
||||
(rx/of (rt/nav :workspace pparams qparams)
|
||||
|
@ -1322,9 +1333,9 @@
|
|||
(ptk/reify ::show-component-in-assets
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [project-id (get-in state [:workspace-project :id])
|
||||
file-id (get-in state [:workspace-file :id])
|
||||
page-id (get state :current-page-id)
|
||||
(let [project-id (:current-project-id state)
|
||||
file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
pparams {:file-id file-id :project-id project-id}
|
||||
qparams {:page-id page-id :layout :assets}
|
||||
component-path (cfh/split-path (get-in state [:workspace-data :components component-id :path]))
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
[app.common.exceptions :as ex]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.util.globals :as glob]
|
||||
[app.util.i18n :refer [tr]]
|
||||
|
@ -116,7 +116,7 @@
|
|||
(if show-oops?
|
||||
(st/async-emit! (rt/assign-exception e))
|
||||
(do
|
||||
(st/emit! (du/logout {:capture-redirect true}))
|
||||
(st/emit! (da/logout))
|
||||
(ts/schedule 500 #(st/emit! (ntf/warn msg)))))))
|
||||
|
||||
;; Error that happens on an active business model validation does not
|
||||
|
|
|
@ -31,10 +31,14 @@
|
|||
(l/derived :profile st/state))
|
||||
|
||||
(def team
|
||||
(l/derived :team st/state))
|
||||
(l/derived (fn [state]
|
||||
(let [team-id (:current-team-id state)
|
||||
teams (:teams state)]
|
||||
(get teams team-id)))
|
||||
st/state))
|
||||
|
||||
(def permissions
|
||||
(l/derived :permissions st/state))
|
||||
(l/derived :permissions team))
|
||||
|
||||
(def teams
|
||||
(l/derived :teams st/state))
|
||||
|
@ -54,68 +58,37 @@
|
|||
(def persistence
|
||||
(l/derived :persistence st/state))
|
||||
|
||||
;; ---- Dashboard refs
|
||||
(def projects
|
||||
(l/derived :projects st/state))
|
||||
|
||||
(def dashboard-local
|
||||
(l/derived :dashboard-local st/state))
|
||||
(def files
|
||||
(l/derived :files st/state))
|
||||
|
||||
(def dashboard-fonts
|
||||
(l/derived :dashboard-fonts st/state))
|
||||
(def shared-files
|
||||
(l/derived :shared-files st/state))
|
||||
|
||||
(def dashboard-projects
|
||||
(l/derived :dashboard-projects st/state))
|
||||
|
||||
(def dashboard-files
|
||||
(l/derived :dashboard-files st/state))
|
||||
|
||||
(def dashboard-shared-files
|
||||
(l/derived :dashboard-shared-files st/state))
|
||||
|
||||
(def dashboard-search-result
|
||||
(l/derived :dashboard-search-result st/state))
|
||||
|
||||
(def dashboard-team-stats
|
||||
(l/derived :dashboard-team-stats st/state))
|
||||
|
||||
(def dashboard-team-members
|
||||
(l/derived :dashboard-team-members st/state))
|
||||
|
||||
(def dashboard-team-invitations
|
||||
(l/derived :dashboard-team-invitations st/state))
|
||||
|
||||
(def dashboard-team-webhooks
|
||||
(l/derived :dashboard-team-webhooks st/state))
|
||||
|
||||
(def dashboard-selected-project
|
||||
(l/derived (fn [state]
|
||||
(dm/get-in state [:dashboard-local :selected-project]))
|
||||
st/state))
|
||||
|
||||
(defn- dashboard-extract-selected
|
||||
(defn extract-selected-files
|
||||
[files selected]
|
||||
(let [get-file #(get files %)
|
||||
sim-file #(select-keys % [:id :name :project-id :is-shared])
|
||||
xform (comp (keep get-file)
|
||||
(map sim-file))]
|
||||
(->> (into #{} xform selected)
|
||||
(->> (sequence xform selected)
|
||||
(d/index-by :id))))
|
||||
|
||||
(def dashboard-selected-search
|
||||
(def selected-files
|
||||
(l/derived (fn [state]
|
||||
;; we need to this because :dashboard-search-result is a list
|
||||
;; of maps and we need a map of maps (using :id as key).
|
||||
(let [files (d/index-by :id (:dashboard-search-result state))]
|
||||
(->> (dm/get-in state [:dashboard-local :selected-files])
|
||||
(dashboard-extract-selected files))))
|
||||
(let [selected (get state :selected-files)
|
||||
files (get state :files)]
|
||||
(extract-selected-files files selected)))
|
||||
st/state))
|
||||
|
||||
(def dashboard-selected-files
|
||||
(l/derived (fn [state]
|
||||
(->> (dm/get-in state [:dashboard-local :selected-files])
|
||||
(dashboard-extract-selected (:dashboard-files state))))
|
||||
st/state))
|
||||
(def selected-project
|
||||
(l/derived :selected-project st/state))
|
||||
|
||||
;; ---- Workspace refs
|
||||
|
||||
(def dashboard-local
|
||||
(l/derived :dashboard-local st/state))
|
||||
|
||||
(def render-state
|
||||
(l/derived :render-state st/state))
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
(ns app.main.ui
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.debug.icons-preview :refer [icons-preview]]
|
||||
[app.main.ui.error-boundary :refer [error-boundary*]]
|
||||
|
@ -34,7 +36,7 @@
|
|||
(mf/lazy-component app.main.ui.viewer/viewer))
|
||||
|
||||
(def dashboard-page
|
||||
(mf/lazy-component app.main.ui.dashboard/dashboard))
|
||||
(mf/lazy-component app.main.ui.dashboard/dashboard*))
|
||||
|
||||
(def settings-page
|
||||
(mf/lazy-component app.main.ui.settings/settings))
|
||||
|
@ -42,12 +44,33 @@
|
|||
(def workspace-page
|
||||
(mf/lazy-component app.main.ui.workspace/workspace))
|
||||
|
||||
(mf/defc main-page
|
||||
(mf/defc team-container*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [team-id children]}]
|
||||
|
||||
(mf/with-effect [team-id]
|
||||
(st/emit! (dtm/initialize-team team-id))
|
||||
(fn []
|
||||
(st/emit! (dtm/finalize-team team-id))))
|
||||
|
||||
(let [team (mf/deref refs/team)]
|
||||
(when (= team-id (:id team))
|
||||
[:& (mf/provider ctx/current-team-id) {:value team-id}
|
||||
[:& (mf/provider ctx/permissions) {:value (:permissions team)}
|
||||
;; The `:key` is mandatory here because we want to reinitialize
|
||||
;; all dom tree instead of simple rerender.
|
||||
[:* {:key (str team-id)} children]]])))
|
||||
|
||||
(mf/defc page-container*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [route profile]}]
|
||||
(let [{:keys [data params]} route
|
||||
props (get profile :props)
|
||||
props (get profile :props)
|
||||
route-name (get data :name)
|
||||
|
||||
|
||||
show-question-modal?
|
||||
(and (contains? cf/flags :onboarding)
|
||||
(not (:onboarding-viewed props))
|
||||
|
@ -72,7 +95,7 @@
|
|||
(not= "0.0" (:main cf/version)))]
|
||||
|
||||
[:& (mf/provider ctx/current-route) {:value route}
|
||||
(case (:name data)
|
||||
(case route-name
|
||||
(:auth-login
|
||||
:auth-register
|
||||
:auth-register-validate
|
||||
|
@ -105,26 +128,66 @@
|
|||
:dashboard-team-invitations
|
||||
:dashboard-team-webhooks
|
||||
:dashboard-team-settings)
|
||||
[:?
|
||||
#_[:& app.main.ui.releases/release-notes-modal {:version "2.3"}]
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
||||
(let [team-id (some-> params :path :team-id uuid)
|
||||
project-id (some-> params :path :project-id uuid)
|
||||
search-term (some-> params :query :search-term)
|
||||
plugin-url (some-> params :query :plugin)]
|
||||
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
[:?
|
||||
#_[:& app.main.ui.releases/release-notes-modal {:version "2.3"}]
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
||||
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal {:go-to-team? true}]
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal {:go-to-team? true}]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}])
|
||||
|
||||
[:> team-container* {:team-id team-id}
|
||||
[:> dashboard-page {:profile profile
|
||||
:route-name route-name
|
||||
:team-id team-id
|
||||
:search-term search-term
|
||||
:plugin-url plugin-url
|
||||
:project-id project-id}]]])
|
||||
|
||||
:workspace
|
||||
(let [project-id (some-> params :path :project-id uuid)
|
||||
file-id (some-> params :path :file-id uuid)
|
||||
page-id (some-> params :query :page-id uuid)
|
||||
layout (some-> params :query :layout keyword)]
|
||||
[:? {}
|
||||
(when (cf/external-feature-flag "onboarding-03" "test")
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal {:go-to-team? false}]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}]))
|
||||
|
||||
[:*
|
||||
[:& workspace-page {:project-id project-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:layout-name layout
|
||||
:key file-id}]]])
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}])
|
||||
|
||||
[:& dashboard-page {:route route :profile profile}]]
|
||||
:viewer
|
||||
(let [{:keys [query-params path-params]} route
|
||||
{:keys [index share-id section page-id interactions-mode frame-id share]
|
||||
|
@ -151,37 +214,12 @@
|
|||
:frame-id frame-id
|
||||
:share share}])])
|
||||
|
||||
:workspace
|
||||
(let [project-id (some-> params :path :project-id uuid)
|
||||
file-id (some-> params :path :file-id uuid)
|
||||
page-id (some-> params :query :page-id uuid)
|
||||
layout (some-> params :query :layout keyword)]
|
||||
[:? {}
|
||||
(when (cf/external-feature-flag "onboarding-03" "test")
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-newsletter-modal?
|
||||
[:& onboarding-newsletter]
|
||||
|
||||
show-team-modal?
|
||||
[:& onboarding-team-modal {:go-to-team? false}]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}]))
|
||||
|
||||
[:& workspace-page {:project-id project-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:layout-name layout
|
||||
:key file-id}]])
|
||||
|
||||
:frame-preview
|
||||
[:& frame-preview/frame-preview]
|
||||
|
||||
nil)]))
|
||||
|
||||
|
||||
(mf/defc app
|
||||
[]
|
||||
(let [route (mf/deref refs/route)
|
||||
|
@ -199,4 +237,4 @@
|
|||
[:> error-boundary* {:fallback static/internal-error*}
|
||||
[:& notifications/current-notification]
|
||||
(when route
|
||||
[:& main-page {:route route :profile profile}])])]]))
|
||||
[:> page-container* {:route route :profile profile}])])]]))
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth.login :refer [login-page]]
|
||||
[app.main.ui.auth.recovery :refer [recovery-page]]
|
||||
|
@ -19,7 +19,6 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
||||
(mf/defc auth
|
||||
{::mf/props :obj}
|
||||
[{:keys [route]}]
|
||||
|
@ -35,7 +34,7 @@
|
|||
|
||||
(mf/with-effect [error]
|
||||
(when error
|
||||
(st/emit! (du/show-redirect-error error))))
|
||||
(st/emit! (da/show-redirect-error error))))
|
||||
|
||||
[:main {:class (stl/css :auth-section)}
|
||||
(when show-login-icon
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
[app.common.logging :as log]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.button-link :as bl]
|
||||
|
@ -43,7 +43,7 @@
|
|||
|
||||
(defn create-demo-profile
|
||||
[]
|
||||
(st/emit! (du/create-demo-profile)))
|
||||
(st/emit! (da/create-demo-profile)))
|
||||
|
||||
(defn- store-login-redirect
|
||||
[save-login-redirect]
|
||||
|
@ -140,7 +140,7 @@
|
|||
(let [params (with-meta (:clean-data @form)
|
||||
{:on-error on-error
|
||||
:on-success on-success})]
|
||||
(st/emit! (du/login params)))))
|
||||
(st/emit! (da/login params)))))
|
||||
|
||||
on-submit-ldap
|
||||
(mf/use-callback
|
||||
|
@ -154,7 +154,7 @@
|
|||
params (with-meta params
|
||||
{:on-error on-error
|
||||
:on-success on-success})]
|
||||
(st/emit! (du/login-with-ldap params)))))
|
||||
(st/emit! (da/login-with-ldap params)))))
|
||||
|
||||
default-recovery-req
|
||||
(mf/use-fn
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth.login :as login]
|
||||
|
@ -194,7 +194,7 @@
|
|||
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
|
||||
|
||||
(:is-active params)
|
||||
(st/emit! (du/login-from-register))
|
||||
(st/emit! (da/login-from-register))
|
||||
|
||||
:else
|
||||
(do
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.auth.verify-token
|
||||
(:require
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.repo :as rp]
|
||||
|
@ -25,7 +26,7 @@
|
|||
[data]
|
||||
(let [msg (tr "dashboard.notifications.email-verified-successfully")]
|
||||
(ts/schedule 1000 #(st/emit! (ntf/success msg)))
|
||||
(st/emit! (du/login-from-token data))))
|
||||
(st/emit! (da/login-from-token data))))
|
||||
|
||||
(defmethod handle-token :change-email
|
||||
[_data]
|
||||
|
@ -36,7 +37,7 @@
|
|||
|
||||
(defmethod handle-token :auth
|
||||
[tdata]
|
||||
(st/emit! (du/login-from-token tdata)))
|
||||
(st/emit! (da/login-from-token tdata)))
|
||||
|
||||
(defmethod handle-token :team-invitation
|
||||
[tdata]
|
||||
|
|
|
@ -33,4 +33,4 @@
|
|||
(def is-component? (mf/create-context false))
|
||||
(def sidebar (mf/create-context nil))
|
||||
|
||||
(def team-permissions (mf/create-context nil))
|
||||
(def permissions (mf/create-context nil))
|
||||
|
|
|
@ -7,9 +7,7 @@
|
|||
(ns app.main.ui.dashboard
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.dashboard.shortcuts :as sc]
|
||||
|
@ -20,15 +18,15 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.dashboard.files :refer [files-section]]
|
||||
[app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]]
|
||||
[app.main.ui.dashboard.files :refer [files-section*]]
|
||||
[app.main.ui.dashboard.fonts :refer [fonts-page* font-providers-page*]]
|
||||
[app.main.ui.dashboard.import]
|
||||
[app.main.ui.dashboard.libraries :refer [libraries-page]]
|
||||
[app.main.ui.dashboard.projects :refer [projects-section]]
|
||||
[app.main.ui.dashboard.search :refer [search-page]]
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar]]
|
||||
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page team-webhooks-page]]
|
||||
[app.main.ui.dashboard.templates :refer [templates-section]]
|
||||
[app.main.ui.dashboard.libraries :refer [libraries-page*]]
|
||||
[app.main.ui.dashboard.projects :refer [projects-section*]]
|
||||
[app.main.ui.dashboard.search :refer [search-page*]]
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar*]]
|
||||
[app.main.ui.dashboard.team :refer [team-settings-page* team-members-page* team-invitations-page* webhooks-page*]]
|
||||
[app.main.ui.dashboard.templates :refer [templates-section*]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.workspace.plugins]
|
||||
[app.plugins.register :as preg]
|
||||
|
@ -42,27 +40,13 @@
|
|||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn ^boolean uuid-str?
|
||||
[s]
|
||||
(and (string? s)
|
||||
(boolean (re-seq us/uuid-rx s))))
|
||||
|
||||
(defn- parse-params
|
||||
[route]
|
||||
(let [search-term (get-in route [:params :query :search-term])
|
||||
team-id (get-in route [:params :path :team-id])
|
||||
project-id (get-in route [:params :path :project-id])]
|
||||
(cond-> {:search-term search-term}
|
||||
(uuid-str? team-id)
|
||||
(assoc :team-id (uuid team-id))
|
||||
|
||||
(uuid-str? project-id)
|
||||
(assoc :project-id (uuid project-id)))))
|
||||
|
||||
(mf/defc dashboard-content
|
||||
[{:keys [team projects project section search-term profile] :as props}]
|
||||
(mf/defc dashboard-content*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [team projects project section search-term profile default-project]}]
|
||||
(let [container (mf/use-ref)
|
||||
content-width (mf/use-state 0)
|
||||
|
||||
project-id (:id project)
|
||||
team-id (:id team)
|
||||
|
||||
|
@ -72,10 +56,7 @@
|
|||
file-menu-open? (:menu-open dashboard-local)
|
||||
|
||||
default-project-id
|
||||
(mf/with-memo [projects]
|
||||
(->> (vals projects)
|
||||
(d/seek :is-default)
|
||||
(:id)))
|
||||
(get default-project :id)
|
||||
|
||||
on-resize
|
||||
(mf/use-fn
|
||||
|
@ -88,7 +69,7 @@
|
|||
(mf/use-fn
|
||||
#(st/emit! (dd/clear-selected-files)))
|
||||
|
||||
show-templates
|
||||
show-templates?
|
||||
(and (contains? cf/flags :dashboard-templates-section)
|
||||
(:can-edit permissions))]
|
||||
|
||||
|
@ -105,59 +86,62 @@
|
|||
(case section
|
||||
:dashboard-projects
|
||||
[:*
|
||||
[:& projects-section
|
||||
[:> projects-section*
|
||||
{:team team
|
||||
:projects projects
|
||||
:profile profile
|
||||
:default-project-id default-project-id}]
|
||||
:profile profile}]
|
||||
|
||||
(when show-templates
|
||||
[:& templates-section {:profile profile
|
||||
:project-id project-id
|
||||
:team-id team-id
|
||||
:default-project-id default-project-id
|
||||
:content-width @content-width}])]
|
||||
(when ^boolean show-templates?
|
||||
[:> templates-section*
|
||||
{:profile profile
|
||||
:project-id project-id
|
||||
:team-id team-id
|
||||
:default-project-id default-project-id
|
||||
:content-width @content-width}])]
|
||||
|
||||
:dashboard-fonts
|
||||
[:& fonts-page {:team team}]
|
||||
[:> fonts-page* {:team team}]
|
||||
|
||||
:dashboard-font-providers
|
||||
[:& font-providers-page {:team team}]
|
||||
[:> font-providers-page* {:team team}]
|
||||
|
||||
:dashboard-files
|
||||
(when project
|
||||
[:*
|
||||
[:& files-section {:team team :project project}]
|
||||
(when show-templates
|
||||
[:& templates-section {:profile profile
|
||||
:team-id team-id
|
||||
:project-id project-id
|
||||
:default-project-id default-project-id
|
||||
:content-width @content-width}])])
|
||||
[:> files-section* {:team team
|
||||
:project project}]
|
||||
(when ^boolean show-templates?
|
||||
[:> templates-section*
|
||||
{:profile profile
|
||||
:team-id team-id
|
||||
:project-id project-id
|
||||
:default-project-id default-project-id
|
||||
:content-width @content-width}])])
|
||||
|
||||
:dashboard-search
|
||||
[:& search-page {:team team
|
||||
:search-term search-term}]
|
||||
[:> search-page* {:team team
|
||||
:search-term search-term}]
|
||||
|
||||
:dashboard-libraries
|
||||
[:& libraries-page {:team team}]
|
||||
[:> libraries-page* {:team team
|
||||
:default-project default-project}]
|
||||
|
||||
:dashboard-team-members
|
||||
[:& team-members-page {:team team :profile profile}]
|
||||
[:> team-members-page* {:team team :profile profile}]
|
||||
|
||||
:dashboard-team-invitations
|
||||
[:& team-invitations-page {:team team}]
|
||||
[:> team-invitations-page* {:team team}]
|
||||
|
||||
:dashboard-team-webhooks
|
||||
[:& team-webhooks-page {:team team}]
|
||||
[:> webhooks-page* {:team team}]
|
||||
|
||||
:dashboard-team-settings
|
||||
[:& team-settings-page {:team team :profile profile}]
|
||||
[:> team-settings-page* {:team team :profile profile}]
|
||||
|
||||
nil)]))
|
||||
|
||||
(def ref:dashboard-initialized
|
||||
(l/derived :current-team-initialized st/state))
|
||||
(l/derived :team-initialized st/state))
|
||||
|
||||
(defn use-plugin-register
|
||||
[plugin-url team-id project-id]
|
||||
|
@ -218,33 +202,29 @@
|
|||
(fn [_]
|
||||
(st/emit! (notif/error "The plugin URL is incorrect")))))))))
|
||||
|
||||
(mf/defc dashboard
|
||||
(mf/defc dashboard*
|
||||
{::mf/props :obj}
|
||||
[{:keys [route profile]}]
|
||||
(let [section (get-in route [:data :name])
|
||||
params (parse-params route)
|
||||
[{:keys [profile project-id team-id search-term plugin-url route-name]}]
|
||||
(let [team (mf/deref refs/team)
|
||||
projects (mf/deref refs/projects)
|
||||
|
||||
project-id (:project-id params)
|
||||
project (get projects project-id)
|
||||
projects (mf/with-memo [projects team-id]
|
||||
(->> (vals projects)
|
||||
(filterv #(= team-id (:team-id %)))))
|
||||
|
||||
team-id (:team-id params)
|
||||
search-term (:search-term params)
|
||||
|
||||
plugin-url (-> route :query-params :plugin)
|
||||
|
||||
team (mf/deref refs/team)
|
||||
projects (mf/deref refs/dashboard-projects)
|
||||
project (get projects project-id)
|
||||
|
||||
default-project (->> projects vals (d/seek :is-default))
|
||||
|
||||
initialized? (mf/deref ref:dashboard-initialized)]
|
||||
default-project
|
||||
(mf/with-memo [projects]
|
||||
(->> projects
|
||||
(filter :is-default)
|
||||
(first)))]
|
||||
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts)
|
||||
|
||||
(mf/with-effect [team-id]
|
||||
(st/emit! (dd/initialize {:id team-id}))
|
||||
(mf/with-effect []
|
||||
(st/emit! (dd/initialize))
|
||||
(fn []
|
||||
(st/emit! (dd/finalize {:id team-id}))))
|
||||
(st/emit! (dd/finalize))))
|
||||
|
||||
(mf/with-effect []
|
||||
(let [key (events/listen goog/global "keydown"
|
||||
|
@ -257,31 +237,30 @@
|
|||
|
||||
(use-plugin-register plugin-url team-id (:id default-project))
|
||||
|
||||
[:& (mf/provider ctx/current-team-id) {:value team-id}
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
[:& (mf/provider ctx/team-permissions) {:value (:permissions team)}
|
||||
;; NOTE: dashboard events and other related functions assumes
|
||||
;; that the team is a implicit context variable that is
|
||||
;; available using react context or accessing
|
||||
;; the :current-team-id on the state. We set the key to the
|
||||
;; team-id because we want to completely refresh all the
|
||||
;; components on team change. Many components assumes that the
|
||||
;; team is already set so don't put the team into mf/deps.
|
||||
(when (and team initialized?)
|
||||
[:main {:class (stl/css :dashboard)
|
||||
:key (:id team)}
|
||||
[:& sidebar
|
||||
{:team team
|
||||
:projects projects
|
||||
:project project
|
||||
:profile profile
|
||||
:section section
|
||||
:search-term search-term}]
|
||||
(when (and team profile (seq projects))
|
||||
[:& dashboard-content
|
||||
{:projects projects
|
||||
:profile profile
|
||||
:project project
|
||||
:section section
|
||||
:search-term search-term
|
||||
:team team}])])]]]))
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
;; NOTE: dashboard events and other related functions assumes
|
||||
;; that the team is a implicit context variable that is
|
||||
;; available using react context or accessing
|
||||
;; the :current-team-id on the state. We set the key to the
|
||||
;; team-id because we want to completely refresh all the
|
||||
;; components on team change. Many components assumes that the
|
||||
;; team is already set so don't put the team into mf/deps.
|
||||
[:main {:class (stl/css :dashboard)
|
||||
:key (dm/str (:id team))}
|
||||
[:> sidebar*
|
||||
{:team team
|
||||
:projects projects
|
||||
:project project
|
||||
:default-project default-project
|
||||
:profile profile
|
||||
:section route-name
|
||||
:search-term search-term}]
|
||||
(when (seq projects)
|
||||
[:> dashboard-content*
|
||||
{:projects projects
|
||||
:profile profile
|
||||
:project project
|
||||
:default-project default-project
|
||||
:section route-name
|
||||
:search-term search-term
|
||||
:team team}])]]))
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
|
@ -25,8 +24,7 @@
|
|||
::mf/register-as :leave-and-reassign}
|
||||
[{:keys [profile team accept]}]
|
||||
(let [form (fm/use-form :schema schema:leave-modal-form :initial {})
|
||||
members-map (mf/deref refs/dashboard-team-members)
|
||||
members (vals members-map)
|
||||
members (get team :members)
|
||||
|
||||
options
|
||||
(into [{:value ""
|
||||
|
|
|
@ -134,7 +134,7 @@
|
|||
(st/emit! (ntf/success (tr "dashboard.success-move-files")))
|
||||
(st/emit! (ntf/success (tr "dashboard.success-move-file"))))
|
||||
(if (or navigate (not= team-id current-team-id))
|
||||
(st/emit! (dd/go-to-files team-id project-id))
|
||||
(st/emit! (dd/go-to-files project-id team-id))
|
||||
(st/emit! (dd/fetch-recent-files)
|
||||
(dd/clear-selected-files))))
|
||||
|
||||
|
|
|
@ -126,25 +126,31 @@
|
|||
:on-menu-close on-menu-close
|
||||
:on-import on-import}])]]))
|
||||
|
||||
(mf/defc files-section
|
||||
(mf/defc files-section*
|
||||
{::mf/props :obj}
|
||||
[{:keys [project team]}]
|
||||
(let [files-map (mf/deref refs/dashboard-files)
|
||||
can-edit? (-> team :permissions :can-edit)
|
||||
project-id (:id project)
|
||||
is-draft-proyect (:is-default project)
|
||||
(let [files (mf/deref refs/files)
|
||||
project-id (get project :id)
|
||||
|
||||
[rowref limit] (hooks/use-dynamic-grid-item-width)
|
||||
files (mf/with-memo [project-id files]
|
||||
(->> (vals files)
|
||||
(filter #(= project-id (:project-id %)))
|
||||
(sort-by :modified-at)
|
||||
(reverse)))
|
||||
|
||||
files (mf/with-memo [project-id files-map]
|
||||
(->> (vals files-map)
|
||||
(filter #(= project-id (:project-id %)))
|
||||
(sort-by :modified-at)
|
||||
(reverse)))
|
||||
file-count (or (count files) 0)
|
||||
|
||||
can-edit? (-> team :permissions :can-edit)
|
||||
project-id (:id project)
|
||||
is-draft-proyect (:is-default project)
|
||||
|
||||
[rowref limit] (hooks/use-dynamic-grid-item-width)
|
||||
|
||||
file-count (or (count files) 0)
|
||||
empty-state-viewer (and (not can-edit?)
|
||||
(= 0 file-count))
|
||||
|
||||
selected-files (mf/deref refs/selected-files)
|
||||
|
||||
on-file-created
|
||||
(mf/use-fn
|
||||
(fn [data]
|
||||
|
@ -191,6 +197,7 @@
|
|||
(tr "dashboard.empty-placeholder-files-subtitle"))}]
|
||||
[:& grid {:project project
|
||||
:files files
|
||||
:selected-files selected-files
|
||||
:can-edit can-edit?
|
||||
:origin :files
|
||||
:create-fn create-file
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
[app.common.media :as cm]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
|
||||
|
@ -24,6 +23,7 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- use-page-title
|
||||
|
@ -42,7 +42,7 @@
|
|||
(and (contains? font :font-family-tmp)
|
||||
(str/blank? (:font-family-tmp font))))
|
||||
|
||||
(mf/defc header
|
||||
(mf/defc header*
|
||||
{::mf/props :obj
|
||||
::mf/memo true
|
||||
::mf/private true}
|
||||
|
@ -52,7 +52,7 @@
|
|||
[:div#dashboard-fonts-title {:class (stl/css :dashboard-title)}
|
||||
[:h1 (tr "labels.fonts")]]])
|
||||
|
||||
(mf/defc font-variant-display-name
|
||||
(mf/defc font-variant-display-name*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [variant]}]
|
||||
|
@ -61,10 +61,10 @@
|
|||
(when (not= "normal" (:font-style variant))
|
||||
[:span " " (str/capital (:font-style variant))])])
|
||||
|
||||
(mf/defc uploaded-fonts
|
||||
(mf/defc uploaded-fonts*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [team installed-fonts] :as props}]
|
||||
[{:keys [team installed-fonts]}]
|
||||
(let [fonts* (mf/use-state {})
|
||||
fonts (deref fonts*)
|
||||
font-vals (mf/with-memo [fonts]
|
||||
|
@ -219,7 +219,7 @@
|
|||
:default-value (:font-family item)}]]
|
||||
[:div {:class (stl/css :table-field :variants)}
|
||||
[:span {:class (stl/css :label)}
|
||||
[:& font-variant-display-name {:variant item}]]]
|
||||
[:> font-variant-display-name* {:variant item}]]]
|
||||
|
||||
[:div {:class (stl/css :table-field :filenames)}
|
||||
(for [item (:names item)]
|
||||
|
@ -364,7 +364,7 @@
|
|||
:inhert-variant (not can-edit))
|
||||
:key (dm/str id)}
|
||||
[:span {:class (stl/css :label)}
|
||||
[:& font-variant-display-name {:variant item}]]
|
||||
[:> font-variant-display-name* {:variant item}]]
|
||||
(when can-edit
|
||||
[:span
|
||||
{:class (stl/css :icon :close)
|
||||
|
@ -396,8 +396,9 @@
|
|||
:on-delete on-delete-font
|
||||
:on-edit on-edit}]]))]))
|
||||
|
||||
(mf/defc installed-fonts
|
||||
[{:keys [fonts can-edit] :as props}]
|
||||
(mf/defc installed-fonts*
|
||||
{::mf/props :obj}
|
||||
[{:keys [fonts can-edit]}]
|
||||
(let [sterm (mf/use-state "")
|
||||
|
||||
matches?
|
||||
|
@ -445,26 +446,27 @@
|
|||
:subtitle (tr "dashboard.fonts.empty-placeholder-viewer-sub")
|
||||
:type 2}]))]))
|
||||
|
||||
(def ^:private ref:fonts
|
||||
(l/derived :fonts st/state))
|
||||
|
||||
(mf/defc fonts-page
|
||||
(mf/defc fonts-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team]}]
|
||||
(let [fonts (mf/deref refs/dashboard-fonts)
|
||||
(let [fonts (mf/deref ref:fonts)
|
||||
permissions (:permissions team)
|
||||
can-edit (:can-edit permissions)]
|
||||
[:*
|
||||
[:& header {:team team :section :fonts}]
|
||||
[:> header* {:team team :section :fonts}]
|
||||
[:section {:class (stl/css :dashboard-container :dashboard-fonts)}
|
||||
(when ^boolean can-edit
|
||||
[:& uploaded-fonts {:team team :installed-fonts fonts}])
|
||||
[:& installed-fonts {:team team :fonts fonts :can-edit can-edit}]]]))
|
||||
[:> uploaded-fonts* {:team team :installed-fonts fonts}])
|
||||
[:> installed-fonts*
|
||||
{:team team :fonts fonts :can-edit can-edit}]]]))
|
||||
|
||||
(mf/defc font-providers-page
|
||||
(mf/defc font-providers-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team]}]
|
||||
[:*
|
||||
[:& header {:team team :section :providers}]
|
||||
[:> header* {:team team :section :providers}]
|
||||
[:section {:class (stl/css :dashboard-container)}
|
||||
[:span "font providers"]]])
|
||||
|
||||
|
||||
|
|
|
@ -71,11 +71,19 @@
|
|||
(rx/mapcat thr/render)
|
||||
(rx/mapcat (partial persist-thumbnail file-id revn))))
|
||||
|
||||
(mf/defc grid-item-thumbnail
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [file-id revn thumbnail-id background-color can-edit]}]
|
||||
(let [container (mf/use-ref)
|
||||
visible? (h/use-visible container :once? true)]
|
||||
(mf/defc grid-item-thumbnail*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [can-edit file]}]
|
||||
(let [file-id (get file :id)
|
||||
revn (get file :revn)
|
||||
thumbnail-id (get file :thumbnail-id)
|
||||
|
||||
;; FIXME: revisit maybe bug
|
||||
bg-color (dm/get-in file [:data :options :background])
|
||||
|
||||
container (mf/use-ref)
|
||||
visible? (h/use-visible container :once? true)]
|
||||
|
||||
(mf/with-effect [file-id revn visible? thumbnail-id]
|
||||
(when (and visible? (not thumbnail-id))
|
||||
|
@ -89,7 +97,7 @@
|
|||
:message (ex-message cause)))))))
|
||||
|
||||
[:div {:class (stl/css :grid-item-th)
|
||||
:style {:background-color background-color}
|
||||
:style {:background-color bg-color}
|
||||
:ref container}
|
||||
(when visible?
|
||||
(if thumbnail-id
|
||||
|
@ -108,10 +116,9 @@
|
|||
(def ^:private menu-icon
|
||||
(i/icon-xref :menu (stl/css :menu-icon)))
|
||||
|
||||
(mf/defc grid-item-library
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [file] :as props}]
|
||||
|
||||
(mf/defc grid-item-library*
|
||||
{::mf/props :obj}
|
||||
[{:keys [file]}]
|
||||
(mf/with-effect [file]
|
||||
(when file
|
||||
(let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))]
|
||||
|
@ -231,16 +238,12 @@
|
|||
(dom/set-text! counter-el (str file-count))
|
||||
counter-el))
|
||||
|
||||
(mf/defc grid-item
|
||||
{:wrap [mf/memo]}
|
||||
[{:keys [file origin library-view? can-edit] :as props}]
|
||||
(mf/defc grid-item*
|
||||
{::mf/props :obj}
|
||||
[{:keys [file origin can-edit selected-files]}]
|
||||
(let [file-id (:id file)
|
||||
|
||||
;; FIXME: this breaks react hooks rule, hooks should never to
|
||||
;; be in a conditional code
|
||||
selected-files (if (= origin :search)
|
||||
(mf/deref refs/dashboard-selected-search)
|
||||
(mf/deref refs/dashboard-selected-files))
|
||||
is-library-view (= origin :libraries)
|
||||
|
||||
dashboard-local (mf/deref refs/dashboard-local)
|
||||
file-menu-open? (:menu-open dashboard-local)
|
||||
|
@ -354,9 +357,12 @@
|
|||
(on-select event)) ;; TODO Fix this
|
||||
)))]
|
||||
|
||||
[:li {:class (stl/css-case :grid-item true :project-th true :library library-view?)}
|
||||
[:li {:class (stl/css-case :grid-item true
|
||||
:project-th true
|
||||
:library is-library-view)}
|
||||
[:div
|
||||
{:class (stl/css-case :selected selected? :library library-view?)
|
||||
{:class (stl/css-case :selected selected?
|
||||
:library is-library-view)
|
||||
:ref node-ref
|
||||
:role "button"
|
||||
:title (:name file)
|
||||
|
@ -369,16 +375,11 @@
|
|||
|
||||
[:div {:class (stl/css :overlay)}]
|
||||
|
||||
(if library-view?
|
||||
[:& grid-item-library {:file file}]
|
||||
[:& grid-item-thumbnail
|
||||
{:file-id (:id file)
|
||||
:can-edit can-edit
|
||||
:revn (:revn file)
|
||||
:thumbnail-id (:thumbnail-id file)
|
||||
:background-color (dm/get-in file [:data :options :background])}])
|
||||
(if ^boolean is-library-view
|
||||
[:> grid-item-library* {:file file}]
|
||||
[:> grid-item-thumbnail* {:file file :can-edit can-edit}])
|
||||
|
||||
(when (and (:is-shared file) (not library-view?))
|
||||
(when (and (:is-shared file) (not is-library-view))
|
||||
[:div {:class (stl/css :item-badge)} i/library])
|
||||
|
||||
[:div {:class (stl/css :info-wrapper)}
|
||||
|
@ -417,7 +418,8 @@
|
|||
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
|
||||
|
||||
(mf/defc grid
|
||||
[{:keys [files project origin limit library-view? create-fn can-edit] :as props}]
|
||||
{::mf/props :obj}
|
||||
[{:keys [files project origin limit create-fn can-edit selected-files]}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
project-id (:id project)
|
||||
node-ref (mf/use-var nil)
|
||||
|
@ -484,13 +486,12 @@
|
|||
(when @dragging?
|
||||
[:li {:class (stl/css :grid-item)}])
|
||||
(for [item slice]
|
||||
[:& grid-item
|
||||
[:> grid-item*
|
||||
{:file item
|
||||
:key (:id item)
|
||||
:navigate? true
|
||||
:key (dm/str (:id item))
|
||||
:origin origin
|
||||
:can-edit can-edit
|
||||
:library-view? library-view?}])])
|
||||
:selected-files selected-files
|
||||
:can-edit can-edit}])])
|
||||
|
||||
:else
|
||||
[:& empty-placeholder
|
||||
|
@ -510,13 +511,12 @@
|
|||
[:li {:class (stl/css :grid-item :dragged)}])
|
||||
|
||||
(for [item (take limit files)]
|
||||
[:& grid-item
|
||||
[:> grid-item*
|
||||
{:id (:id item)
|
||||
:file item
|
||||
:selected-files selected-files
|
||||
:can-edit can-edit
|
||||
:key (:id item)
|
||||
:navigate? false}])]))
|
||||
:key (dm/str (:id item))}])]))
|
||||
|
||||
(mf/defc line-grid
|
||||
[{:keys [project team files limit create-fn can-edit] :as props}]
|
||||
|
@ -524,8 +524,8 @@
|
|||
project-id (:id project)
|
||||
team-id (:id team)
|
||||
|
||||
selected-files (mf/deref refs/dashboard-selected-files)
|
||||
selected-project (mf/deref refs/dashboard-selected-project)
|
||||
selected-files (mf/deref refs/selected-files)
|
||||
selected-project (mf/deref refs/selected-project)
|
||||
|
||||
on-finish-import
|
||||
(mf/use-fn
|
||||
|
|
|
@ -7,9 +7,7 @@
|
|||
(ns app.main.ui.dashboard.libraries
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.dashboard.grid :refer [grid]]
|
||||
|
@ -18,35 +16,32 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc libraries-page
|
||||
(mf/defc libraries-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team] :as props}]
|
||||
(let [files-map (mf/deref refs/dashboard-shared-files)
|
||||
projects (mf/deref refs/dashboard-projects)
|
||||
can-edit (-> team :permissions :can-edit)
|
||||
[{:keys [team default-project]}]
|
||||
(let [files
|
||||
(mf/deref refs/shared-files)
|
||||
|
||||
default-project (->> projects vals (d/seek :is-default))
|
||||
files
|
||||
(mf/with-memo [files]
|
||||
(->> (vals files)
|
||||
(sort-by :modified-at)
|
||||
(reverse)))
|
||||
|
||||
files (mf/with-memo [files-map]
|
||||
(if (nil? files-map)
|
||||
nil
|
||||
(->> (vals files-map)
|
||||
(sort-by :modified-at)
|
||||
(reverse))))
|
||||
can-edit
|
||||
(-> team :permissions :can-edit)
|
||||
|
||||
components-v2 (features/use-feature "components/v2")
|
||||
|
||||
[rowref limit] (hooks/use-dynamic-grid-item-width 350)]
|
||||
[rowref limit]
|
||||
(hooks/use-dynamic-grid-item-width 350)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
(when team
|
||||
(let [tname (if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team))]
|
||||
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname)))))
|
||||
(let [tname (if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team))]
|
||||
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))
|
||||
|
||||
(mf/with-effect []
|
||||
(st/emit! (dd/fetch-shared-files (:id team))
|
||||
(mf/with-effect [team]
|
||||
(st/emit! (dd/fetch-shared-files)
|
||||
(dd/clear-selected-files)))
|
||||
|
||||
[:*
|
||||
|
@ -58,6 +53,5 @@
|
|||
:project default-project
|
||||
:origin :libraries
|
||||
:limit limit
|
||||
:can-edit can-edit
|
||||
:library-view? components-v2}]]]))
|
||||
:can-edit can-edit}]]]))
|
||||
|
||||
|
|
|
@ -43,8 +43,10 @@
|
|||
(def ^:private menu-icon
|
||||
(i/icon-xref :menu (stl/css :menu-icon)))
|
||||
|
||||
(mf/defc header
|
||||
{::mf/wrap [mf/memo]}
|
||||
(mf/defc header*
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [can-edit]}]
|
||||
(let [on-click (mf/use-fn #(st/emit! (dd/create-project)))]
|
||||
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
||||
|
@ -96,27 +98,30 @@
|
|||
:aria-label (tr "labels.close")}
|
||||
close-icon]]))
|
||||
|
||||
(def builtin-templates
|
||||
(l/derived :builtin-templates st/state))
|
||||
|
||||
(mf/defc project-item
|
||||
[{:keys [project first? team files can-edit] :as props}]
|
||||
(mf/defc project-item*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [project is-first team files can-edit]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
file-count (or (:count project) 0)
|
||||
|
||||
project-id (:id project)
|
||||
is-draft-proyect (:is-default project)
|
||||
team-id (:id team)
|
||||
empty-state-viewer (and (not can-edit)
|
||||
(= 0 file-count))
|
||||
|
||||
file-count (or (:count project) 0)
|
||||
is-draft? (:is-default project)
|
||||
empty? (and (not can-edit)
|
||||
(= 0 file-count))
|
||||
|
||||
dstate (mf/deref refs/dashboard-local)
|
||||
edit-id (:project-for-edit dstate)
|
||||
|
||||
local (mf/use-state {:menu-open false
|
||||
:menu-pos nil
|
||||
:edition? (= (:id project) edit-id)})
|
||||
:edition (= (:id project) edit-id)})
|
||||
|
||||
[rowref limit]
|
||||
(hooks/use-dynamic-grid-item-width)
|
||||
|
||||
[rowref limit] (hooks/use-dynamic-grid-item-width)
|
||||
on-nav
|
||||
(mf/use-fn
|
||||
(mf/deps project-id team-id)
|
||||
|
@ -152,7 +157,7 @@
|
|||
(mf/use-fn #(swap! local assoc :menu-open false))
|
||||
|
||||
on-edit-open
|
||||
(mf/use-fn #(swap! local assoc :edition? true))
|
||||
(mf/use-fn #(swap! local assoc :edition true))
|
||||
|
||||
on-edit
|
||||
(mf/use-fn
|
||||
|
@ -162,7 +167,7 @@
|
|||
(when-not (str/empty? name)
|
||||
(st/emit! (-> (dd/rename-project (assoc project :name name))
|
||||
(with-meta {::ev/origin "dashboard"}))))
|
||||
(swap! local assoc :edition? false))))
|
||||
(swap! local assoc :edition false))))
|
||||
|
||||
on-file-created
|
||||
(mf/use-fn
|
||||
|
@ -212,10 +217,10 @@
|
|||
(on-menu-click event))))
|
||||
title-width (/ 100 limit)]
|
||||
|
||||
[:article {:class (stl/css-case :dashboard-project-row true :first first?)}
|
||||
[:article {:class (stl/css-case :dashboard-project-row true :first is-first)}
|
||||
[:header {:class (stl/css :project)}
|
||||
[:div {:class (stl/css :project-name-wrapper)}
|
||||
(if (:edition? @local)
|
||||
(if (:edition @local)
|
||||
[:& inline-edition {:content (:name project)
|
||||
:on-end on-edit}]
|
||||
[:h2 {:on-click on-nav
|
||||
|
@ -231,7 +236,6 @@
|
|||
|
||||
[:div {:class (stl/css :info-wrapper)}
|
||||
|
||||
|
||||
;; We group these two spans under a div to avoid having extra space between them.
|
||||
[:div
|
||||
[:span {:class (stl/css :info)} (str (tr "labels.num-of-files" (i18n/c file-count)))]
|
||||
|
@ -274,13 +278,13 @@
|
|||
:on-import on-import}])]]]
|
||||
|
||||
[:div {:class (stl/css :grid-container) :ref rowref}
|
||||
(if empty-state-viewer
|
||||
[:> empty-placeholder* {:title (if is-draft-proyect
|
||||
(if ^boolean empty?
|
||||
[:> empty-placeholder* {:title (if ^boolean is-draft?
|
||||
(tr "dashboard.empty-placeholder-drafts-title")
|
||||
(tr "dashboard.empty-placeholder-files-title"))
|
||||
:class (stl/css :placeholder-placement)
|
||||
:type 1
|
||||
:subtitle (if is-draft-proyect
|
||||
:subtitle (if ^boolean is-draft?
|
||||
(tr "dashboard.empty-placeholder-drafts-subtitle")
|
||||
(tr "dashboard.empty-placeholder-files-subtitle"))}]
|
||||
|
||||
|
@ -303,16 +307,19 @@
|
|||
[:span {:class (stl/css :placeholder-label)} (tr "dashboard.show-all-files")]
|
||||
show-more-icon])]))
|
||||
|
||||
(def ref:recent-files
|
||||
(l/derived :dashboard-recent-files st/state))
|
||||
(def ^:private ref:recent-files
|
||||
(l/derived :recent-files st/state))
|
||||
|
||||
(mf/defc projects-section
|
||||
(mf/defc projects-section*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team projects profile]}]
|
||||
|
||||
(let [projects (->> (vals projects)
|
||||
(sort-by :modified-at)
|
||||
(reverse))
|
||||
(let [projects
|
||||
(mf/with-memo [projects]
|
||||
(->> projects
|
||||
(sort-by :modified-at)
|
||||
(reverse)))
|
||||
|
||||
recent-map (mf/deref ref:recent-files)
|
||||
permisions (:permissions team)
|
||||
|
||||
|
@ -334,7 +341,7 @@
|
|||
::ev/origin "dashboard"}))))]
|
||||
|
||||
(mf/with-effect [show-team-hero?]
|
||||
(swap! storage/global assoc ::show-team-hero show-team-hero?))
|
||||
(swap! storage/global assoc ::show-eam-hero show-team-hero?))
|
||||
|
||||
(mf/with-effect [team]
|
||||
(let [tname (if (:is-default team)
|
||||
|
@ -348,7 +355,7 @@
|
|||
|
||||
(when (seq projects)
|
||||
[:*
|
||||
[:& header {:can-edit can-edit}]
|
||||
[:> header* {:can-edit can-edit}]
|
||||
[:div {:class (stl/css :projects-container)}
|
||||
[:*
|
||||
(when (and show-team-hero?
|
||||
|
@ -368,9 +375,9 @@
|
|||
(->> (vals recent-map)
|
||||
(filterv #(= id (:project-id %)))
|
||||
(sort-by :modified-at #(compare %2 %1))))]
|
||||
[:& project-item {:project project
|
||||
:team team
|
||||
:files files
|
||||
:can-edit can-edit
|
||||
:first? (= project (first projects))
|
||||
:key id}]))]]]])))
|
||||
[:> project-item* {:project project
|
||||
:team team
|
||||
:files files
|
||||
:can-edit can-edit
|
||||
:is-first (= project (first projects))
|
||||
:key id}]))]]]])))
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.ui.dashboard.search
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
|
@ -15,28 +16,43 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc search-page
|
||||
[{:keys [team search-term] :as props}]
|
||||
(let [search-term (or search-term "")
|
||||
result (mf/deref refs/dashboard-search-result)
|
||||
[rowref limit] (hooks/use-dynamic-grid-item-width)]
|
||||
(def ^:private ref:search-result
|
||||
(l/derived :search-result st/state))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(when team
|
||||
(let [tname (if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team))]
|
||||
(dom/set-html-title (tr "title.dashboard.search" tname))))))
|
||||
(def ^:private ref:selected
|
||||
(l/derived (fn [state]
|
||||
;; we need to this because :dashboard-search-result is a list
|
||||
;; of maps and we need a map of maps (using :id as key).
|
||||
(let [files (d/index-by :id (:search-result state))]
|
||||
(->> (get state :selected-files)
|
||||
(refs/extract-selected-files files))))
|
||||
st/state))
|
||||
|
||||
(mf/defc search-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team search-term]}]
|
||||
(let [search-term (d/nilv search-term "")
|
||||
|
||||
result (mf/deref ref:search-result)
|
||||
selected (mf/deref ref:selected)
|
||||
|
||||
[rowref limit]
|
||||
(hooks/use-dynamic-grid-item-width)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
(when team
|
||||
(let [tname (if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team))]
|
||||
(dom/set-html-title (tr "title.dashboard.search" tname)))))
|
||||
|
||||
(mf/with-effect [search-term]
|
||||
(st/emit! (dd/search {:search-term search-term})
|
||||
(dd/clear-selected-files)))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps search-term)
|
||||
(fn []
|
||||
(st/emit! (dd/search {:search-term search-term})
|
||||
(dd/clear-selected-files))))
|
||||
[:*
|
||||
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
||||
[:div#dashboard-search-title {:class (stl/css :dashboard-title)}
|
||||
|
@ -62,6 +78,6 @@
|
|||
|
||||
:else
|
||||
[:& grid {:files result
|
||||
:hide-new? true
|
||||
:selected-files selected
|
||||
:origin :search
|
||||
:limit limit}])]]))
|
||||
|
|
|
@ -7,15 +7,15 @@
|
|||
(ns app.main.ui.dashboard.sidebar
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
|
||||
|
@ -29,7 +29,6 @@
|
|||
[app.util.dom.dnd :as dnd]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.v2.core :as rx]
|
||||
|
@ -80,6 +79,7 @@
|
|||
:dragging? false})
|
||||
|
||||
local @local*
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(mf/deps item)
|
||||
|
@ -348,15 +348,17 @@
|
|||
go-webhooks #(st/emit! (dd/go-to-team-webhooks))
|
||||
go-settings #(st/emit! (dd/go-to-team-settings))
|
||||
|
||||
members-map (mf/deref refs/dashboard-team-members)
|
||||
members (vals members-map)
|
||||
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
|
||||
members (get team :members)
|
||||
permissions (get team :permissions)
|
||||
can-rename? (or (:is-owner permissions)
|
||||
(:is-admin permissions))
|
||||
|
||||
on-success
|
||||
(fn []
|
||||
;; FIXME: this should be handled in the event, not here
|
||||
(st/emit! (dd/go-to-projects (:default-team-id profile))
|
||||
(modal/hide)
|
||||
(du/fetch-teams)))
|
||||
(dtm/fetch-teams)))
|
||||
|
||||
on-error
|
||||
(fn [{:keys [code] :as error}]
|
||||
|
@ -377,15 +379,15 @@
|
|||
(mf/deps on-success on-error)
|
||||
(fn [member-id]
|
||||
(let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))]
|
||||
(st/emit! (dd/leave-team (with-meta params
|
||||
{:on-success on-success
|
||||
:on-error on-error}))))))
|
||||
(st/emit! (dtm/leave-current-team (with-meta params
|
||||
{:on-success on-success
|
||||
:on-error on-error}))))))
|
||||
delete-fn
|
||||
(mf/use-fn
|
||||
(mf/deps team on-success on-error)
|
||||
(fn []
|
||||
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
|
||||
:on-error on-error})))))
|
||||
(st/emit! (dtm/delete-team (with-meta team {:on-success on-success
|
||||
:on-error on-error})))))
|
||||
on-rename-clicked
|
||||
(mf/use-fn
|
||||
(mf/deps team)
|
||||
|
@ -406,7 +408,7 @@
|
|||
(mf/use-fn
|
||||
(mf/deps team profile leave-fn)
|
||||
(fn []
|
||||
(st/emit! (dd/fetch-team-members (:id team))
|
||||
(st/emit! (dtm/fetch-members)
|
||||
(modal/show
|
||||
{:type :leave-and-reassign
|
||||
:profile profile
|
||||
|
@ -590,6 +592,10 @@
|
|||
(when (get-in team [:permissions :is-owner])
|
||||
"teams-options-delete-team")]
|
||||
|
||||
|
||||
;; _ (prn "--------------- sidebar-team-switch")
|
||||
;; _ (app.common.pprint/pprint teams)
|
||||
|
||||
handle-show-team-click
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
|
@ -679,12 +685,12 @@
|
|||
[:& team-options-dropdown {:team team
|
||||
:profile profile}]]]))
|
||||
|
||||
(mf/defc sidebar-content
|
||||
[{:keys [projects profile section team project search-term] :as props}]
|
||||
(mf/defc sidebar-content*
|
||||
{::mf/private true
|
||||
::mf/props :obj}
|
||||
[{:keys [projects profile section team project search-term default-project] :as props}]
|
||||
(let [default-project-id
|
||||
(->> (vals projects)
|
||||
(d/seek :is-default)
|
||||
(:id))
|
||||
(get default-project :id)
|
||||
|
||||
projects? (= section :dashboard-projects)
|
||||
fonts? (= section :dashboard-fonts)
|
||||
|
@ -763,7 +769,7 @@
|
|||
(dom/focus! libs-title)
|
||||
(dom/set-attribute! libs-title "tabindex" "-1")))))))
|
||||
pinned-projects
|
||||
(->> (vals projects)
|
||||
(->> projects
|
||||
(remove :is-default)
|
||||
(filter :is-pinned))]
|
||||
|
||||
|
@ -826,11 +832,12 @@
|
|||
pin-icon
|
||||
[:span {:class (stl/css :empty-text)} (tr "dashboard.no-projects-placeholder")]])]]))
|
||||
|
||||
(mf/defc profile-section
|
||||
[{:keys [profile team] :as props}]
|
||||
(mf/defc profile-section*
|
||||
{::mf/props :obj}
|
||||
[{:keys [profile team]}]
|
||||
(let [show* (mf/use-state false)
|
||||
show (deref show*)
|
||||
photo (cf/resolve-profile-photo-url profile)
|
||||
photo (cf/resolve-profile-photo-url profile)
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
|
@ -875,14 +882,13 @@
|
|||
(when (kbd/enter? event)
|
||||
(reset! show* true))))
|
||||
|
||||
handle-close
|
||||
on-close
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show* false))
|
||||
|
||||
handle-key-down-profile
|
||||
(mf/use-fn
|
||||
(mf/deps on-click)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-click :settings-profile event))))
|
||||
|
@ -910,34 +916,27 @@
|
|||
(show-release-notes))))
|
||||
|
||||
handle-feedback-click
|
||||
(mf/use-fn
|
||||
(mf/deps on-click)
|
||||
#(on-click :settings-feedback %))
|
||||
(mf/use-fn #(on-click :settings-feedback %))
|
||||
|
||||
handle-feedback-keydown
|
||||
(mf/use-fn
|
||||
(mf/deps on-click)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-click :settings-feedback event))))
|
||||
|
||||
handle-logout-click
|
||||
(mf/use-fn
|
||||
(mf/deps on-click)
|
||||
#(on-click (du/logout) %))
|
||||
#(on-click (da/logout) %))
|
||||
|
||||
handle-logout-keydown
|
||||
(mf/use-fn
|
||||
(mf/deps on-click)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-click (du/logout) event))))
|
||||
(on-click (da/logout) event))))
|
||||
|
||||
handle-set-profile
|
||||
(mf/use-fn
|
||||
(mf/deps on-click)
|
||||
(fn [event]
|
||||
(on-click :settings-profile event)))]
|
||||
#(on-click :settings-profile %))]
|
||||
|
||||
[:*
|
||||
(when (and team profile)
|
||||
|
@ -959,7 +958,9 @@
|
|||
:alt (:fullname profile)}]
|
||||
[:span {:class (stl/css :profile-fullname)} (:fullname profile)]]
|
||||
|
||||
[:& dropdown-menu {:on-close handle-close :show show :list-class (stl/css :profile-dropdown)}
|
||||
[:& dropdown-menu {:on-close on-close
|
||||
:show show
|
||||
:list-class (stl/css :profile-dropdown)}
|
||||
[:li {:tab-index (if show "0" "-1")
|
||||
:class (stl/css :profile-dropdown-item)
|
||||
:on-click handle-set-profile
|
||||
|
@ -1045,15 +1046,13 @@
|
|||
:show? show-comments?
|
||||
:on-show-comments handle-show-comments}])]]))
|
||||
|
||||
(mf/defc sidebar
|
||||
{::mf/wrap-props false
|
||||
(mf/defc sidebar*
|
||||
{::mf/props :obj
|
||||
::mf/wrap [mf/memo]}
|
||||
[props]
|
||||
(let [team (obj/get props "team")
|
||||
profile (obj/get props "profile")]
|
||||
[:nav {:class (stl/css :dashboard-sidebar) :data-testid "dashboard-sidebar"}
|
||||
[:> sidebar-content props]
|
||||
[:& profile-section
|
||||
{:profile profile
|
||||
:team team}]]))
|
||||
[{:keys [team profile] :as props}]
|
||||
[:nav {:class (stl/css :dashboard-sidebar) :data-testid "dashboard-sidebar"}
|
||||
[:> sidebar-content* props]
|
||||
[:> profile-section*
|
||||
{:profile profile
|
||||
:team team}]])
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
|
@ -31,6 +31,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private arrow-icon
|
||||
|
@ -139,12 +140,12 @@
|
|||
::mf/register-as :invite-members
|
||||
::mf/props :obj}
|
||||
[{:keys [team origin invite-email]}]
|
||||
(let [members-map (mf/deref refs/dashboard-team-members)
|
||||
perms (:permissions team)
|
||||
(let [members (get team :members)
|
||||
perms (get team :permissions)
|
||||
team-id (get team :id)
|
||||
|
||||
roles (mf/with-memo [perms]
|
||||
(get-available-roles perms))
|
||||
team-id (:id team)
|
||||
|
||||
initial (mf/with-memo [team-id invite-email]
|
||||
(if invite-email
|
||||
|
@ -156,7 +157,7 @@
|
|||
error-text (mf/use-state "")
|
||||
|
||||
current-data-emails (into #{} (dm/get-in @form [:clean-data :emails]))
|
||||
current-members-emails (into #{} (map (comp :email second)) members-map)
|
||||
current-members-emails (into #{} (map :email) members)
|
||||
|
||||
on-success
|
||||
(fn [_form {:keys [total]}]
|
||||
|
@ -164,8 +165,8 @@
|
|||
(st/emit! (ntf/success (tr "notifications.invitation-email-sent"))))
|
||||
|
||||
(st/emit! (modal/hide)
|
||||
(dd/fetch-team-members)
|
||||
(dd/fetch-team-invitations)))
|
||||
(dtm/fetch-members)
|
||||
(dtm/fetch-invitations)))
|
||||
|
||||
on-error
|
||||
(fn [_form cause]
|
||||
|
@ -198,11 +199,11 @@
|
|||
(let [params (:clean-data @form)
|
||||
mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}]
|
||||
(st/emit! (-> (dd/invite-team-members (with-meta params mdata))
|
||||
(st/emit! (-> (dtm/create-invitations (with-meta params mdata))
|
||||
(with-meta {::ev/origin origin}))
|
||||
(dd/fetch-team-invitations)
|
||||
(dd/fetch-team-members (:id team)))))]
|
||||
|
||||
;; FIXME: looks duplicate
|
||||
(dtm/fetch-invitations)
|
||||
(dtm/fetch-members))))]
|
||||
|
||||
[:div {:class (stl/css-case :modal-team-container true
|
||||
:modal-team-container-workspace (= origin :workspace)
|
||||
|
@ -338,7 +339,8 @@
|
|||
(when is-you?
|
||||
[:li {:on-click on-leave
|
||||
:class (stl/css :action-dropdown-item)
|
||||
:key "is-you-option"} (tr "dashboard.leave-team")])
|
||||
:key "is-you-option"}
|
||||
(tr "dashboard.leave-team")])
|
||||
(when (and can-delete? (not is-you?) (not (and is-owner? (not owner?))))
|
||||
[:li {:on-click on-delete
|
||||
:class (stl/css :action-dropdown-item)
|
||||
|
@ -346,18 +348,18 @@
|
|||
|
||||
(defn- set-role! [member-id role]
|
||||
(let [params {:member-id member-id :role role}]
|
||||
(st/emit! (dd/update-team-member-role params))))
|
||||
(st/emit! (dtm/update-member-role params))))
|
||||
|
||||
(mf/defc team-member
|
||||
(mf/defc team-member*
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/props :obj}
|
||||
[{:keys [team member members profile]}]
|
||||
[{:keys [team member total-members profile]}]
|
||||
|
||||
(let [member-id (:id member)
|
||||
(let [member-id (:id member)
|
||||
on-set-admin (mf/use-fn (mf/deps member-id) (partial set-role! member-id :admin))
|
||||
on-set-editor (mf/use-fn (mf/deps member-id) (partial set-role! member-id :editor))
|
||||
on-set-viewer (mf/use-fn (mf/deps member-id) (partial set-role! member-id :viewer))
|
||||
owner? (dm/get-in team [:permissions :is-owner])
|
||||
owner? (dm/get-in team [:permissions :is-owner])
|
||||
|
||||
on-set-owner
|
||||
(mf/use-fn
|
||||
|
@ -373,18 +375,12 @@
|
|||
(st/emit! (modal/show params)))))
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
(mf/deps profile)
|
||||
(fn []
|
||||
(st/emit! (dd/go-to-projects (:default-team-id profile))
|
||||
(modal/hide)
|
||||
(du/fetch-teams))))
|
||||
(mf/use-fn (fn [] (rx/of (dd/go-to-default-team))))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(fn [{:keys [code] :as error}]
|
||||
(condp = code
|
||||
|
||||
:no-enough-members-for-leave
|
||||
(rx/of (ntf/error (tr "errors.team-leave.insufficient-members")))
|
||||
|
||||
|
@ -400,17 +396,17 @@
|
|||
(mf/use-fn
|
||||
(mf/deps team on-success on-error)
|
||||
(fn []
|
||||
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
|
||||
:on-error on-error})))))
|
||||
(st/emit! (dtm/delete-team (with-meta team {:on-success on-success
|
||||
:on-error on-error})))))
|
||||
|
||||
on-leave-accepted
|
||||
(mf/use-fn
|
||||
(mf/deps on-success on-error)
|
||||
(fn [member-id]
|
||||
(let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))]
|
||||
(st/emit! (dd/leave-team (with-meta params
|
||||
{:on-success on-success
|
||||
:on-error on-error}))))))
|
||||
(st/emit! (dtm/leave-current-team (with-meta params
|
||||
{:on-success on-success
|
||||
:on-error on-error}))))))
|
||||
|
||||
on-leave-and-close
|
||||
(mf/use-fn
|
||||
|
@ -428,7 +424,7 @@
|
|||
(mf/use-fn
|
||||
(mf/deps profile team on-leave-accepted)
|
||||
(fn []
|
||||
(st/emit! (dd/fetch-team-members (:id team))
|
||||
(st/emit! (dtm/fetch-members)
|
||||
(modal/show
|
||||
{:type :leave-and-reassign
|
||||
:profile profile
|
||||
|
@ -450,7 +446,7 @@
|
|||
(mf/use-fn
|
||||
(mf/deps member-id)
|
||||
(fn []
|
||||
(let [on-accept #(st/emit! (dd/delete-team-member {:member-id member-id}))
|
||||
(let [on-accept #(st/emit! (dtm/delete-member {:member-id member-id}))
|
||||
params {:type :confirm
|
||||
:title (tr "modals.delete-team-member-confirm.title")
|
||||
:message (tr "modals.delete-team-member-confirm.message")
|
||||
|
@ -459,7 +455,7 @@
|
|||
(st/emit! (modal/show params)))))
|
||||
|
||||
on-leave'
|
||||
(cond (= 1 (count members)) on-leave-and-close
|
||||
(cond (= 1 total-members) on-leave-and-close
|
||||
(= true owner?) on-change-owner-and-leave
|
||||
:else on-leave)]
|
||||
|
||||
|
@ -483,16 +479,26 @@
|
|||
:on-delete on-delete
|
||||
:on-leave on-leave'}]]]))
|
||||
|
||||
(mf/defc team-members
|
||||
{::mf/props :obj}
|
||||
[{:keys [members-map team profile]}]
|
||||
(let [members (mf/with-memo [members-map]
|
||||
(->> (vals members-map)
|
||||
(sort-by :created-at)
|
||||
(remove :is-owner)))
|
||||
owner (mf/with-memo [members-map]
|
||||
(->> (vals members-map)
|
||||
(d/seek :is-owner)))]
|
||||
(mf/defc team-members*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [team profile]}]
|
||||
(let [members (get team :members)
|
||||
|
||||
total-members
|
||||
(count members)
|
||||
|
||||
|
||||
owner
|
||||
(mf/with-memo [members]
|
||||
(d/seek :is-owner members))
|
||||
|
||||
members
|
||||
(mf/with-memo [team]
|
||||
(->> (:members team)
|
||||
(sort-by :created-at)
|
||||
(remove :is-owner)
|
||||
(vec)))]
|
||||
|
||||
[:div {:class (stl/css :dashboard-table :team-members)}
|
||||
[:div {:class (stl/css :table-header)}
|
||||
|
@ -500,42 +506,39 @@
|
|||
[:div {:class (stl/css :table-field :title-field-role)} (tr "labels.role")]]
|
||||
|
||||
[:div {:class (stl/css :table-rows)}
|
||||
[:& team-member
|
||||
[:> team-member*
|
||||
{:member owner
|
||||
:team team
|
||||
:profile profile
|
||||
:members members-map}]
|
||||
:total-members total-members}]
|
||||
|
||||
(for [item members]
|
||||
[:& team-member
|
||||
[:> team-member*
|
||||
{:member item
|
||||
:team team
|
||||
:profile profile
|
||||
:key (:id item)
|
||||
:members members-map}])]]))
|
||||
:key (dm/str (:id item))
|
||||
:total-members total-members}])]]))
|
||||
|
||||
(mf/defc team-members-page
|
||||
(mf/defc team-members-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team profile]}]
|
||||
(let [members-map (mf/deref refs/dashboard-team-members)]
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title
|
||||
(tr "title.team-members"
|
||||
(if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team)))))
|
||||
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title
|
||||
(tr "title.team-members"
|
||||
(if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team)))))
|
||||
(mf/with-effect []
|
||||
(st/emit! (dtm/fetch-members)))
|
||||
|
||||
(mf/with-effect [team]
|
||||
(st/emit! (dd/fetch-team-members (:id team))))
|
||||
|
||||
[:*
|
||||
[:& header {:section :dashboard-team-members :team team}]
|
||||
[:section {:class (stl/css :dashboard-container :dashboard-team-members)}
|
||||
[:& team-members
|
||||
{:profile profile
|
||||
:team team
|
||||
:members-map members-map}]]]))
|
||||
[:*
|
||||
[:& header {:section :dashboard-team-members :team team}]
|
||||
[:section {:class (stl/css :dashboard-container :dashboard-team-members)}
|
||||
[:> team-members*
|
||||
{:profile profile
|
||||
:team team}]]])
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INVITATIONS SECTION
|
||||
|
@ -587,8 +590,9 @@
|
|||
:on-click on-change'}
|
||||
(tr "labels.viewer")]]]]))
|
||||
|
||||
(mf/defc invitation-actions
|
||||
{::mf/props :obj}
|
||||
(mf/defc invitation-actions*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [invitation team-id]}]
|
||||
(let [show? (mf/use-state false)
|
||||
|
||||
|
@ -622,15 +626,15 @@
|
|||
(mf/deps email team-id)
|
||||
(fn []
|
||||
(let [params {:email email :team-id team-id}
|
||||
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
||||
(st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
|
||||
mdata {:on-success #(st/emit! (dtm/fetch-invitations))}]
|
||||
(st/emit! (dtm/delete-invitation (with-meta params mdata))))))
|
||||
|
||||
on-resend-success
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ntf/success (tr "notifications.invitation-email-sent"))
|
||||
(modal/hide)
|
||||
(dd/fetch-team-invitations))))
|
||||
(dtm/fetch-invitations))))
|
||||
|
||||
on-resend
|
||||
(mf/use-fn
|
||||
|
@ -643,7 +647,7 @@
|
|||
{:on-success on-resend-success
|
||||
:on-error on-error})]
|
||||
(st/emit!
|
||||
(-> (dd/invite-team-members params)
|
||||
(-> (dtm/create-invitations params)
|
||||
(with-meta {::ev/origin :team}))))))
|
||||
|
||||
on-copy-success
|
||||
|
@ -660,7 +664,7 @@
|
|||
{:on-success on-copy-success
|
||||
:on-error on-error})]
|
||||
(st/emit!
|
||||
(-> (dd/copy-invitation-link params)
|
||||
(-> (dtm/copy-invitation-link params)
|
||||
(with-meta {::ev/origin :team}))))))
|
||||
|
||||
on-hide (mf/use-fn #(reset! show? false))
|
||||
|
@ -694,17 +698,19 @@
|
|||
role (:role invitation)
|
||||
status (if expired? :expired :pending)
|
||||
type (if expired? :warning :default)
|
||||
badge-content (if (= status :expired)
|
||||
(tr "labels.expired-invitation")
|
||||
(tr "labels.pending-invitation"))
|
||||
|
||||
badge-content
|
||||
(if (= status :expired)
|
||||
(tr "labels.expired-invitation")
|
||||
(tr "labels.pending-invitation"))
|
||||
|
||||
on-change-role
|
||||
(mf/use-fn
|
||||
(mf/deps email team-id)
|
||||
(fn [role _event]
|
||||
(let [params {:email email :team-id team-id :role role}
|
||||
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
||||
(st/emit! (dd/update-team-invitation-role (with-meta params mdata))))))]
|
||||
mdata {:on-success #(st/emit! (dtm/fetch-invitations))}]
|
||||
(st/emit! (dtm/update-invitation-role (with-meta params mdata))))))]
|
||||
|
||||
[:div {:class (stl/css :table-row :table-row-invitations)}
|
||||
[:div {:class (stl/css :table-field :field-email)} email]
|
||||
|
@ -720,25 +726,36 @@
|
|||
[:& badge-notification {:type type :content badge-content}]]
|
||||
|
||||
[:div {:class (stl/css :table-field :field-actions)}
|
||||
(when can-invite
|
||||
[:& invitation-actions
|
||||
(when ^boolean can-invite
|
||||
[:> invitation-actions*
|
||||
{:invitation invitation
|
||||
:team-id team-id}])]]))
|
||||
|
||||
(mf/defc empty-invitation-table
|
||||
[{:keys [can-invite] :as props}]
|
||||
(mf/defc empty-invitation-table*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [can-invite]}]
|
||||
[:div {:class (stl/css :empty-invitations)}
|
||||
[:span (tr "labels.no-invitations")]
|
||||
(when can-invite
|
||||
(when ^boolean can-invite
|
||||
[:> i18n/tr-html* {:content (tr "labels.no-invitations-hint")
|
||||
:tag-name "span"}])])
|
||||
|
||||
(mf/defc invitation-section
|
||||
[{:keys [team invitations] :as props}]
|
||||
(let [owner? (dm/get-in team [:permissions :is-owner])
|
||||
admin? (dm/get-in team [:permissions :is-admin])
|
||||
can-invite (or owner? admin?)
|
||||
team-id (:id team)]
|
||||
(def ^:private ref:invitations
|
||||
(l/derived :invitations st/state))
|
||||
|
||||
(mf/defc invitation-section*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [team]}]
|
||||
(let [permissions (get team :permissions)
|
||||
invitations (mf/deref ref:invitations)
|
||||
|
||||
team-id (get team :id)
|
||||
|
||||
owner? (get permissions :is-owner)
|
||||
admin? (get permissions :is-admin)
|
||||
can-invite? (or owner? admin?)]
|
||||
|
||||
[:div {:class (stl/css :invitations)}
|
||||
[:div {:class (stl/css :table-header)}
|
||||
|
@ -746,38 +763,34 @@
|
|||
[:div {:class (stl/css :title-field-role)} (tr "labels.role")]
|
||||
[:div {:class (stl/css :title-field-status)} (tr "labels.status")]]
|
||||
(if (empty? invitations)
|
||||
[:& empty-invitation-table {:can-invite can-invite}]
|
||||
[:> empty-invitation-table* {:can-invite can-invite?}]
|
||||
[:div {:class (stl/css :table-rows)}
|
||||
(for [invitation invitations]
|
||||
[:> invitation-row*
|
||||
{:key (:email invitation)
|
||||
:invitation invitation
|
||||
:can-invite can-invite
|
||||
:can-invite can-invite?
|
||||
:team-id team-id}])])]))
|
||||
|
||||
(mf/defc team-invitations-page
|
||||
(mf/defc team-invitations-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team]}]
|
||||
(let [invitations (mf/deref refs/dashboard-team-invitations)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title
|
||||
(tr "title.team-invitations"
|
||||
(if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team)))))
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title
|
||||
(tr "title.team-invitations"
|
||||
(if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team)))))
|
||||
|
||||
(mf/with-effect []
|
||||
(st/emit! (dd/fetch-team-invitations)))
|
||||
(mf/with-effect []
|
||||
(st/emit! (dtm/fetch-invitations)))
|
||||
|
||||
[:*
|
||||
[:& header {:section :dashboard-team-invitations
|
||||
:team team}]
|
||||
[:section {:class (stl/css :dashboard-team-invitations)}
|
||||
;; TODO: We should consider adding a "loading state" here
|
||||
;; with an (if (nil? invitations) [:& loading-state] [:& invitations])
|
||||
(when-not (nil? invitations)
|
||||
[:& invitation-section {:team team
|
||||
:invitations invitations}])]]))
|
||||
[:*
|
||||
[:& header {:section :dashboard-team-invitations
|
||||
:team team}]
|
||||
[:section {:class (stl/css :dashboard-team-invitations)}
|
||||
[:> invitation-section* {:team team}]]])
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; WEBHOOKS SECTION
|
||||
|
@ -811,9 +824,8 @@
|
|||
(mf/use-fn
|
||||
(fn [_]
|
||||
(let [message (tr "dashboard.webhooks.create.success")]
|
||||
(st/emit! (dd/fetch-team-webhooks)
|
||||
(ntf/success message)
|
||||
(modal/hide)))))
|
||||
(rx/of (ntf/success message)
|
||||
(modal/hide)))))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
|
@ -846,7 +858,7 @@
|
|||
params {:uri (:uri cdata)
|
||||
:mtype (:mtype cdata)
|
||||
:is-active (:is-active cdata)}]
|
||||
(st/emit! (dd/create-team-webhook
|
||||
(st/emit! (dtm/create-webhook
|
||||
(with-meta params mdata))))))
|
||||
|
||||
on-update-submit
|
||||
|
@ -855,7 +867,7 @@
|
|||
(let [params (:clean-data @form)
|
||||
mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}]
|
||||
(st/emit! (dd/update-team-webhook
|
||||
(st/emit! (dtm/update-webhook
|
||||
(with-meta params mdata))))))
|
||||
|
||||
on-submit
|
||||
|
@ -910,7 +922,7 @@
|
|||
(tr "modals.edit-webhook.submit-label")
|
||||
(tr "modals.create-webhook.submit-label"))}]]]]]]))
|
||||
|
||||
(mf/defc webhooks-hero
|
||||
(mf/defc webhooks-hero*
|
||||
{::mf/props :obj}
|
||||
[]
|
||||
[:div {:class (stl/css :webhooks-hero-container)}
|
||||
|
@ -922,7 +934,7 @@
|
|||
:on-click #(st/emit! (modal/show :webhook {}))}
|
||||
(tr "dashboard.webhooks.create")]])
|
||||
|
||||
(mf/defc webhook-actions
|
||||
(mf/defc webhook-actions*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [on-edit on-delete can-edit]}]
|
||||
|
@ -945,8 +957,10 @@
|
|||
:class (stl/css :menu-disabled)}
|
||||
[:> icon* {:id "menu"}]])))
|
||||
|
||||
(mf/defc webhook-item
|
||||
{::mf/wrap [mf/memo]}
|
||||
(mf/defc webhook-item*
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [webhook permissions]}]
|
||||
(let [error-code (:error-code webhook)
|
||||
id (:id webhook)
|
||||
|
@ -966,8 +980,8 @@
|
|||
(mf/deps id)
|
||||
(fn []
|
||||
(let [params {:id id}
|
||||
mdata {:on-success #(st/emit! (dd/fetch-team-webhooks))}]
|
||||
(st/emit! (dd/delete-team-webhook (with-meta params mdata))))))
|
||||
mdata {:on-success #(st/emit! (dtm/fetch-webhooks))}]
|
||||
(st/emit! (dtm/delete-webhook (with-meta params mdata))))))
|
||||
|
||||
on-delete
|
||||
(mf/use-fn
|
||||
|
@ -1005,22 +1019,29 @@
|
|||
(tr "labels.active")
|
||||
(tr "labels.inactive"))]]
|
||||
[:div {:class (stl/css :table-field :actions)}
|
||||
[:& webhook-actions
|
||||
[:> webhook-actions*
|
||||
{:on-edit on-edit
|
||||
:on-delete on-delete
|
||||
:can-edit can-edit}]]]))
|
||||
|
||||
(mf/defc webhooks-list
|
||||
{::mf/props :obj}
|
||||
(mf/defc webhooks-list*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [webhooks permissions]}]
|
||||
[:div {:class (stl/css :table-rows :webhook-table)}
|
||||
(for [webhook webhooks]
|
||||
[:& webhook-item {:webhook webhook :key (:id webhook) :permissions permissions}])])
|
||||
[:> webhook-item*
|
||||
{:webhook webhook
|
||||
:key (dm/str (:id webhook))
|
||||
:permissions permissions}])])
|
||||
|
||||
(mf/defc team-webhooks-page
|
||||
(def ^:private ref:webhooks
|
||||
(l/derived :webhooks st/state))
|
||||
|
||||
(mf/defc webhooks-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team]}]
|
||||
(let [webhooks (mf/deref refs/dashboard-team-webhooks)]
|
||||
(let [webhooks (mf/deref ref:webhooks)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title
|
||||
|
@ -1030,33 +1051,34 @@
|
|||
(:name team)))))
|
||||
|
||||
(mf/with-effect [team]
|
||||
(st/emit! (dd/fetch-team-webhooks)))
|
||||
(st/emit! (dtm/fetch-webhooks)))
|
||||
|
||||
[:*
|
||||
[:& header {:team team :section :dashboard-team-webhooks}]
|
||||
[:section {:class (stl/css :dashboard-container :dashboard-team-webhooks)}
|
||||
[:*
|
||||
[:& webhooks-hero]
|
||||
[:> webhooks-hero* {}]
|
||||
(if (empty? webhooks)
|
||||
[:div {:class (stl/css :webhooks-empty)}
|
||||
[:div (tr "dashboard.webhooks.empty.no-webhooks")]
|
||||
[:div (tr "dashboard.webhooks.empty.add-one")]]
|
||||
[:& webhooks-list {:webhooks webhooks :permissions (:permissions team)}])]]]))
|
||||
[:> webhooks-list*
|
||||
{:webhooks webhooks
|
||||
:permissions (:permissions team)}])]]]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SETTINGS SECTION
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(mf/defc team-settings-page
|
||||
(mf/defc team-settings-page*
|
||||
{::mf/props :obj}
|
||||
[{:keys [team]}]
|
||||
(let [finput (mf/use-ref)
|
||||
|
||||
members-map (mf/deref refs/dashboard-team-members)
|
||||
owner (->> (vals members-map)
|
||||
(d/seek :is-owner))
|
||||
members (get team :members)
|
||||
stats (get team :stats)
|
||||
|
||||
stats (mf/deref refs/dashboard-team-stats)
|
||||
owner (d/seek :is-owner members)
|
||||
|
||||
permissions (:permissions team)
|
||||
can-edit (or (:is-owner permissions)
|
||||
|
@ -1067,8 +1089,7 @@
|
|||
|
||||
on-file-selected
|
||||
(fn [file]
|
||||
(st/emit! (dd/update-team-photo file)))]
|
||||
|
||||
(st/emit! (dtm/update-team-photo file)))]
|
||||
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title (tr "title.team-settings"
|
||||
|
@ -1076,11 +1097,9 @@
|
|||
(tr "dashboard.your-penpot")
|
||||
(:name team)))))
|
||||
|
||||
|
||||
(mf/with-effect [team]
|
||||
(let [team-id (:id team)]
|
||||
(st/emit! (dd/fetch-team-members team-id)
|
||||
(dd/fetch-team-stats team-id))))
|
||||
(mf/with-effect []
|
||||
(st/emit! (dtm/fetch-members)
|
||||
(dtm/fetch-stats)))
|
||||
|
||||
[:*
|
||||
[:& header {:section :dashboard-team-settings :team team}]
|
||||
|
@ -1116,7 +1135,7 @@
|
|||
[:div {:class (stl/css :block-content)}
|
||||
user-icon
|
||||
[:span {:class (stl/css :block-text)}
|
||||
(tr "dashboard.num-of-members" (count members-map))]]]
|
||||
(tr "dashboard.num-of-members" (count members))]]]
|
||||
|
||||
[:div {:class (stl/css :block)}
|
||||
[:div {:class (stl/css :block-label)}
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
|
@ -51,7 +51,7 @@
|
|||
(let [mdata {:on-success (partial on-create-success form)
|
||||
:on-error (partial on-error form)}
|
||||
params {:name (get-in @form [:clean-data :name])}]
|
||||
(st/emit! (-> (dd/create-team (with-meta params mdata))
|
||||
(st/emit! (-> (dtm/create-team (with-meta params mdata))
|
||||
(with-meta {::ev/origin :dashboard})))))
|
||||
|
||||
(defn- on-update-submit
|
||||
|
@ -59,7 +59,7 @@
|
|||
(let [mdata {:on-success (partial on-update-success form)
|
||||
:on-error (partial on-error form)}
|
||||
team (get @form :clean-data)]
|
||||
(st/emit! (dd/update-team (with-meta team mdata))
|
||||
(st/emit! (dtm/update-team (with-meta team mdata))
|
||||
(modal/hide))))
|
||||
|
||||
(defn- on-submit
|
||||
|
|
|
@ -157,8 +157,8 @@
|
|||
[:div {:class (stl/css :template-link-title)} (tr "dashboard.libraries-and-templates")]
|
||||
[:div {:class (stl/css :template-link-text)} (tr "dashboard.libraries-and-templates.explore")]]]]]]))
|
||||
|
||||
(mf/defc templates-section
|
||||
{::mf/wrap-props false}
|
||||
(mf/defc templates-section*
|
||||
{::mf/props :obj}
|
||||
[{:keys [default-project-id profile project-id team-id]}]
|
||||
(let [templates (mf/deref builtin-templates)
|
||||
templates (mf/with-memo [templates]
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
|
@ -117,7 +117,7 @@
|
|||
(let [mdata {:on-success on-success
|
||||
:on-error on-error}
|
||||
params {:name name}]
|
||||
(st/emit! (-> (dd/create-team (with-meta params mdata))
|
||||
(st/emit! (-> (dtm/create-team (with-meta params mdata))
|
||||
(with-meta {::ev/origin :onboarding-without-invitations}))
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-step"
|
||||
|
@ -133,7 +133,7 @@
|
|||
(let [mdata {:on-success on-success
|
||||
:on-error on-error}]
|
||||
|
||||
(st/emit! (-> (dd/create-team-with-invitations (with-meta params mdata))
|
||||
(st/emit! (-> (dtm/create-team-with-invitations (with-meta params mdata))
|
||||
(with-meta {::ev/origin :onboarding-with-invitations}))
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "onboarding-step"
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.util.router :as rt]
|
||||
|
@ -119,7 +119,11 @@
|
|||
(st/emit! (rt/nav :auth-login))
|
||||
|
||||
empty-path?
|
||||
(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)} (u/query-string->map qs)))
|
||||
(let [team-id (or (dtm/get-last-team-id)
|
||||
(:default-team-id profile))]
|
||||
(st/emit! (rt/nav :dashboard-projects
|
||||
{:team-id team-id}
|
||||
(u/query-string->map qs))))
|
||||
|
||||
:else
|
||||
(st/emit! (rt/assign-exception {:type :not-found})))))))))
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
(defn- on-success
|
||||
[profile]
|
||||
(st/emit! (ntf/success (tr "notifications.profile-saved"))
|
||||
(du/profile-fetched profile)))
|
||||
(du/initialize-profile profile)))
|
||||
|
||||
(defn- on-submit
|
||||
[form _event]
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
[app.config :as cf]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.dashboard.sidebar :refer [profile-section]]
|
||||
[app.main.ui.dashboard.sidebar :refer [profile-section*]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
|
@ -58,7 +58,8 @@
|
|||
options? (= section :settings-options)
|
||||
feedback? (= section :settings-feedback)
|
||||
access-tokens? (= section :settings-access-tokens)
|
||||
team-id (du/get-current-team-id profile)
|
||||
team-id (or (dtm/get-last-team-id)
|
||||
(:default-team-id profile))
|
||||
|
||||
go-dashboard
|
||||
(mf/use-fn
|
||||
|
@ -119,5 +120,5 @@
|
|||
[:div {:class (stl/css :dashboard-sidebar :settings)}
|
||||
[:& sidebar-content {:profile profile
|
||||
:section section}]
|
||||
[:& profile-section {:profile profile}]])
|
||||
[:> profile-section* {:profile profile}]])
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[app.main.ui.auth.login :refer [login-methods]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
|
||||
[app.main.ui.auth.register :as register]
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar]]
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
|
||||
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
|
||||
[app.main.ui.icons :as i]
|
||||
|
@ -267,7 +267,7 @@
|
|||
|
||||
[:div {:class (stl/css :dashboard)}
|
||||
[:div {:class (stl/css :dashboard-sidebar)}
|
||||
[:& sidebar
|
||||
[:> sidebar*
|
||||
{:team nil
|
||||
:projects []
|
||||
:project (:default-project-id profile)
|
||||
|
|
|
@ -214,7 +214,7 @@
|
|||
[:& (mf/provider ctx/components-v2) {:value components-v2?}
|
||||
[:& (mf/provider ctx/design-tokens) {:value design-tokens?}
|
||||
[:& (mf/provider ctx/workspace-read-only?) {:value read-only?}
|
||||
[:& (mf/provider ctx/team-permissions) {:value permissions}
|
||||
[:& (mf/provider ctx/permissions) {:value permissions}
|
||||
[:section {:class (stl/css :workspace)
|
||||
:style {:background-color background-color
|
||||
:touch-action "none"}}
|
||||
|
|
|
@ -425,7 +425,7 @@
|
|||
(let [select-all (mf/use-fn #(st/emit! (dw/select-all)))
|
||||
undo (mf/use-fn #(st/emit! dwu/undo))
|
||||
redo (mf/use-fn #(st/emit! dwu/redo))
|
||||
perms (mf/use-ctx ctx/team-permissions)
|
||||
perms (mf/use-ctx ctx/permissions)
|
||||
can-edit (:can-edit perms)]
|
||||
|
||||
[:& dropdown-menu {:show true
|
||||
|
@ -488,7 +488,7 @@
|
|||
frames (->> (cfh/get-immediate-children objects uuid/zero)
|
||||
(filterv cfh/frame-shape?))
|
||||
|
||||
perms (mf/use-ctx ctx/team-permissions)
|
||||
perms (mf/use-ctx ctx/permissions)
|
||||
can-edit (:can-edit perms)
|
||||
|
||||
on-remove-shared
|
||||
|
|
|
@ -134,7 +134,7 @@
|
|||
::mf/props :obj}
|
||||
[{:keys [selected shapes shapes-with-children page-id file-id on-change-section on-expand]}]
|
||||
(let [objects (mf/deref refs/workspace-page-objects)
|
||||
permissions (mf/use-ctx ctx/team-permissions)
|
||||
permissions (mf/use-ctx ctx/permissions)
|
||||
|
||||
selected-shapes (into [] (keep (d/getf objects)) selected)
|
||||
first-selected-shape (first selected-shapes)
|
||||
|
|
|
@ -207,7 +207,7 @@
|
|||
(st/emit! (dw/create-page {:file-id file-id :project-id project-id}))
|
||||
(-> event dom/get-current-target dom/blur!)))
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
permissions (mf/use-ctx ctx/team-permissions)]
|
||||
permissions (mf/use-ctx ctx/permissions)]
|
||||
|
||||
[:div {:class (stl/css :sitemap)
|
||||
:style #js {"--height" (str size "px")}}
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
|
||||
vbox' (mf/use-debounce 100 vbox)
|
||||
|
||||
permissions (mf/use-ctx ctx/team-permissions)
|
||||
permissions (mf/use-ctx ctx/permissions)
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
|
||||
;; DEREFS
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
show-distances?
|
||||
picking-color?]} wglobal
|
||||
|
||||
permissions (mf/use-ctx ctx/team-permissions)
|
||||
permissions (mf/use-ctx ctx/permissions)
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
|
||||
;; DEREFS
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.uri :as u]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.features :as features]
|
||||
[app.main.render :as render]
|
||||
[app.main.repo :as repo]
|
||||
|
@ -37,7 +37,7 @@
|
|||
(watch [_ _ _]
|
||||
(->> (repo/cmd! :get-team {:file-id file-id})
|
||||
(rx/mapcat (fn [team]
|
||||
(rx/of (du/set-current-team team)
|
||||
(rx/of (dtm/set-current-team team)
|
||||
(ptk/data-event ::team-fetched team))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
Loading…
Add table
Reference in a new issue