diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 9f0a0b3ff..066ee7e10 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -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 diff --git a/backend/src/app/rpc/queries/recent_files.clj b/backend/src/app/rpc/queries/recent_files.clj index 51c1bbe3f..e878d34e6 100644 --- a/backend/src/app/rpc/queries/recent_files.clj +++ b/backend/src/app/rpc/queries/recent_files.clj @@ -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 diff --git a/backend/tests/app/tests/test_services_files.clj b/backend/tests/app/tests/test_services_files.clj index 68f34eacb..248dc86f2 100644 --- a/backend/tests/app/tests/test_services_files.clj +++ b/backend/tests/app/tests/test_services_files.clj @@ -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) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 40f09051f..e69e39fe2 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -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})))))) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 3312f4c50..73142e4ae 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 2308d2ca7..69e19f32b 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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] diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index dbb4830be..b3a437835 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -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) diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs index 15c5c6c9b..08a86c2be 100644 --- a/frontend/src/app/main/data/workspace/path/changes.cljs +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -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))))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 627470a71..d48d681bd 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs index f0e752556..a63225870 100644 --- a/frontend/src/app/main/data/workspace/undo.cljs +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -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])) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 41eff4d51..9b828903c 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -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 diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 95437edd4..0bca30870 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 83a148a72..dc14df9cd 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -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}])])]])) diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 5b8f80644..f24a8d412 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -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}])))) - diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 4a5aca8d6..a43026964 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -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))] diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index d5822551c..df35fe7cf 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -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?}])])) diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index 77db054de..e61d4bcd4 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index fc2ace6f0..73cf15978 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index b75d0545c..41e5b895c 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -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)}]))]]))) diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index 2f9568e14..d3c6ed9ed 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index b9dc8446e..b35edc7e0 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -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")]) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 64eed4bee..168a944c2 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 6833c69d0..c21de7bea 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -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)]