0
Fork 0
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:
Andrey Antukh 2024-11-26 20:36:13 +01:00
parent b31afcfb47
commit b17d7c0289
45 changed files with 1684 additions and 1500 deletions

View file

@ -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))))))

View file

@ -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

View file

@ -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
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -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() {

View file

@ -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

View 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))))))

View file

@ -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))))))

View file

@ -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))

View 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))))))

View file

@ -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))))))

View file

@ -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]))

View file

@ -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

View file

@ -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))

View file

@ -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}])])]]))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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))

View file

@ -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}])]]))

View file

@ -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 ""

View file

@ -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))))

View file

@ -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

View 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"]]])

View file

@ -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

View file

@ -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}]]]))

View file

@ -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}]))]]]])))

View file

@ -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}])]]))

View file

@ -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}]])

View file

@ -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)}

View file

@ -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

View file

@ -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]

View file

@ -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"

View file

@ -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})))))))))

View file

@ -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]

View file

@ -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}]])

View file

@ -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)

View file

@ -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"}}

View file

@ -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

View file

@ -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)

View file

@ -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")}}

View file

@ -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

View file

@ -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

View file

@ -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))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;