0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-14 07:51:35 -05:00

♻️ Refactor dashboard state management.

Mainly for performance, also affects backend endpoints.
This commit is contained in:
Andrey Antukh 2021-05-07 11:36:34 +02:00 committed by Andrés Moya
parent e7b3f12b71
commit c70bc5baff
23 changed files with 1110 additions and 1007 deletions

View file

@ -12,6 +12,7 @@
[app.db :as db]
[app.rpc.permissions :as perms]
[app.rpc.queries.projects :as projects]
[app.rpc.queries.teams :as teams]
[app.util.blob :as blob]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
@ -97,7 +98,13 @@
ppr.is_owner = true or
ppr.can_edit = true)
)
select distinct f.*
select distinct
f.id,
f.project_id,
f.created_at,
f.modified_at,
f.name,
f.is_shared
from file as f
inner join projects as pr on (f.project_id = pr.id)
where f.name ilike ('%' || ? || '%')
@ -109,14 +116,14 @@
(sv/defmethod ::search-files
[{:keys [pool] :as cfg} {:keys [profile-id team-id search-term] :as params}]
(let [rows (db/exec! pool [sql:search-files
profile-id team-id
profile-id team-id
search-term])]
(into [] decode-row-xf rows)))
(db/exec! pool [sql:search-files
profile-id team-id
profile-id team-id
search-term]))
;; --- Query: Files
;; --- Query: Project Files
;; DEPRECATED: should be removed probably on 1.6.x
(def ^:private sql:files
"select f.*
@ -136,6 +143,29 @@
(into [] decode-row-xf (db/exec! conn [sql:files project-id]))))
;; --- Query: Project Files
(def ^:private sql:project-files
"select f.id,
f.project_id,
f.created_at,
f.modified_at,
f.name,
f.is_shared
from file as f
where f.project_id = ?
and f.deleted_at is null
order by f.modified_at desc")
(s/def ::project-files
(s/keys :req-un [::profile-id ::project-id]))
(sv/defmethod ::project-files
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
(with-open [conn (db/open pool)]
(projects/check-read-permissions! conn profile-id project-id)
(db/exec! conn [sql:project-files project-id])))
;; --- Query: File (By ID)
(defn retrieve-file
@ -154,17 +184,20 @@
(retrieve-file conn id)))
(s/def ::page
(s/keys :req-un [::profile-id ::id ::file-id]))
(s/keys :req-un [::profile-id ::file-id]))
(sv/defmethod ::page
[{:keys [pool] :as cfg} {:keys [profile-id file-id id]}]
[{:keys [pool] :as cfg} {:keys [profile-id file-id]}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
(let [file (retrieve-file conn file-id)]
(get-in file [:data :pages-index id]))))
(let [file (retrieve-file conn file-id)
page-id (get-in file [:data :pages 0])]
(get-in file [:data :pages-index page-id]))))
;; --- Query: Shared Library Files
;; DEPRECATED: and will be removed on 1.6.x
(def ^:private sql:shared-files
"select f.*
from file as f
@ -183,11 +216,35 @@
(into [] decode-row-xf (db/exec! pool [sql:shared-files team-id])))
;; --- Query: Shared Library Files
(def ^:private sql:team-shared-files
"select f.id,
f.project_id,
f.created_at,
f.modified_at,
f.name,
f.is_shared
from file as f
inner join project as p on (p.id = f.project_id)
where f.is_shared = true
and f.deleted_at is null
and p.deleted_at is null
and p.team_id = ?
order by f.modified_at desc")
(s/def ::team-shared-files
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::team-shared-files
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(db/exec! pool [sql:team-shared-files team-id]))
;; --- Query: File Libraries used by a File
(def ^:private sql:file-libraries
"select fl.*,
? as is_indirect,
flr.synced_at as synced_at
from file as fl
inner join file_library_rel as flr on (flr.library_file_id = fl.id)
@ -196,22 +253,12 @@
(defn retrieve-file-libraries
[conn is-indirect file-id]
(let [direct-libraries
(into [] decode-row-xf (db/exec! conn [sql:file-libraries is-indirect file-id]))
select-distinct
(fn [used-libraries new-libraries]
(remove (fn [new-library]
(some #(= (:id %) (:id new-library)) used-libraries))
new-libraries))]
(reduce (fn [used-libraries library]
(concat used-libraries
(select-distinct
used-libraries
(retrieve-file-libraries conn true (:id library)))))
direct-libraries
direct-libraries)))
(let [libraries (->> (db/exec! conn [sql:file-libraries file-id])
(map #(assoc :is-indirect is-indirect))
(into #{} decode-row-xf))]
(reduce #(into %1 (retrieve-file-libraries conn true %2))
libraries
(map :id libraries))))
(s/def ::file-libraries
(s/keys :req-un [::profile-id ::file-id]))
@ -222,31 +269,35 @@
(check-edition-permissions! conn profile-id file-id)
(retrieve-file-libraries conn false file-id)))
;; --- QUERY: team-recent-files
;; --- Query: Single File Library
(def sql:team-recent-files
"with recent_files as (
select f.id,
f.project_id,
f.created_at,
f.modified_at,
f.name,
f.is_shared,
row_number() over w as row_num
from file as f
join project as p on (p.id = f.project_id)
where p.team_id = ?
and p.deleted_at is null
and f.deleted_at is null
window w as (partition by f.project_id order by f.modified_at desc)
order by f.modified_at desc
)
select * from recent_files where row_num <= 10;")
;; TODO: this looks like is duplicate of `::file`
(s/def ::team-recent-files
(s/keys :req-un [::profile-id ::team-id]))
(def ^:private sql:file-library
"select fl.*
from file as fl
where fl.id = ?")
(defn retrieve-file-library
[conn file-id]
(let [rows (db/exec! conn [sql:file-library file-id])]
(when-not (seq rows)
(ex/raise :type :not-found))
(first (sequence decode-row-xf rows))))
(s/def ::file-library
(s/keys :req-un [::profile-id ::file-id]))
(sv/defmethod ::file-library
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id) ;; TODO: this should check read permissions
(retrieve-file-library conn file-id)))
(sv/defmethod ::team-recent-files
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(db/exec! conn [sql:team-recent-files team-id])))
;; --- Helpers

View file

@ -13,6 +13,8 @@
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; DEPRECATED: should be removed on 1.6.x
(def sql:recent-files
"with recent_files as (
select f.*, row_number() over w as row_num

View file

@ -52,7 +52,7 @@
(t/is (= (:id data) (:id result)))
(t/is (= (:name data) (:name result))))))
(t/testing "query files"
(t/testing "query files (deprecated)"
(let [data {::th/type :files
:project-id proj-id
:profile-id (:id prof)}
@ -67,6 +67,20 @@
(t/is (= "new name" (get-in result [0 :name])))
(t/is (= 1 (count (get-in result [0 :data :pages])))))))
(t/testing "query files"
(let [data {::th/type :project-files
:project-id proj-id
:profile-id (:id prof)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 1 (count result)))
(t/is (= file-id (get-in result [0 :id])))
(t/is (= "new name" (get-in result [0 :name]))))))
(t/testing "query single file without users"
(let [data {::th/type :file
:profile-id (:id prof)

View file

@ -54,146 +54,194 @@
(s/def ::file
(s/keys :req-un [::id
::name
::created-at
::modified-at
::project-id]))
::project-id]
:opt-un [::created-at
::modified-at]))
(s/def ::set-of-uuid
(s/every ::us/uuid :kind set?))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Fetching
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn fetch-team
(declare fetch-projects)
(defn initialize
[{:keys [id] :as params}]
(letfn [(fetched [team state]
(update state :teams assoc id team))]
(ptk/reify ::fetch-team
ptk/WatchEvent
(watch [_ state stream]
(let [profile (:profile state)]
(->> (rp/query :team params)
(rx/map #(partial fetched %))))))))
(us/assert ::us/uuid id)
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(let [prev-team-id (:current-team-id state)]
(cond-> state
(not= prev-team-id id)
(-> (assoc :current-team-id id)
(dissoc :dashboard-files)
(dissoc :dashboard-projects)
(dissoc :dashboard-recent-files)
(dissoc :dashboard-team-members)
(dissoc :dashboard-team-stats)))))
ptk/WatchEvent
(watch [_ state stream]
(rx/merge
(ptk/watch (fetch-projects) state stream)
(ptk/watch (du/fetch-teams) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 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
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(letfn [(fetched [members state]
(->> members
(d/index-by :id)
(assoc-in state [:team-members id])))]
(ptk/reify ::fetch-team-members
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :team-members {:team-id id})
(rx/map #(partial fetched %)))))))
(defn fetch-team-stats
[{:keys [id] :as team}]
(us/assert ::us/uuid id)
[]
(ptk/reify ::fetch-team-members
ptk/WatchEvent
(watch [_ state stream]
(let [fetched #(assoc-in %2 [:team-stats id] %1)]
(->> (rp/query :team-stats {:team-id id})
(rx/map #(partial fetched %)))))))
(let [team-id (:current-team-id state)]
(->> (rp/query :team-members {:team-id team-id})
(rx/map team-members-fetched))))))
(defn fetch-projects
[{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id)
(letfn [(fetched [projects state]
(assoc-in state [:projects team-id] (d/index-by :id projects)))]
(ptk/reify ::fetch-projects
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :projects {:team-id team-id})
(rx/map #(partial fetched %)))))))
;; --- EVENT: fetch-team-stats
(defn fetch-bundle
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::fetch-bundle
(defn team-stats-fetched
[stats]
(ptk/reify ::team-stats-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-team-stats stats))))
(defn fetch-team-stats
[]
(ptk/reify ::fetch-team-stats
ptk/WatchEvent
(watch [_ state stream]
(let [profile (:profile state)]
(rx/merge (ptk/watch (fetch-team params) state stream)
(ptk/watch (fetch-projects {:team-id id}) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream))))))
(let [team-id (:current-team-id state)]
(->> (rp/query :team-stats {:team-id team-id})
(rx/map team-stats-fetched))))))
(s/def :internal.event.search-files/team-id ::us/uuid)
(s/def :internal.event.search-files/search-term (s/nilable ::us/string))
;; --- EVENT: fetch-projects
(s/def :internal.event/search-files
(s/keys :req-un [:internal.event.search-files/search-term
:internal.event.search-files/team-id]))
(defn projects-fetched
[projects]
(ptk/reify ::projects-fetched
ptk/UpdateEvent
(update [_ state]
(let [projects (d/index-by :id projects)]
(assoc state :dashboard-projects projects)))))
(defn search-files
(defn fetch-projects
[]
(ptk/reify ::fetch-projects
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)]
(->> (rp/query :projects {:team-id team-id})
(rx/map projects-fetched))))))
;; --- EVENT: search
(defn search-result-fetched
[result]
(ptk/reify ::search-result-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-search-result result))))
(s/def ::search-term (s/nilable ::us/string))
(s/def ::search
(s/keys :req-un [::search-term ]))
(defn search
[params]
(us/assert :internal.event/search-files params)
(letfn [(fetched [result state]
(update state :dashboard-local
assoc :search-result result))]
(ptk/reify ::search-files
(us/assert ::search params)
(ptk/reify ::search
ptk/UpdateEvent
(update [_ state]
(dissoc state :dashboard-search-result))
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/query :search-files params)
(rx/map search-result-fetched))))))
;; --- EVENT: files
(defn files-fetched
[project-id files]
(letfn [(remove-project-files [files]
(reduce-kv (fn [result id file]
(cond-> result
(= (:project-id file) project-id) (dissoc id)))
files
files))]
(ptk/reify ::files-fetched
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-local
assoc :search-result nil))
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :search-files params)
(rx/map #(partial fetched %)))))))
(update state :dashboard-files
(fn [state]
(let [state (remove-project-files state)]
(reduce #(assoc %1 (:id %2) %2) state files))))))))
(defn fetch-files
[{:keys [project-id] :as params}]
(us/assert ::us/uuid project-id)
(letfn [(fetched [files state]
(update state :files assoc project-id (d/index-by :id files)))]
(ptk/reify ::fetch-files
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :files params)
(rx/map #(partial fetched %)))))))
(defn fetch-shared-files
[{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id)
(letfn [(fetched [files state]
(update state :shared-files assoc team-id (d/index-by :id files)))]
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :shared-files {:team-id team-id})
(rx/map #(partial fetched %)))))))
(declare recent-files-fetched)
(defn fetch-recent-files
[{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id)
(ptk/reify ::fetch-recent-files
(ptk/reify ::fetch-files
ptk/WatchEvent
(watch [_ state stream]
(let [params {:team-id team-id}]
(->> (rp/query :recent-files params)
(rx/map #(recent-files-fetched team-id %)))))))
(->> (rp/query :project-files {:project-id project-id})
(rx/map #(files-fetched project-id %))))))
;; --- EVENT: shared-files
(defn shared-files-fetched
[files]
(ptk/reify ::shared-files-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-shared-files (d/index-by :id files)))))
(defn fetch-shared-files
[]
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)]
(->> (rp/query :team-shared-files {:team-id team-id})
(rx/map shared-files-fetched))))))
;; --- EVENT: recent-files
(defn recent-files-fetched
[team-id files]
[files]
(ptk/reify ::recent-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [projects (keys (get-in state [:projects team-id]))]
(reduce (fn [state project-id]
(let [files (filter #(= project-id (:project-id %)) files)]
(-> state
(update-in [:files project-id] merge (d/index-by :id files))
(assoc-in [:recent-files project-id] (into #{} (map :id) files)))))
state
projects)))))
(let [files (d/index-by :id files)]
(-> state
(assoc :dashboard-recent-files files)
(update :dashboard-files d/merge files))))))
(defn fetch-recent-files
[]
(ptk/reify ::fetch-recent-files
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)]
(->> (rp/query :team-recent-files {:team-id team-id})
(rx/map recent-files-fetched))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection
@ -209,31 +257,34 @@
:selected-project nil))))
(defn toggle-file-select
[{:keys [file] :as params}]
[{:keys [id project-id] :as file}]
(us/assert ::file file)
(ptk/reify ::toggle-file-select
ptk/UpdateEvent
(update [_ state]
(let [file-id (:id file)
selected-project (get-in state [:dashboard-local
:selected-project])]
(if (or (nil? selected-project)
(= selected-project (:project-id file)))
(let [selected-project-id (get-in state [:dashboard-local :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? % file-id)
(disj % file-id)
(conj % file-id)))
(assoc :selected-project
(:project-id file)))))
(update :selected-files #(if (contains? % id)
(disj % id)
(conj % id)))
(assoc :selected-project project-id))))
state)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Modification
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Create Project
;; --- EVENT: create-team
(defn team-created
[team]
(ptk/reify ::team-created
IDeref
(-deref [_] team)))
(defn create-team
[{:keys [name] :as params}]
@ -246,9 +297,10 @@
on-error rx/throw}} (meta params)]
(->> (rp/mutation! :create-team {:name name})
(rx/tap on-success)
(rx/catch on-error)
(rx/map (fn [team]
(ptk/event ::ev/event {::ev/name "create-team" :id (:id team)}))))))))
(rx/map team-created)
(rx/catch on-error))))))
;; --- EVENT: update-team
(defn update-team
[{:keys [id name] :as params}]
@ -264,75 +316,75 @@
(rx/ignore)))))
(defn update-team-photo
[{:keys [file team-id] :as params}]
[{:keys [file] :as params}]
(us/assert ::di/file file)
(us/assert ::us/uuid team-id)
(ptk/reify ::update-team-photo
ptk/WatchEvent
(watch [_ state stream]
(let [on-success di/notify-finished-loading
on-error #(do (di/notify-finished-loading)
(di/process-error %))
prepare #(hash-map :file % :team-id team-id)]
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/mutation :update-team-photo %))
(rx/do on-success)
(rx/map #(fetch-team %))
(rx/map du/fetch-teams)
(rx/catch on-error))))))
(defn update-team-member-role
[{:keys [team-id role member-id] :as params}]
(us/assert ::us/uuid team-id)
[{:keys [role member-id] :as params}]
(us/assert ::us/uuid member-id)
(us/assert ::us/keyword role)
(ptk/reify ::update-team-member-role
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation! :update-team-member-role params)
(rx/mapcat #(rx/of (fetch-team-members {:id team-id})
(fetch-team {:id team-id})))))))
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/mutation! :update-team-member-role params)
(rx/mapcat (fn [_]
(rx/of (fetch-team-members)
(du/fetch-teams)))))))))
(defn delete-team-member
[{:keys [team-id member-id] :as params}]
(us/assert ::us/uuid team-id)
[{:keys [member-id] :as params}]
(us/assert ::us/uuid member-id)
(ptk/reify ::delete-team-member
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation! :delete-team-member params)
(rx/mapcat #(rx/of (fetch-team-members {:id team-id})
(fetch-team {:id team-id})))))))
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/mutation! :delete-team-member params)
(rx/mapcat (fn [_]
(rx/of (fetch-team-members)
(du/fetch-teams)))))))))
(defn leave-team
[{:keys [id reassign-to] :as params}]
(us/assert ::team params)
[{:keys [reassign-to] :as params}]
(us/assert (s/nilable ::us/uuid) reassign-to)
(ptk/reify ::leave-team
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
on-error rx/throw}} (meta params)
team-id (:current-team-id state)]
(rx/concat
(when (uuid? reassign-to)
(->> (rp/mutation! :update-team-member-role {:team-id id
(->> (rp/mutation! :update-team-member-role {:team-id team-id
:role :owner
:member-id reassign-to})
(rx/ignore)))
(->> (rp/mutation! :leave-team {:id id})
(->> (rp/mutation! :leave-team {:id team-id})
(rx/tap on-success)
(rx/catch on-error)))))))
(defn invite-team-member
[{:keys [team-id email role] :as params}]
(us/assert ::us/uuid team-id)
[{:keys [email role] :as params}]
(us/assert ::us/email email)
(us/assert ::us/keyword role)
(ptk/reify ::invite-team-member
@ -340,11 +392,15 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
on-error rx/throw}} (meta params)
team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/mutation! :invite-team-member params)
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: delete-team
(defn delete-team
[{:keys [id] :as params}]
(us/assert ::team params)
@ -358,9 +414,10 @@
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: create-project
(defn- project-created
[{:keys [id team-id] :as project}]
[{:keys [id] :as project}]
(ptk/reify ::project-created
IDeref
(-deref [_] project)
@ -368,44 +425,52 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:projects team-id id] project)
(assoc-in [:dashboard-projects id] project)
(assoc-in [:dashboard-local :project-for-edit] id)))))
(defn create-project
[{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id)
[]
(ptk/reify ::create-project
ptk/WatchEvent
(watch [_ state stream]
(let [name (name (gensym "New Project "))
(let [name (name (gensym "New Project "))
team-id (:current-team-id state)
params {:name name
:team-id team-id}
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/mutation! :create-project {:name name :team-id team-id})
(->> (rp/mutation! :create-project params)
(rx/tap on-success)
(rx/catch on-error)
(rx/map project-created))))))
(rx/map project-created)
(rx/catch on-error))))))
;; --- EVENT: duplicate-project
(defn project-duplicated
[{:keys [id] :as project}]
(ptk/reify ::project-duplicated
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard-projects id] project))))
(defn duplicate-project
[{:keys [id name] :as params}]
(us/assert ::us/uuid id)
(letfn [(duplicated [project state]
(-> state
(assoc-in [:projects (:team-id project) (:id project)] project)))]
(ptk/reify ::duplicate-project
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)
(ptk/reify ::duplicate-project
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
new-name (str name " " (tr "dashboard.copy-suffix"))]
new-name (str name " " (tr "dashboard.copy-suffix"))]
(->> (rp/mutation! :duplicate-project {:project-id id
:name new-name})
(rx/tap on-success)
(rx/map #(partial duplicated %))
(rx/catch on-error)))))))
(->> (rp/mutation! :duplicate-project {:project-id id
:name new-name})
(rx/tap on-success)
(rx/map project-duplicated)
(rx/catch on-error))))))
(defn move-project
[{:keys [id team-id] :as params}]
@ -416,7 +481,7 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)]
on-error rx/throw}} (meta params)]
(->> (rp/mutation! :move-project {:project-id id
:team-id team-id})
@ -424,21 +489,21 @@
(rx/catch on-error))))))
(defn toggle-project-pin
[{:keys [id is-pinned team-id] :as params}]
(us/assert ::project params)
[{:keys [id is-pinned team-id] :as project}]
(us/assert ::project project)
(ptk/reify ::toggle-project-pin
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:projects team-id id :is-pinned] (not is-pinned)))
(assoc-in state [:dashboard-projects id :is-pinned] (not is-pinned)))
ptk/WatchEvent
(watch [_ state stream]
(let [project (get-in state [:projects team-id id])
(let [project (get-in state [:dashboard-projects id])
params (select-keys project [:id :is-pinned :team-id])]
(->> (rp/mutation :update-project-pin params)
(rx/ignore))))))
;; --- Rename Project
;; --- EVENT: rename-project
(defn rename-project
[{:keys [id name team-id] :as params}]
@ -447,7 +512,7 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:projects team-id id :name] name)
(update-in [:dashboard-projects id :name] (constantly name))
(update :dashboard-local dissoc :project-for-edit)))
ptk/WatchEvent
@ -456,7 +521,7 @@
(->> (rp/mutation :rename-project params)
(rx/ignore))))))
;; --- Delete Project (by id)
;; --- EVENT: delete-project
(defn delete-project
[{:keys [id team-id] :as params}]
@ -464,16 +529,21 @@
(ptk/reify ::delete-project
ptk/UpdateEvent
(update [_ state]
(update-in state [:projects team-id] dissoc id))
(update state :dashboard-projects dissoc id))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/mutation :delete-project {:id id})
(rx/ignore)))))
;; --- Delete File (by id)
;; --- EVENT: delete-file
(declare delete-file-result)
(defn file-deleted
[team-id project-id]
(ptk/reify ::file-deleted
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard-projects project-id :count] dec))))
(defn delete-file
[{:keys [id project-id] :as params}]
@ -482,33 +552,26 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(update-in [:files project-id] dissoc id)
(update-in [:recent-files project-id] (fnil disj #{}) id)))
(d/update-when :dashboard-files dissoc id)
(d/update-when :dashboard-recent-files dissoc id)))
ptk/WatchEvent
(watch [_ state s]
(let [team-id (uuid/uuid (get-in state [:route :path-params :team-id]))]
(->> (rp/mutation :delete-file {:id id})
(rx/map #(delete-file-result team-id project-id)))))))
(defn delete-file-result
[team-id project-id]
(ptk/reify ::delete-file
ptk/UpdateEvent
(update [_ state]
(-> state
(update-in [:projects team-id project-id :count] dec)))))
(rx/map #(file-deleted team-id project-id)))))))
;; --- Rename File
(defn rename-file
[{:keys [id name project-id] :as params}]
[{:keys [id name] :as params}]
(us/assert ::file params)
(ptk/reify ::rename-file
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:files project-id id :name] name))
(-> state
(d/update-in-when [:dashboard-files id :name] (constantly name))
(d/update-in-when [:dashboard-recent-files id :name] (constantly name))))
ptk/WatchEvent
(watch [_ state stream]
@ -519,12 +582,14 @@
;; --- Set File shared
(defn set-file-shared
[{:keys [id project-id is-shared] :as params}]
[{:keys [id is-shared] :as params}]
(us/assert ::file params)
(ptk/reify ::set-file-shared
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:files project-id id :is-shared] is-shared))
(-> 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))))
ptk/WatchEvent
(watch [_ state stream]
@ -532,10 +597,23 @@
(->> (rp/mutation :set-file-shared params)
(rx/ignore))))))
;; --- Create File
;; --- EVENT: create-file
(declare file-created)
(defn file-created
[{:keys [id] :as file}]
(us/verify ::file file)
(ptk/reify ::file-created
IDeref
(-deref [_] file)
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:dashboard-files id] file)
(assoc-in [:dashboard-recent-files id] file)))))
(defn create-file
[{:keys [project-id] :as params}]
(us/assert ::us/uuid project-id)
@ -551,23 +629,10 @@
(->> (rp/mutation! :create-file params)
(rx/tap on-success)
(rx/catch on-error)
(rx/map file-created))))))
(rx/map file-created)
(rx/catch on-error))))))
(defn file-created
[{:keys [project-id id] :as file}]
(us/verify ::file file)
(ptk/reify ::file-created
IDeref
(-deref [_] file)
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:files project-id id] file)
(update-in [:recent-files project-id] (fnil conj #{}) id)))))
;; --- Duplicate File
;; --- EVENT: duplicate-file
(defn duplicate-file
[{:keys [id name] :as params}]
@ -578,7 +643,7 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)
on-error rx/throw}} (meta params)
new-name (str name " " (tr "dashboard.copy-suffix"))]
@ -588,7 +653,7 @@
(rx/map file-created)
(rx/catch on-error))))))
;; --- Move File
;; --- EVENT: move-files
(defn move-files
[{:keys [ids project-id] :as params}]
@ -599,10 +664,76 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)]
on-error rx/throw}} (meta params)]
(->> (rp/mutation! :move-files {:ids ids
:project-id project-id})
(->> (rp/mutation! :move-files {:ids ids :project-id project-id})
(rx/tap on-success)
(rx/catch on-error))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Navigation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn go-to-workspace
[{:keys [id project-id] :as file}]
(us/assert ::file file)
(ptk/reify ::go-to-workspace
ptk/WatchEvent
(watch [_ state stream]
(let [pparams {:project-id project-id :file-id id}]
(rx/of (rt/nav :workspace pparams))))))
(defn go-to-files
[project-id]
(ptk/reify ::go-to-files
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-files {:team-id team-id
:project-id project-id}))))))
(defn go-to-search
([] (go-to-search nil))
([term]
(ptk/reify ::go-to-search
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)]
(if (empty? term)
(rx/of (rt/nav :dashboard-search
{:team-id team-id}))
(rx/of (rt/nav :dashboard-search
{:team-id team-id}
{:search-term term}))))))))
(defn go-to-projects
([]
(ptk/reify ::go-to-projects
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
([team-id]
(ptk/reify ::go-to-projects
ptk/WatchEvent
(watch [_ state stream]
(du/set-current-team! team-id)
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
(defn go-to-team-members
[]
(ptk/reify ::go-to-team-members
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-team-members {:team-id team-id}))))))
(defn go-to-team-settings
[]
(ptk/reify ::go-to-team-settings
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-team-settings {:team-id team-id}))))))

View file

@ -58,19 +58,28 @@
[team-id]
(swap! storage assoc ::current-team-id team-id))
;; --- EVENT: fetch-teams
(defn teams-fetched
[teams]
(let [teams (d/index-by :id teams)]
(ptk/reify ::teams-fetched
IDeref
(-deref [_] teams)
ptk/UpdateEvent
(update [_ state]
(assoc state :teams teams)))))
(defn fetch-teams
[]
(letfn [(on-fetched [state data]
(let [teams (d/index-by :id data)]
(assoc state :teams teams)))]
(ptk/reify ::fetch-teams
ptk/WatchEvent
(watch [_ state s]
(->> (rp/query! :teams)
(rx/map (fn [data] #(on-fetched % data))))))))
(ptk/reify ::fetch-teams
ptk/WatchEvent
(watch [_ state s]
(->> (rp/query! :teams)
(rx/map teams-fetched)))))
;; --- EVENT: fetch-profile
(defn profile-fetched
[{:keys [id] :as profile}]
@ -94,8 +103,6 @@
(some-> (:theme profile)
(theme/set-current-theme!)))))))
;; --- Fetch Profile
(defn fetch-profile
[]
(ptk/reify ::fetch-profile

View file

@ -160,16 +160,12 @@
;; Initialize notifications (websocket connection) and the file persistence
(->> stream
(rx/filter (ptk/type? ::dwp/bundle-fetched))
(rx/first)
(rx/mapcat #(rx/of (dwn/initialize file-id)
(dwp/initialize-file-persistence file-id))))
;; Initialize Indexes (webworker)
(->> stream
(rx/filter (ptk/type? ::dwp/bundle-fetched))
(rx/take 1)
(rx/map deref)
(rx/map dwc/initialize-indices)
(rx/first))
(rx/mapcat (fn [bundle]
(rx/of (dwn/initialize file-id)
(dwp/initialize-file-persistence file-id)
(dwc/initialize-indices bundle)))))
;; Mark file initialized when indexes are ready
(->> stream
@ -217,35 +213,39 @@
(rx/of (dwn/finalize file-id)
::dwp/finalize))))
(declare go-to-page)
(defn initialize-page
[page-id]
(us/assert ::us/uuid page-id)
(ptk/reify ::initialize-page
ptk/UpdateEvent
(update [_ state]
(let [;; we maintain a cache of page state for user convenience
;; with the exception of the selection; when user abandon
;; the current page, the selection is lost
local (-> state
(get-in [:workspace-cache page-id] workspace-local-default)
(assoc :selected (d/ordered-set)))
page (-> (get-in state [:workspace-data :pages-index page-id])
(select-keys [:id :name]))]
(assoc state
:current-page-id page-id ; mainly used by events
:trimmed-page page
:workspace-local local)))))
page (get-in state [:workspace-data :pages-index page-id])
local (-> state
(get-in [:workspace-cache page-id] workspace-local-default)
(assoc :selected (d/ordered-set)))]
(-> state
(assoc :current-page-id page-id)
(assoc :trimmed-page (select-keys page [:id :name]))
(assoc :workspace-local local)
(update-in [:route :params :query] assoc :page-id (str page-id)))))))
(defn finalize-page
[page-id]
(us/verify ::us/uuid page-id)
(us/assert ::us/uuid page-id)
(ptk/reify ::finalize-page
ptk/UpdateEvent
(update [_ state]
(let [local (-> (:workspace-local state)
(dissoc :edition)
(dissoc :edit-path)
(dissoc :selected))]
(let [page-id (or page-id (get-in state [:workspace-data :pages 0]))
local (-> (:workspace-local state)
(dissoc :edition)
(dissoc :edit-path)
(dissoc :selected))]
(-> state
(assoc-in [:workspace-cache page-id] local)
(dissoc :current-page-id :workspace-local :trimmed-page :workspace-drawing))))))
@ -1043,7 +1043,7 @@
uchg {:type :mov-page
:id id
:index cidx}]
(rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(rx/of (dch/commit-changes [rchg] [uchg]))))))
;; --- Shape / Selection Alignment and Distribution
@ -1169,16 +1169,27 @@
(rx/of (rt/nav :workspace/page params))))))
(defn go-to-page
[page-id]
(us/verify ::us/uuid page-id)
(ptk/reify ::go-to-page
ptk/WatchEvent
(watch [_ state stream]
(let [project-id (get-in state [:workspace-project :id])
file-id (get-in state [:workspace-file :id])
pparams {:file-id file-id :project-id project-id}
qparams {:page-id page-id}]
(rx/of (rt/nav :workspace pparams qparams))))))
([]
(ptk/reify ::go-to-page
ptk/WatchEvent
(watch [_ state stream]
(let [project-id (:current-project-id state)
file-id (:current-file-id state)
page-id (get-in state [:workspace-data :pages 0])
pparams {:file-id file-id :project-id project-id}
qparams {:page-id page-id}]
(rx/of (rt/nav :workspace pparams qparams))))))
([page-id]
(us/verify ::us/uuid page-id)
(ptk/reify ::go-to-page
ptk/WatchEvent
(watch [_ state stream]
(let [project-id (:current-project-id state)
file-id (:current-file-id state)
pparams {:file-id file-id :project-id project-id}
qparams {:page-id page-id}]
(rx/of (rt/nav :workspace pparams qparams)))))))
(defn go-to-layout
[layout]

View file

@ -162,8 +162,7 @@
file-id]
:or {save-undo? true}
:as opts}]
(us/assert ::cp/changes changes)
(us/assert ::cp/changes undo-changes)
(log/debug :msg "commit-changes"
:js/changes changes
:js/undo-changes undo-changes)
@ -178,12 +177,13 @@
ptk/UpdateEvent
(update [_ state]
(let [current-file-id (get state :current-file-id)
file-id (or file-id current-file-id)
path (if (= file-id current-file-id)
[:workspace-data]
[:workspace-libraries file-id :data])]
file-id (or file-id current-file-id)
path (if (= file-id current-file-id)
[:workspace-data]
[:workspace-libraries file-id :data])]
(try
(us/assert ::spec/changes changes)
(us/assert ::spec/changes undo-changes)
(update-in state path cp/process-changes changes false)
(catch :default e
(vreset! error e)

View file

@ -21,9 +21,9 @@
[objects page-id shape old-content new-content]
(us/verify ::spec/content old-content)
(us/verify ::spec/content new-content)
(let [shape-id (:id shape)
frame-id (:frame-id shape)
parent-id (:parent-id shape)
(let [shape-id (:id shape)
frame-id (:frame-id shape)
parent-id (:parent-id shape)
parent-index (cp/position-on-parent shape-id objects)
[old-points old-selrect] (helpers/content->points+selrect shape old-content)
@ -72,7 +72,6 @@
(defn save-path-content
([]
(save-path-content {}))
([{:keys [preserve-move-to] :or {preserve-move-to false}}]
(ptk/reify ::save-path-content
ptk/UpdateEvent
@ -86,14 +85,14 @@
ptk/WatchEvent
(watch [_ state stream]
(let [objects (wsh/lookup-page-objects state)
id (get-in state [:workspace-local :edition])
(let [objects (wsh/lookup-page-objects state)
page-id (:current-page-id state)
id (get-in state [:workspace-local :edition])
old-content (get-in state [:workspace-local :edit-path id :old-content])]
(if (some? old-content)
(let [shape (get-in state (st/get-path state))
page-id (:current-page-id state)
(let [shape (get-in state (st/get-path state))
[rch uch] (generate-path-changes objects page-id shape old-content (:content shape))]
(rx/of (dch/commit-changes rch uch {:commit-local? true})))
(rx/of (dch/commit-changes rch uch)))
(rx/empty)))))))

View file

@ -310,8 +310,8 @@
(us/assert ::us/uuid team-id)
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :shared-files params)
(watch [it state stream]
(->> (rp/query :team-shared-files {:team-id team-id})
(rx/map shared-files-fetched)))))
(defn shared-files-fetched

View file

@ -27,8 +27,8 @@
;; Undo / Redo
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::undo-changes ::cp/changes)
(s/def ::redo-changes ::cp/changes)
(s/def ::undo-changes ::spec/changes)
(s/def ::redo-changes ::spec/changes)
(s/def ::undo-entry
(s/keys :req-un [::undo-changes ::redo-changes]))

View file

@ -47,6 +47,30 @@
(def dashboard-fonts
(l/derived :dashboard-fonts 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
(l/derived (fn [state]
(let [team-id (:current-team-id state)]
(get-in state [:teams team-id])))
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-selected-project
(l/derived (fn [state]
(get-in state [:dashboard-local :selected-project]))
@ -54,17 +78,14 @@
(def dashboard-selected-files
(l/derived (fn [state]
(get-in state [:dashboard-local :selected-files] #{}))
st/state))
(def dashboard-selected-file-objs
(l/derived (fn [state]
(let [dashboard-local (get state :dashboard-local)
selected-project (get dashboard-local :selected-project)
selected-files (get dashboard-local :selected-files #{})]
(map #(get-in state [:files selected-project %])
selected-files)))
st/state))
(let [get-file #(get-in state [:dashboard-files %])
sim-file #(select-keys % [:id :name :project-id])
selected (get-in state [:dashboard-local :selected-files])
xform (comp (map get-file)
(map sim-file))]
(->> (into #{} xform selected)
(d/index-by :id))))
st/state =))
;; ---- Workspace refs
@ -130,10 +151,11 @@
(def workspace-file
(l/derived (fn [state]
(when-let [file (:workspace-file state)]
(let [file (:workspace-file state)
data (:workspace-data state)]
(-> file
(dissoc :data)
(assoc :pages (get-in file [:data :pages])))))
(assoc :pages (:pages data)))))
st/state =))
(def workspace-file-colors

View file

@ -107,85 +107,85 @@
(mf/defc main-page
{::mf/wrap [#(mf/catch % {:fallback on-main-error})]}
[{:keys [route] :as props}]
(let [{:keys [data params]} route]
[:& (mf/provider ctx/current-route) {:value route}
(case (:name data)
(:auth-login
:auth-register
:auth-register-success
:auth-recovery-request
:auth-recovery)
[:& auth {:route route}]
[:& (mf/provider ctx/current-route) {:value route}
(case (get-in route [:data :name])
(:auth-login
:auth-register
:auth-register-success
:auth-recovery-request
:auth-recovery)
[:& auth {:route route}]
:auth-verify-token
[:& verify-token {:route route}]
:auth-verify-token
[:& verify-token {:route route}]
(:settings-profile
:settings-password
:settings-options
:settings-feedback)
[:& settings/settings {:route route}]
(:settings-profile
:settings-password
:settings-options
:settings-feedback)
[:& settings/settings {:route route}]
:debug-icons-preview
(when *assert*
[:div.debug-preview
[:h1 "Cursors"]
[:& c/debug-preview]
[:h1 "Icons"]
[:& i/debug-icons-preview]
])
:debug-icons-preview
(when *assert*
[:div.debug-preview
[:h1 "Cursors"]
[:& c/debug-preview]
[:h1 "Icons"]
[:& i/debug-icons-preview]
])
(:dashboard-search
:dashboard-projects
:dashboard-files
:dashboard-libraries
:dashboard-fonts
:dashboard-font-providers
:dashboard-team-members
:dashboard-team-settings)
[:*
#_[:div.modal-wrapper
[:& app.main.ui.onboarding/release-notes-modal {:version "1.5"}]]
[:& dashboard {:route route}]]
(:dashboard-search
:dashboard-projects
:dashboard-files
:dashboard-libraries
:dashboard-fonts
:dashboard-font-providers
:dashboard-team-members
:dashboard-team-settings)
[:*
#_[:div.modal-wrapper
[:& app.main.ui.onboarding/release-notes-modal {:version "1.5"}]]
[:& dashboard {:route route}]]
:viewer
(let [index (get-in route [:query-params :index])
token (get-in route [:query-params :token])
section (get-in route [:query-params :section] :interactions)
file-id (get-in route [:path-params :file-id])
page-id (get-in route [:path-params :page-id])]
[:& fs/fullscreen-wrapper {}
(if (= section :handoff)
[:& handoff {:page-id page-id
:file-id file-id
:index index
:token token}]
[:& viewer-page {:page-id page-id
:file-id file-id
:section section
:index index
:token token}])])
:viewer
(let [index (get-in route [:query-params :index])
token (get-in route [:query-params :token])
section (get-in route [:query-params :section] :interactions)
file-id (get-in route [:path-params :file-id])
page-id (get-in route [:path-params :page-id])]
[:& fs/fullscreen-wrapper {}
(if (= section :handoff)
[:& handoff {:page-id page-id
:file-id file-id
:index index
:token token}]
[:& viewer-page {:page-id page-id
:file-id file-id
:section section
:index index
:token token}])])
:render-object
(do
(let [file-id (uuid (get-in route [:path-params :file-id]))
page-id (uuid (get-in route [:path-params :page-id]))
object-id (uuid (get-in route [:path-params :object-id]))]
[:& render/render-object {:file-id file-id
:page-id page-id
:object-id object-id}]))
:render-object
(do
(let [file-id (uuid (get-in route [:path-params :file-id]))
page-id (uuid (get-in route [:path-params :page-id]))
object-id (uuid (get-in route [:path-params :object-id]))]
[:& render/render-object {:file-id file-id
:page-id page-id
:object-id object-id}]))
:workspace
(let [project-id (uuid (get-in route [:params :path :project-id]))
file-id (uuid (get-in route [:params :path :file-id]))
page-id (uuid (get-in route [:params :query :page-id]))
layout-name (get-in route [:params :query :layout])]
[:& workspace/workspace {:project-id project-id
:file-id file-id
:page-id page-id
:layout-name (keyword layout-name)
:key file-id}])
nil)])
: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)]
[:& workspace/workspace {:project-id project-id
:file-id file-id
:page-id page-id
:layout-name layout
:key file-id}])
nil)]))
(mf/defc app
[]
@ -249,7 +249,7 @@
context (str/fmt "ns: '%s'\nname: '%s'\nfile: '%s:%s'"
(:ns context)
(:name context)
(str cfg/public-uri "/js/cljs-runtime/" (:file context))
(str cfg/public-uri "js/cljs-runtime/" (:file context))
(:line context))]
(ts/schedule
(st/emitf

View file

@ -51,14 +51,6 @@
(uuid-str? project-id)
(assoc :project-id (uuid project-id)))))
(defn- team-ref
[id]
(l/derived (l/in [:teams id]) st/state))
(defn- projects-ref
[team-id]
(l/derived (l/in [:projects team-id]) st/state))
(mf/defc dashboard-content
[{:keys [team projects project section search-term profile] :as props}]
[:div.dashboard-content {:on-click (st/emitf (dd/clear-selected-files))}
@ -101,16 +93,15 @@
team-id (:team-id params)
search-term (:search-term params)
projects-ref (mf/use-memo (mf/deps team-id) #(projects-ref team-id))
team-ref (mf/use-memo (mf/deps team-id) #(team-ref team-id))
teams (mf/deref refs/teams)
team (get teams team-id)
team (mf/deref team-ref)
projects (mf/deref projects-ref)
projects (mf/deref refs/dashboard-projects)
project (get projects project-id)]
(mf/use-effect
(mf/deps team-id)
(st/emitf (dd/fetch-bundle {:id team-id})))
(st/emitf (dd/initialize {:id team-id})))
(mf/use-effect
(mf/deps)
@ -122,12 +113,18 @@
(not= "0.0" (:main @cf/version)))
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes :version (:main @cf/version)})))))))
[:& (mf/provider ctx/current-file-id) {:value nil}
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id}
[:& (mf/provider ctx/current-page-id) {:value nil}
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id}
[:section.dashboard-layout
;; 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 becase we want to completly refresh all the
;; components on team change. Many components assumess that the
;; team is already set so don't put the team into mf/deps.
(when team
[:section.dashboard-layout {:key (:id team)}
[:& sidebar
{:team team
:projects projects
@ -142,5 +139,5 @@
:project project
:section section
:search-term search-term
:team team}])]]]]]))
:team team}])])]]))

View file

@ -19,6 +19,33 @@
[beicon.core :as rx]
[rumext.alpha :as mf]))
(defn get-project-name
[project]
(if (:is-default project)
(tr "labels.drafts")
(:name project)))
(defn get-team-name
[team]
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team)))
(defn group-by-team
"Group projects by team."
[projects]
(reduce (fn [teams project]
(update teams
(:team-id project)
#(if (nil? %)
{:id (:team-id project)
:name (:team-name project)
:is-default (:is-default-team project)
:projects [project]}
(update % :projects conj project))))
{}
projects))
(mf/defc file-menu
[{:keys [files show? on-edit on-menu-close top left navigate?] :as props}]
(assert (seq files) "missing `files` prop")
@ -26,8 +53,8 @@
(assert (fn? on-edit) "missing `on-edit` prop")
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
(assert (boolean? navigate?) "missing `navigate?` prop")
(let [top (or top 0)
left (or left 0)
(let [top (or top 0)
left (or left 0)
file (first files)
file-count (count files)
@ -35,157 +62,113 @@
current-team-id (mf/use-ctx ctx/current-team-id)
teams (mf/use-state nil)
current-team (get @teams current-team-id)
other-teams (remove #(= (:id %) current-team-id)
(vals @teams))
other-teams (remove #(= (:id %) current-team-id) (vals @teams))
current-projects (remove #(= (:id %) (:project-id file))
(:projects current-team))
project-name (fn [project]
(if (:is-default project)
(tr "labels.drafts")
(:name project)))
team-name (fn [team]
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team)))
on-new-tab
(mf/use-callback
(mf/deps file)
(fn [event]
(let [pparams {:project-id (:project-id file)
:file-id (:id file)}
qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav-new-window :workspace pparams qparams)))))
(fn [event]
(let [pparams {:project-id (:project-id file)
:file-id (:id file)}
qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav-new-window :workspace pparams qparams))))
on-duplicate
(mf/use-callback
(mf/deps files)
(fn [event]
(apply st/emit! (map dd/duplicate-file files))
(st/emit! (dm/success (tr "dashboard.success-duplicate-file")))))
(fn [event]
(apply st/emit! (map dd/duplicate-file files))
(st/emit! (dm/success (tr "dashboard.success-duplicate-file"))))
delete-fn
(mf/use-callback
(mf/deps files)
(fn [event]
(apply st/emit! (map dd/delete-file files))
(st/emit! (dm/success (tr "dashboard.success-delete-file")))))
(fn [event]
(apply st/emit! (map dd/delete-file files))
(st/emit! (dm/success (tr "dashboard.success-delete-file"))))
on-delete
(mf/use-callback
(mf/deps files)
(fn [event]
(dom/stop-propagation event)
(if multi?
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-file-multi-confirm.title" file-count)
:message (tr "modals.delete-file-multi-confirm.message" file-count)
:accept-label (tr "modals.delete-file-multi-confirm.accept" file-count)
(fn [event]
(dom/stop-propagation event)
(if multi?
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-file-multi-confirm.title" file-count)
:message (tr "modals.delete-file-multi-confirm.message" file-count)
:accept-label (tr "modals.delete-file-multi-confirm.accept" file-count)
:on-accept delete-fn}))
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-file-confirm.title")
:message (tr "modals.delete-file-confirm.message")
:accept-label (tr "modals.delete-file-confirm.accept")
:on-accept delete-fn})))))
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-file-confirm.title")
:message (tr "modals.delete-file-confirm.message")
:accept-label (tr "modals.delete-file-confirm.accept")
:on-accept delete-fn}))))
on-move-success
(fn [team-id project-id]
(if multi?
(st/emit! (dm/success (tr "dashboard.success-move-files")))
(st/emit! (dm/success (tr "dashboard.success-move-file"))))
(if (or navigate? (not= team-id current-team-id))
(st/emit! (dd/go-to-files project-id))
(st/emit! (dd/fetch-recent-files)
(dd/clear-selected-files))))
on-move
(mf/use-callback
(mf/deps file)
(fn [team-id project-id]
(let [data {:ids (set (map :id files))
:project-id project-id}
mdata {:on-success
#(do
(if multi?
(st/emit! (dm/success (tr "dashboard.success-move-files")))
(st/emit! (dm/success (tr "dashboard.success-move-file"))))
(if (or navigate? (not= team-id current-team-id))
(st/emit! (rt/nav :dashboard-files
{:team-id team-id
:project-id project-id}))
(st/emit! (dd/fetch-recent-files {:team-id team-id})
(dd/clear-selected-files))))}]
(st/emitf (dd/move-files (with-meta data mdata))))))
(fn [team-id project-id]
(let [data {:ids (set (map :id files))
:project-id project-id}
mdata {:on-success #(on-move-success team-id project-id)}]
(st/emitf (dd/move-files (with-meta data mdata)))))
add-shared
(mf/use-callback
(mf/deps file)
(st/emitf (dd/set-file-shared (assoc file :is-shared true))))
(st/emitf (dd/set-file-shared (assoc file :is-shared true)))
del-shared
(mf/use-callback
(mf/deps file)
(st/emitf (dd/set-file-shared (assoc file :is-shared false))))
(st/emitf (dd/set-file-shared (assoc file :is-shared false)))
on-add-shared
(mf/use-callback
(mf/deps file)
(fn [event]
(dom/stop-propagation event)
(st/emit! (modal/show
{:type :confirm
:message ""
:title (tr "modals.add-shared-confirm.message" (:name file))
:hint (tr "modals.add-shared-confirm.hint")
:cancel-label :omit
:accept-label (tr "modals.add-shared-confirm.accept")
:accept-style :primary
:on-accept add-shared}))))
(fn [event]
(dom/stop-propagation event)
(st/emit! (modal/show
{:type :confirm
:message ""
:title (tr "modals.add-shared-confirm.message" (:name file))
:hint (tr "modals.add-shared-confirm.hint")
:cancel-label :omit
:accept-label (tr "modals.add-shared-confirm.accept")
:accept-style :primary
:on-accept add-shared})))
on-del-shared
(mf/use-callback
(mf/deps file)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (modal/show
{:type :confirm
:message ""
:title (tr "modals.remove-shared-confirm.message" (:name file))
:hint (tr "modals.remove-shared-confirm.hint")
:cancel-label :omit
:accept-label (tr "modals.remove-shared-confirm.accept")
:on-accept del-shared}))))]
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (modal/show
{:type :confirm
:message ""
:title (tr "modals.remove-shared-confirm.message" (:name file))
:hint (tr "modals.remove-shared-confirm.hint")
:cancel-label :omit
:accept-label (tr "modals.remove-shared-confirm.accept")
:on-accept del-shared})))]
(mf/use-layout-effect
(mf/deps show?)
(fn []
(let [group-by-team (fn [projects]
(reduce
(fn [teams project]
(update teams (:team-id project)
#(if (nil? %)
{:id (:team-id project)
:name (:team-name project)
:is-default (:is-default-team project)
:projects [project]}
(update % :projects conj project))))
{}
projects))]
(if show?
(->> (rp/query! :all-projects)
(rx/map group-by-team)
(rx/subs #(reset! teams %)))
(reset! teams [])))))
(mf/use-effect
(fn []
(->> (rp/query! :all-projects)
(rx/map group-by-team)
(rx/subs #(reset! teams %)))))
(when current-team
(let [sub-options (conj (vec (for [project current-projects]
[(project-name project)
[(get-project-name project)
(on-move (:id current-team)
(:id project))]))
(when (seq other-teams)
[(tr "dashboard.move-to-other-team") nil
(for [team other-teams]
[(team-name team) nil
[(get-team-name team) nil
(for [sub-project (:projects team)]
[(project-name sub-project)
[(get-project-name sub-project)
(on-move (:id team)
(:id sub-project))])])]))
@ -214,4 +197,3 @@
:top top
:left left
:options options}]))))

View file

@ -8,6 +8,7 @@
(:require
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
@ -77,15 +78,11 @@
[:a.btn-secondary.btn-small {:on-click on-create-clicked}
(tr "dashboard.new-file")]]))
(defn files-ref
[project-id]
(l/derived (l/in [:files project-id]) st/state))
(mf/defc files-section
[{:keys [project team] :as props}]
(let [files-ref (mf/use-memo (mf/deps (:id project)) #(files-ref (:id project)))
files-map (mf/deref files-ref)
(let [files-map (mf/deref refs/dashboard-files)
files (->> (vals files-map)
(filter #(= (:id project) (:project-id %)))
(sort-by :modified-at)
(reverse))]

View file

@ -21,7 +21,7 @@
[app.main.worker :as wrk]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [t tr]]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[app.util.time :as dt]
@ -55,76 +55,66 @@
[{:keys [modified-at]}]
(let [locale (mf/deref i18n/locale)
time (dt/timeago modified-at {:locale locale})]
(str (t locale "ds.updated-at" time))))
(str (tr "ds.updated-at" time))))
(defn create-counter-element
[element file-count]
(let [counter-el (dom/create-element "div")]
(dom/set-property! counter-el "class" "drag-counter")
(dom/set-text! counter-el (str file-count))
counter-el))
(mf/defc grid-item
{:wrap [mf/memo]}
[{:keys [id file selected-files navigate?] :as props}]
(let [local (mf/use-state {:menu-open false
:menu-pos nil
:edition false})
locale (mf/deref i18n/locale)
item-ref (mf/use-ref)
menu-ref (mf/use-ref)
selected? (contains? selected-files id)
selected-file-objs
(deref refs/dashboard-selected-file-objs)
;; not needed to subscribe and repaint if changed
[{:keys [file navigate?] :as props}]
(let [file-id (:id file)
local (mf/use-state {:menu-open false
:menu-pos nil
:edition false})
selected-files (mf/deref refs/dashboard-selected-files)
item-ref (mf/use-ref)
menu-ref (mf/use-ref)
selected? (contains? selected-files file-id)
on-menu-close
(mf/use-callback
#(swap! local assoc :menu-open false))
on-select
(mf/use-callback
(mf/deps id selected? selected-files @local)
(fn [event]
(when (and (or (not selected?) (> (count selected-files) 1))
(not (:menu-open @local)))
(dom/stop-propagation event)
(let [shift? (kbd/shift? event)]
(when-not shift?
(st/emit! (dd/clear-selected-files)))
(st/emit! (dd/toggle-file-select {:file file}))))))
(fn [event]
(when (and (or (not selected?) (> (count selected-files) 1))
(not (:menu-open @local)))
(dom/stop-propagation event)
(let [shift? (kbd/shift? event)]
(when-not shift?
(st/emit! (dd/clear-selected-files)))
(st/emit! (dd/toggle-file-select file)))))
on-navigate
(mf/use-callback
(mf/deps id)
(mf/deps file)
(fn [event]
(let [menu-icon (mf/ref-val menu-ref)
target (dom/get-target event)]
(when-not (dom/child? target menu-icon)
(let [pparams {:project-id (:project-id file)
:file-id (:id file)}
qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav :workspace pparams qparams)))))))
create-counter
(mf/use-callback
(fn [element file-count]
(let [counter-el (dom/create-element "div")]
(dom/set-property! counter-el "class" "drag-counter")
(dom/set-text! counter-el (str file-count))
counter-el)))
(st/emit! (dd/go-to-workspace file))))))
on-drag-start
(mf/use-callback
(mf/deps selected-files)
(fn [event]
(let [offset (dom/get-offset-position (.-nativeEvent event))
(let [offset (dom/get-offset-position (.-nativeEvent event))
select-current? (not (contains? selected-files (:id file)))
item-el (mf/ref-val item-ref)
counter-el (create-counter item-el
(if select-current?
1
(count selected-files)))]
item-el (mf/ref-val item-ref)
counter-el (create-counter-element item-el
(if select-current?
1
(count selected-files)))]
(when select-current?
(st/emit! (dd/clear-selected-files))
(st/emit! (dd/toggle-file-select {:file file})))
(st/emit! (dd/toggle-file-select file)))
(dnd/set-data! event "penpot/files" "dummy")
(dnd/set-allowed-effect! event "move")
@ -135,7 +125,7 @@
;; afterwards, in the next render cycle.
(dom/append-child! item-el counter-el)
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
(ts/raf #(.removeChild item-el counter-el)))))
(ts/raf #(.removeChild ^js item-el counter-el)))))
on-menu-click
(mf/use-callback
@ -146,10 +136,11 @@
(let [shift? (kbd/shift? event)]
(when-not shift?
(st/emit! (dd/clear-selected-files)))
(st/emit! (dd/toggle-file-select {:file file}))))
(st/emit! (dd/toggle-file-select file))))
(let [position (dom/get-client-position event)]
(swap! local assoc :menu-open true
:menu-pos position))))
(swap! local assoc
:menu-open true
:menu-pos position))))
edit
(mf/use-callback
@ -168,19 +159,20 @@
:menu-open false)))]
(mf/use-effect
(mf/deps selected? local)
(fn []
(when (and (not selected?) (:menu-open @local))
(swap! local assoc :menu-open false))))
(mf/deps selected? local)
(fn []
(when (and (not selected?) (:menu-open @local))
(swap! local assoc :menu-open false))))
[:div.grid-item.project-th
{:class (dom/classnames :selected selected?)
:ref item-ref
:draggable true
:on-click on-select
:on-double-click on-navigate
:on-drag-start on-drag-start
:on-context-menu on-menu-click}
[:div.grid-item.project-th {:class (dom/classnames
:selected selected?)
:ref item-ref
:draggable true
:on-click on-select
:on-double-click on-navigate
:on-drag-start on-drag-start
:on-context-menu on-menu-click}
[:div.overlay]
[:& grid-item-thumbnail {:file file}]
(when (:is-shared file)
@ -198,7 +190,7 @@
:on-click on-menu-click}
i/actions
(when selected?
[:& file-menu {:files selected-file-objs
[:& file-menu {:files (vals selected-files)
:show? (:menu-open @local)
:left (+ 24 (:x (:menu-pos @local)))
:top (:y (:menu-pos @local))
@ -223,28 +215,24 @@
(mf/defc grid
[{:keys [id opts files] :as props}]
(let [locale (mf/deref i18n/locale)
selected-files (mf/deref refs/dashboard-selected-files)]
[:section.dashboard-grid
(cond
(nil? files)
[:& loading-placeholder]
[:section.dashboard-grid
(cond
(nil? files)
[:& loading-placeholder]
(seq files)
[:div.grid-row
(for [item files]
[:& grid-item
{:id (:id item)
:file item
:selected-files selected-files
:key (:id item)
:navigate? true}])]
(seq files)
[:div.grid-row
(for [item files]
[:& grid-item
{:file item
:key (:id item)
:navigate? true}])]
:else
[:& empty-placeholder])]))
:else
[:& empty-placeholder])])
(mf/defc line-grid-row
[{:keys [locale files team-id selected-files on-load-more dragging?] :as props}]
[{:keys [files team-id selected-files on-load-more dragging?] :as props}]
(let [rowref (mf/use-ref)
width (mf/use-state nil)
@ -294,12 +282,11 @@
[:div.grid-item.placeholder {:on-click on-load-more}
[:div.placeholder-icon i/arrow-down]
[:div.placeholder-label
(t locale "dashboard.show-all-files")]])]))
(tr "dashboard.show-all-files")]])]))
(mf/defc line-grid
[{:keys [project-id team-id opts files on-load-more] :as props}]
(let [locale (mf/deref i18n/locale)
dragging? (mf/use-state false)
(let [dragging? (mf/use-state false)
selected-files (mf/deref refs/dashboard-selected-files)
selected-project (mf/deref refs/dashboard-selected-project)
@ -327,6 +314,12 @@
(when-not (dnd/from-child? e)
(reset! dragging? false))))
on-drop-success
(fn []
(st/emit! (dm/success (tr "dashboard.success-move-file"))
(dd/fetch-recent-files)
(dd/clear-selected-files)))
on-drop
(mf/use-callback
(mf/deps files selected-files)
@ -335,11 +328,7 @@
(when (not= selected-project project-id)
(let [data {:ids selected-files
:project-id project-id}
mdata {:on-success
(st/emitf (dm/success (tr "dashboard.success-move-file"))
(dd/fetch-recent-files {:team-id team-id})
(dd/clear-selected-files))}]
mdata {:on-success on-drop-success}]
(st/emit! (dd/move-files (with-meta data mdata)))))))]
[:section.dashboard-grid {:on-drag-enter on-drag-enter
@ -355,8 +344,7 @@
:team-id team-id
:selected-files selected-files
:on-load-more on-load-more
:dragging? @dragging?
:locale locale}]
:dragging? @dragging?}]
:else
[:& empty-placeholder {:dragging? @dragging?}])]))

View file

@ -10,20 +10,16 @@
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
[app.main.ui.icons :as i]
[app.main.refs :as refs]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[okulary.core :as l]
[rumext.alpha :as mf]))
(defn files-ref
[team-id]
(l/derived (l/in [:shared-files team-id]) st/state))
(mf/defc libraries-page
[{:keys [team] :as props}]
(let [files-ref (mf/use-memo (mf/deps (:id team)) #(files-ref (:id team)))
files-map (mf/deref files-ref)
(let [files-map (mf/deref refs/dashboard-shared-files)
files (->> (vals files-map)
(sort-by :modified-at)
(reverse))]
@ -33,9 +29,11 @@
(dom/set-html-title (tr "title.dashboard.shared-libraries"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))))
(st/emit! (dd/fetch-shared-files {:team-id (:id team)})
(dd/clear-selected-files))))
(:name team))))))
(mf/use-effect
(st/emitf (dd/fetch-shared-files)
(dd/clear-selected-files)))
[:*
[:header.dashboard-header

View file

@ -11,8 +11,8 @@
[app.main.data.modal :as modal]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.context :as ctx]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[beicon.core :as rx]
@ -30,64 +30,53 @@
current-team-id (mf/use-ctx ctx/current-team-id)
teams (mf/use-state nil)
on-duplicate-success
(fn [new-project]
(st/emit! (dm/success (tr "dashboard.success-duplicate-project"))
(rt/nav :dashboard-files
{:team-id (:team-id new-project)
:project-id (:id new-project)})))
on-duplicate
(mf/use-callback
(mf/deps project)
#(let [on-success
(fn [new-project]
(st/emit! (dm/success (tr "dashboard.success-duplicate-project"))
(rt/nav :dashboard-files
{:team-id (:team-id new-project)
:project-id (:id new-project)})))]
(st/emit! (dd/duplicate-project
(with-meta project {:on-success on-success})))))
(fn []
(st/emit! (dd/duplicate-project
(with-meta project {:on-success on-duplicate-success}))))
toggle-pin
(mf/use-callback
(mf/deps project)
(st/emitf (dd/toggle-project-pin project)))
(st/emitf (dd/toggle-project-pin project))
on-move-success
(fn [team-id]
(st/emit! (dd/go-to-projects team-id)))
on-move
(mf/use-callback
(mf/deps project)
(fn [team-id]
(let [data {:id (:id project)
:team-id team-id}
mdata {:on-success
(st/emitf (rt/nav :dashboard-projects
{:team-id team-id}))}]
(fn [team-id]
(let [data {:id (:id project) :team-id team-id}
mdata {:on-success #(on-move-success team-id)}]
(st/emitf (dm/success (tr "dashboard.success-move-project"))
(dd/move-project (with-meta data mdata))))))
(dd/move-project (with-meta data mdata)))))
delete-fn
(mf/use-callback
(mf/deps project)
(fn [event]
(st/emit! (dm/success (tr "dashboard.success-delete-project"))
(dd/delete-project project)
(rt/nav :dashboard-projects {:team-id (:team-id project)}))))
(fn [event]
(st/emit! (dm/success (tr "dashboard.success-delete-project"))
(dd/delete-project project)
(dd/go-to-projects (:team-id project))))
on-delete
(mf/use-callback
(mf/deps project)
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.delete-project-confirm.title")
:message (tr "modals.delete-project-confirm.message")
:accept-label (tr "modals.delete-project-confirm.accept")
:on-accept delete-fn})))]
(st/emitf
(modal/show
{:type :confirm
:title (tr "modals.delete-project-confirm.title")
:message (tr "modals.delete-project-confirm.message")
:accept-label (tr "modals.delete-project-confirm.accept")
:on-accept delete-fn}))]
(mf/use-layout-effect
(mf/deps show?)
(fn []
(if show?
(->> (rp/query! :teams)
(rx/map (fn [teams]
(remove #(= (:id %) current-team-id) teams)))
(rx/subs #(reset! teams %)))
(reset! teams []))))
(mf/use-effect
(fn []
(->> (rp/query! :teams)
(rx/map (fn [teams]
(into [] (remove #(= (:id %) current-team-id)) teams)))
(rx/subs #(reset! teams %)))))
(when @teams
[:& context-menu {:on-close on-menu-close

View file

@ -25,45 +25,29 @@
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [locale team] :as props}]
(let [create #(st/emit! (dd/create-project {:team-id (:id team)}))]
[]
(let [create (st/emitf (dd/create-project))]
[:header.dashboard-header
[:div.dashboard-title
[:h1 (t locale "dashboard.projects-title")]]
[:h1 (tr "dashboard.projects-title")]]
[:a.btn-secondary.btn-small {:on-click create}
(t locale "dashboard.new-project")]]))
(defn files-ref
[project-id]
(l/derived (l/in [:files project-id]) st/state))
(defn recent-ref
[project-id]
(l/derived (l/in [:recent-files project-id]) st/state))
(tr "dashboard.new-project")]]))
(mf/defc project-item
[{:keys [project first? locale] :as props}]
(let [files-ref (mf/use-memo (mf/deps project) #(files-ref (:id project)))
recent-ref (mf/use-memo (mf/deps project) #(recent-ref (:id project)))
files-map (mf/deref files-ref)
recent-ids (mf/deref recent-ref)
files (some->> recent-ids
(map #(get files-map %))
(sort-by :modified-at)
(filter some?)
(reverse))
[{:keys [project first? files] :as props}]
(let [locale (mf/deref i18n/locale)
project-id (:id project)
team-id (:team-id project)
file-count (or (:count project) 0)
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)})
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)})
on-nav
(mf/use-callback
@ -145,20 +129,26 @@
[:a.btn-secondary.btn-small
{:on-click create-file}
(t locale "dashboard.new-file")]]
(tr "dashboard.new-file")]]
[:& line-grid
{:project-id (:id project)
:project project
:team-id team-id
:on-load-more on-nav
:files files}]]))
(def recent-files-ref
(l/derived :dashboard-recent-files st/state))
(mf/defc projects-section
[{:keys [team projects] :as props}]
(let [projects (->> (vals projects)
(sort-by :modified-at)
(reverse))
locale (mf/deref i18n/locale)]
(let [projects (->> (vals projects)
(sort-by :modified-at)
(reverse))
recent-map (mf/deref recent-files-ref)
files (vals recent-map)]
(mf/use-effect
(mf/deps team)
@ -166,18 +156,20 @@
(dom/set-html-title (tr "title.dashboard.projects"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))))
(st/emit! (dd/fetch-recent-files {:team-id (:id team)})
(dd/clear-selected-files))))
(:name team))))))
(mf/use-effect
(st/emitf (dd/fetch-recent-files)
(dd/clear-selected-files)))
(when (seq projects)
[:*
[:& header {:locale locale
:team team}]
[:& header]
[:section.dashboard-container
(for [project projects]
[:& project-item {:project project
:locale locale
:first? (= project (first projects))
:key (:id project)}])]])))
(for [{:keys [id] :as project} projects]
(let [files (some->> files (filterv #(= id (:project-id %))))]
[:& project-item {:project project
:files files
:first? (= project (first projects))
:key (:id project)}]))]])))

View file

@ -7,56 +7,52 @@
(ns app.main.ui.dashboard.search
(:require
[app.main.data.dashboard :as dd]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t]]
[app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
[rumext.alpha :as mf]))
(def result-ref
(l/derived (l/in [:dashboard-local :search-result]) st/state))
(mf/defc search-page
[{:keys [team search-term] :as props}]
(let [result (mf/deref result-ref)
locale (mf/deref i18n/locale)]
(let [result (mf/deref refs/dashboard-search-result)]
(mf/use-effect
(mf/deps team search-term)
(mf/deps team)
(fn []
(dom/set-html-title (t locale "title.dashboard.search"
(dom/set-html-title (tr "title.dashboard.search"
(if (:is-default team)
(t locale "dashboard.your-penpot")
(:name team))))
(when search-term
(st/emit! (dd/search-files {:team-id (:id team)
:search-term search-term})
(dd/clear-selected-files)))))
(tr "dashboard.your-penpot")
(:name team))))))
(mf/use-effect
(mf/deps search-term)
(fn []
(st/emit! (dd/search {:search-term search-term})
(dd/clear-selected-files))))
[:*
[:header.dashboard-header
[:div.dashboard-title
[:h1 (t locale "dashboard.title-search")]]]
[:h1 (tr "dashboard.title-search")]]]
[:section.dashboard-container.search
(cond
(empty? search-term)
[:div.grid-empty-placeholder
[:div.icon i/search]
[:div.text (t locale "dashboard.type-something")]]
[:div.text (tr "dashboard.type-something")]]
(nil? result)
[:div.grid-empty-placeholder
[:div.icon i/search]
[:div.text (t locale "dashboard.searching-for" search-term)]]
[:div.text (tr "dashboard.searching-for" search-term)]]
(empty? result)
[:div.grid-empty-placeholder
[:div.icon i/search]
[:div.text (t locale "dashboard.no-matches-for" search-term)]]
[:div.text (tr "dashboard.no-matches-for" search-term)]]
:else
[:& grid {:files result

View file

@ -56,8 +56,7 @@
(mf/use-callback
(mf/deps item)
(fn []
(st/emit! (rt/nav :dashboard-files {:team-id (:team-id item)
:project-id (:id item)}))))
(st/emit! (dd/go-to-files (:id item)))))
on-menu-click
(mf/use-callback
@ -103,21 +102,22 @@
(when-not (dnd/from-child? e)
(swap! local assoc :dragging? false))))
on-drop-success
(mf/use-callback
(mf/deps (:id item))
(st/emitf (dm/success (tr "dashboard.success-move-file"))
(dd/go-to-files (:id item))))
on-drop
(mf/use-callback
(mf/deps item selected-files)
(fn [e]
(swap! local assoc :dragging? false)
(when (not= selected-project (:id item))
(let [data {:ids selected-files
:project-id (:id item)}
mdata {:on-success
(st/emitf (dm/success (tr "dashboard.success-move-file"))
(rt/nav :dashboard-files
{:team-id team-id
:project-id (:id item)}))}]
(st/emit! (dd/move-files (with-meta data mdata)))))))]
(mf/deps item selected-files)
(fn [e]
(swap! local assoc :dragging? false)
(when (not= selected-project (:id item))
(let [data {:ids selected-files
:project-id (:id item)}
mdata {:on-success on-drop-success}]
(st/emit! (dd/move-files (with-meta data mdata)))))))]
[:*
[:li {:class (if selected? "current"
@ -151,12 +151,9 @@
(mf/deps team-id)
(fn [event]
(reset! focused? true)
(let [target (dom/get-target event)
value (dom/get-value target)]
(dom/select-text! target)
(if (empty? value)
(emit! (rt/nav :dashboard-search {:team-id team-id} {}))
(emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value}))))))
(let [value (dom/get-target-val event)]
(dom/select-text! (dom/get-target event))
(emit! (dd/go-to-search value)))))
on-search-blur
(mf/use-callback
@ -167,9 +164,8 @@
(mf/use-callback
(mf/deps team-id)
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value))]
(emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value})))))
(let [value (dom/get-target-val event)]
(emit! (dd/go-to-search value)))))
on-clear-click
(mf/use-callback
@ -178,7 +174,7 @@
(let [search-input (dom/get-element "search-input")]
(dom/clean-value! search-input)
(dom/focus! search-input)
(emit! (rt/nav :dashboard-search {:team-id team-id} {})))))]
(emit! (dd/go-to-search)))))]
[:form.sidebar-search
[:input.input-text
@ -213,9 +209,8 @@
team-selected
(mf/use-callback
(fn [team-id]
(du/set-current-team! team-id)
(st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))]
(fn [team-id]
(st/emit! (dd/go-to-projects team-id))))]
[:ul.dropdown.teams-dropdown
[:li.title (tr "dashboard.switch-team")]
@ -243,21 +238,17 @@
{::mf/register modal/components
::mf/register-as ::leave-and-reassign}
[{:keys [members profile team accept]}]
(let [form (fm/use-form :spec ::leave-modal-form :initial {})
not-current-user? (fn [{:keys [id]}] (not= id (:id profile)))
members (->> members (filterv not-current-user?))
options (into [{:value "" :label (tr "modals.leave-and-reassign.select-memeber-to-promote")}]
(map #(hash-map :label (:name %) :value (str (:id %))) members))
on-cancel
(mf/use-callback (st/emitf (modal/hide)))
(let [form (fm/use-form :spec ::leave-modal-form :initial {})
members (some->> members (filterv #(not= (:id %) (:id profile))))
options (into [{:value ""
:label (tr "modals.leave-and-reassign.select-memeber-to-promote")}]
(map #(hash-map :label (:name %) :value (str (:id %))) members))
on-cancel (st/emitf (modal/hide))
on-accept
(mf/use-callback
(mf/deps form)
(fn [event]
(let [member-id (get-in @form [:clean-data :member-id])]
(accept member-id))))]
(fn [event]
(let [member-id (get-in @form [:clean-data :member-id])]
(accept member-id)))]
[:div.modal-overlay
[:div.modal-container.confirm-dialog
@ -292,92 +283,60 @@
:value (tr "modals.leave-and-reassign.promote-and-leave")
:on-click on-accept}]]]]]))
(mf/defc team-options-dropdown
[{:keys [team profile] :as props}]
(let [members (mf/use-state [])
(let [go-members (st/emitf (dd/go-to-team-members))
go-settings (st/emitf (dd/go-to-team-settings))
go-members
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-team-members {:team-id (:id team)})))
go-settings
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-team-settings {:team-id (:id team)})))
members-map (mf/deref refs/dashboard-team-members)
members (vals members-map)
on-create-clicked
(mf/use-callback
(st/emitf (modal/show :team-form {})))
(st/emitf (modal/show :team-form {}))
on-rename-clicked
(mf/use-callback
(mf/deps team)
(st/emitf (modal/show :team-form {:team team})))
(st/emitf (modal/show :team-form {:team team}))
on-leaved-success
(mf/use-callback
(mf/deps team profile)
(fn []
(let [team-id (:default-team-id profile)]
(du/set-current-team! team-id)
(st/emit! (modal/hide)
(du/fetch-teams)
(rt/nav :dashboard-projects {:team-id team-id})))))
(fn []
(st/emit! (modal/hide)
(dd/go-to-projects (:default-team-id profile))))
leave-fn
(mf/use-callback
(mf/deps team)
(st/emitf (dd/leave-team (with-meta team {:on-success on-leaved-success}))))
(st/emitf (dd/leave-team (with-meta {} {:on-success on-leaved-success})))
leave-and-reassign-fn
(mf/use-callback
(mf/deps team)
(fn [member-id]
(let [team (assoc team :reassign-to member-id)]
(st/emit! (dd/leave-team (with-meta team {:on-success on-leaved-success}))))))
(fn [member-id]
(let [params {:reassign-to member-id}]
(st/emit! (dd/leave-team (with-meta params {:on-success on-leaved-success})))))
on-leave-clicked
(mf/use-callback
(mf/deps team)
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.leave-confirm.title")
:message (tr "modals.leave-confirm.message")
:accept-label (tr "modals.leave-confirm.accept")
:on-accept leave-fn})))
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.leave-confirm.title")
:message (tr "modals.leave-confirm.message")
:accept-label (tr "modals.leave-confirm.accept")
:on-accept leave-fn}))
on-leave-as-owner-clicked
(mf/use-callback
(mf/deps team @members)
(st/emitf (modal/show
{:type ::leave-and-reassign
:profile profile
:team team
:accept leave-and-reassign-fn
:members @members})))
(st/emitf (modal/show
{:type ::leave-and-reassign
:profile profile
:team team
:members members
:accept leave-and-reassign-fn}))
delete-fn
(mf/use-callback
(mf/deps team)
(st/emitf (dd/delete-team (with-meta team {:on-success on-leaved-success}))))
(st/emitf (dd/delete-team (with-meta team {:on-success on-leaved-success})))
on-delete-clicked
(mf/use-callback
(mf/deps team)
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.delete-team-confirm.title")
:message (tr "modals.delete-team-confirm.message")
:accept-label (tr "modals.delete-team-confirm.accept")
:on-accept delete-fn})))]
(mf/use-layout-effect
(mf/deps (:id team))
(fn []
(->> (rp/query! :team-members {:team-id (:id team)})
(rx/subs #(reset! members %)))))
(st/emitf
(modal/show
{:type :confirm
:title (tr "modals.delete-team-confirm.title")
:message (tr "modals.delete-team-confirm.message")
:accept-label (tr "modals.delete-team-confirm.accept")
:on-accept delete-fn}))]
[:ul.dropdown.options-dropdown
[:li {:on-click go-members} (tr "labels.members")]
@ -389,7 +348,7 @@
(:is-owner team)
[:li {:on-click on-leave-as-owner-clicked} (tr "dashboard.leave-team")]
(> (count @members) 1)
(> (count members) 1)
[:li {:on-click on-leave-clicked} (tr "dashboard.leave-team")])

View file

@ -32,22 +32,9 @@
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [section team] :as props}]
(let [go-members
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-team-members {:team-id (:id team)})))
go-settings
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-team-settings {:team-id (:id team)})))
invite-member
(mf/use-callback
(mf/deps team)
(st/emitf (modal/show {:type ::invite-member
:team team})))
(let [go-members (st/emitf (dd/go-to-team-members))
go-settings (st/emitf (dd/go-to-team-settings))
invite-member (st/emitf (modal/show {:type ::invite-member}))
members-section? (= section :dashboard-team-members)
settings-section? (= section :dashboard-team-settings)]
@ -69,6 +56,16 @@
(tr "dashboard.invite-profile")]
[:div])]))
(defn get-available-roles
[]
[{:value "" :label (tr "labels.role")}
{:value "admin" :label (tr "labels.admin")}
{:value "editor" :label (tr "labels.editor")}
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/uxboxproject/issue/1083
;; {:value "viewer" :label (tr "labels.viewer")}
])
(s/def ::email ::us/email)
(s/def ::role ::us/keyword)
(s/def ::invite-member-form
@ -77,53 +74,40 @@
(mf/defc invite-member-modal
{::mf/register modal/components
::mf/register-as ::invite-member}
[{:keys [team] :as props}]
(let [roles [{:value "" :label (tr "labels.role")}
{:value "admin" :label (tr "labels.admin")}
{:value "editor" :label (tr "labels.editor")}]
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/uxboxproject/issue/1083
;; {:value "viewer" :label (tr "labels.viewer")}]
initial (mf/use-memo (mf/deps team) (constantly {:team-id (:id team)
:role "editor"}))
[]
(let [roles (mf/use-memo get-available-roles)
initial (mf/use-memo (constantly {:role "editor"}))
form (fm/use-form :spec ::invite-member-form
:initial initial)
on-success
(mf/use-callback
(mf/deps team)
(st/emitf (dm/success (tr "notifications.invitation-email-sent"))
(modal/hide)))
(st/emitf (dm/success (tr "notifications.invitation-email-sent"))
(modal/hide))
on-error
(mf/use-callback
(mf/deps team)
(fn [form {:keys [type code] :as error}]
(let [email (get @form [:data :email])]
(cond
(and (= :validation type)
(= :profile-is-muted code))
(dm/error (tr "errors.profile-is-muted"))
(fn [form {:keys [type code] :as error}]
(let [email (get @form [:data :email])]
(cond
(and (= :validation type)
(= :profile-is-muted code))
(dm/error (tr "errors.profile-is-muted"))
(and (= :validation type)
(= :member-is-muted code))
(dm/error (tr "errors.member-is-muted"))
(and (= :validation type)
(= :member-is-muted code))
(dm/error (tr "errors.member-is-muted"))
(and (= :validation type)
(= :email-has-permanent-bounces))
(dm/error (tr "errors.email-has-permanent-bounces" email))
(and (= :validation type)
(= :email-has-permanent-bounces))
(dm/error (tr "errors.email-has-permanent-bounces" email))
:else
(dm/error (tr "errors.generic"))))))
:else
(dm/error (tr "errors.generic")))))
on-submit
(mf/use-callback
(mf/deps team)
(fn [form]
(let [params (:clean-data @form)
mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}]
(st/emit! (dd/invite-team-member (with-meta params mdata))))))]
(fn [form]
(let [params (:clean-data @form)
mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}]
(st/emit! (dd/invite-team-member (with-meta params mdata)))))]
[:div.modal.dashboard-invite-modal.form-container
[:& fm/form {:on-submit on-submit :form form}
@ -139,50 +123,39 @@
[:div.action-buttons
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))
(mf/defc team-member
{::mf/wrap [mf/memo]}
[{:keys [team member profile] :as props}]
(let [show? (mf/use-state false)
set-role
#(st/emit! (dd/update-team-member-role {:team-id (:id team)
:member-id (:id member)
:role %}))
set-owner-fn
(partial set-role :owner)
(fn [role]
(let [params {:member-id (:id member) :role role}]
(st/emit! (dd/update-team-member-role params))))
set-admin
(mf/use-callback (mf/deps team member) (partial set-role :admin))
set-editor
(mf/use-callback (mf/deps team member) (partial set-role :editor))
set-viewer
(mf/use-callback (mf/deps team member) (partial set-role :viewer))
set-owner-fn (partial set-role :owner)
set-admin (partial set-role :admin)
set-editor (partial set-role :editor)
set-viewer (partial set-role :viewer)
set-owner
(mf/use-callback
(mf/deps team member)
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.promote-owner-confirm.title")
:message (tr "modals.promote-owner-confirm.message")
:accept-label (tr "modals.promote-owner-confirm.accept")
:on-accept set-owner-fn})))
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.promote-owner-confirm.title")
:message (tr "modals.promote-owner-confirm.message")
:accept-label (tr "modals.promote-owner-confirm.accept")
:on-accept set-owner-fn}))
delete-fn
(st/emitf (dd/delete-team-member {:team-id (:id team) :member-id (:id member)}))
(st/emitf (dd/delete-team-member {:member-id (:id member)}))
delete
(mf/use-callback
(mf/deps team member)
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.delete-team-member-confirm.title")
:message (tr "modals.delete-team-member-confirm.message")
:accept-label (tr "modals.delete-team-member-confirm.accept")
:on-accept delete-fn})))]
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.delete-team-member-confirm.title")
:message (tr "modals.delete-team-member-confirm.message")
:accept-label (tr "modals.delete-team-member-confirm.accept")
:on-accept delete-fn}))]
[:div.table-row
[:div.table-field.name (:name member)]
@ -244,23 +217,21 @@
(for [item members]
[:& team-member {:member item :team team :profile profile :key (:id item)}])]]))
(defn- members-ref
[{:keys [id] :as team}]
(l/derived (l/in [:team-members id]) st/state))
(mf/defc team-members-page
[{:keys [team profile] :as props}]
(let [members-ref (mf/use-memo (mf/deps team) #(members-ref team))
members-map (mf/deref members-ref)]
(let [members-map (mf/deref refs/dashboard-team-members)]
(mf/use-effect
(mf/deps team)
(fn []
(dom/set-html-title (tr "title.team-members"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))))
(st/emit! (dd/fetch-team-members team))))
(dom/set-html-title
(tr "title.team-members"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))))))
(mf/use-effect
(st/emitf (dd/fetch-team-members)))
[:*
[:& header {:section :dashboard-team-members
@ -270,42 +241,35 @@
:team team
:members-map members-map}]]]))
(defn- stats-ref
[{:keys [id] :as team}]
(l/derived (l/in [:team-stats id]) st/state))
(mf/defc team-settings-page
[{:keys [team profile] :as props}]
(let [finput (mf/use-ref)
members-ref (mf/use-memo (mf/deps team) #(members-ref team))
members-map (mf/deref members-ref)
members-map (mf/deref refs/dashboard-team-members)
owner (->> (vals members-map)
(d/seek :is-owner))
stats-ref (mf/use-memo (mf/deps team) #(stats-ref team))
stats (mf/deref stats-ref)
stats (mf/deref refs/dashboard-team-stats)
on-image-click
(mf/use-callback #(dom/click (mf/ref-val finput)))
on-file-selected
(mf/use-callback
(mf/deps team)
(fn [file]
(st/emit! (dd/update-team-photo {:file file
:team-id (:id team)}))))]
(fn [file]
(st/emit! (dd/update-team-photo {:file file})))]
(mf/use-effect
(mf/deps team)
(fn []
(dom/set-html-title (tr "title.team-settings"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))))
(st/emit! (dd/fetch-team-members team)
(dd/fetch-team-stats team))))
(mf/deps team)
(fn []
(dom/set-html-title (tr "title.team-settings"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))))))
(mf/use-effect
(st/emitf (dd/fetch-team-members)
(dd/fetch-team-stats)))
[:*
[:& header {:section :dashboard-team-settings

View file

@ -100,8 +100,13 @@
(mf/use-layout-effect
(mf/deps page-id)
(fn []
(st/emit! (dw/initialize-page page-id))
(st/emitf (dw/finalize-page page-id))))
(if (nil? page-id)
(st/emit! (dw/go-to-page))
(st/emit! (dw/initialize-page page-id)))
(fn []
(when page-id
(st/emitf (dw/finalize-page page-id))))))
(when page
[:& workspace-content {:key page-id
@ -116,7 +121,6 @@
(mf/defc workspace
{::mf/wrap [mf/memo]}
[{:keys [project-id file-id page-id layout-name] :as props}]
(let [file (mf/deref refs/workspace-file)
project (mf/deref refs/workspace-project)
layout (mf/deref refs/workspace-layout)]