diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 19b7637e0e..a6c74b8100 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -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)))))) diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj index 1a25a6dcfd..801ff555b0 100644 --- a/backend/src/app/rpc/commands/search.clj +++ b/backend/src/app/rpc/commands/search.clj @@ -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 diff --git a/frontend/playwright/data/dashboard/get-project-files.json b/frontend/playwright/data/dashboard/get-project-files.json index b0394aff1f..3eec69caa3 100644 --- a/frontend/playwright/data/dashboard/get-project-files.json +++ b/frontend/playwright/data/dashboard/get-project-files.json @@ -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 } ] diff --git a/frontend/playwright/data/dashboard/thumbnail.png b/frontend/playwright/data/dashboard/thumbnail.png new file mode 100644 index 0000000000..d03c7de5b9 Binary files /dev/null and b/frontend/playwright/data/dashboard/thumbnail.png differ diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js index 75464cd0ba..c29469c59c 100644 --- a/frontend/playwright/ui/pages/DashboardPage.js +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -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() { diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index e9824671f1..a01ffdb09d 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -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 diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs new file mode 100644 index 0000000000..4a04d0d333 --- /dev/null +++ b/frontend/src/app/main/data/auth.cljs @@ -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)))))) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index e66187bead..1f727313d5 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -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)))))) diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs index 87f6709035..2daf207c63 100644 --- a/frontend/src/app/main/data/fonts.cljs +++ b/frontend/src/app/main/data/fonts.cljs @@ -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)) diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs new file mode 100644 index 0000000000..74805e3808 --- /dev/null +++ b/frontend/src/app/main/data/team.cljs @@ -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)))))) + + + diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 237cdde463..8ace629bc2 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -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)))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index e2ad1f6d8b..5267273451 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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])) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 7a4f2e2e4f..2a59336ed8 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -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 diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 4ac723f5cc..cc7e193a87 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -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)) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 17ce25d757..90605632d7 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -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}])])]])) diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index 1b5fb62b4a..7a5acbd448 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -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 diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 901f0dd58e..13513d46ae 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -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 diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 98cee17f1c..7f29071821 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -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 diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index 9e8bdbbd5e..6ec884ed5a 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -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] diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index d192558a37..09f12f9956 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 9efd0635aa..b5862ed631 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -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}])]])) diff --git a/frontend/src/app/main/ui/dashboard/change_owner.cljs b/frontend/src/app/main/ui/dashboard/change_owner.cljs index d87056e00d..2118b96ddb 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.cljs +++ b/frontend/src/app/main/ui/dashboard/change_owner.cljs @@ -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 "" diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 9a0926110c..6ce4ff2abe 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -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)))) diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 019e7b5025..f7e0ccb96b 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index fecf68eb0e..7ae210b5ff 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -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"]]]) - - diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 4c72b6852a..f73bebd11b 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index e883c5f0c5..f1d428e93d 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -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}]]])) diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 611f205cf3..37cab4ad28 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -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}]))]]]]))) diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index 862fc700af..0ad67d06a7 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -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}])]])) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 2e53b3bf76..db0219ee9a 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -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}]]) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 431dd2711a..20eb49cec9 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -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)} diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs index 631b17556f..8250d9f8c1 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/templates.cljs b/frontend/src/app/main/ui/dashboard/templates.cljs index f410c332df..d94b551929 100644 --- a/frontend/src/app/main/ui/dashboard/templates.cljs +++ b/frontend/src/app/main/ui/dashboard/templates.cljs @@ -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] diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index 0fe0f961b3..59f453985f 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -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" diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index a43038b730..74f10126f0 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -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}))))))))) diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index e1d49acfd4..2ea4a4408b 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -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] diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 651129a363..0cc08fe1eb 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -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}]]) diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 50a3141e81..4efa39b07e 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index b88f4fed9f..d5104699c5 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -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"}} diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index f002f90a24..4dc8879906 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index a635583466..3d61627edf 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 4d3ba46920..f30af07358 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -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")}} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index a89eab0c64..c839f6d91e 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 579b27c10b..2f95a851eb 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -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 diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index 215f45648a..d8a3d86505 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -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)))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;