diff --git a/backend/src/app/services/queries/files.clj b/backend/src/app/services/queries/files.clj index 12b2764ea2..8047e37d91 100644 --- a/backend/src/app/services/queries/files.clj +++ b/backend/src/app/services/queries/files.clj @@ -185,39 +185,6 @@ (let [file (retrieve-file conn file-id)] (get-in file [:data :pages-index id])))) -;; --- Query: File users - -(def ^:private sql:file-users - "select pf.id, pf.fullname, pf.photo - from profile as pf - inner join file_profile_rel as fpr on (fpr.profile_id = pf.id) - where fpr.file_id = ? - union - select pf.id, pf.fullname, pf.photo - from profile as pf - inner join project_profile_rel as ppr on (ppr.profile_id = pf.id) - inner join file as f on (f.project_id = ppr.project_id) - where f.id = ? - union - select pf.id, pf.fullname, pf.photo - from profile as pf - inner join team_profile_rel as tpr on (tpr.profile_id = pf.id) - inner join project as p on (tpr.team_id = p.team_id) - inner join file as f on (p.id = f.project_id) - where f.id = ?") - -(defn retrieve-file-users - [conn id] - (db/exec! conn [sql:file-users id id id])) - -(s/def ::file-users - (s/keys :req-un [::profile-id ::id])) - -(sq/defquery ::file-users - [{:keys [profile-id id] :as params}] - (db/with-atomic [conn db/pool] - (check-edition-permissions! conn profile-id id) - (retrieve-file-users conn id))) ;; --- Query: Shared Library Files diff --git a/backend/src/app/services/queries/teams.clj b/backend/src/app/services/queries/teams.clj index 594ba5633b..e5191bca3f 100644 --- a/backend/src/app/services/queries/teams.clj +++ b/backend/src/app/services/queries/teams.clj @@ -131,8 +131,29 @@ [conn team-id] (db/exec! conn [sql:team-members team-id])) + ;; --- Query: Team Users +(declare retrieve-users) +(declare retrieve-team-for-file) + +(s/def ::file-id ::us/uuid) +(s/def ::team-users + (s/and (s/keys :req-un [::profile-id] + :opt-un [::team-id ::file-id]) + #(or (:team-id %) (:file-id %)))) + +(sq/defquery ::team-users + [{:keys [profile-id team-id file-id]}] + (with-open [conn (db/open)] + (if team-id + (do + (check-edition-permissions! conn profile-id team-id) + (retrieve-users conn team-id)) + (let [{team-id :id} (retrieve-team-for-file conn file-id)] + (check-edition-permissions! conn profile-id team-id) + (retrieve-users conn team-id))))) + ;; This is a similar query to team members but can contain more data ;; because some user can be explicitly added to project or file (not ;; implemented in UI) @@ -156,12 +177,38 @@ inner join project as p on (f.project_id = p.id) where p.team_id = ?") -(s/def ::team-users +(def sql:team-by-file + "select p.team_id as id + from project as p + join file as f on (p.id = f.project_id) + where f.id = ?") + +(defn retrieve-users + [conn team-id] + (db/exec! conn [sql:team-users team-id team-id team-id])) + +(defn retrieve-team-for-file + [conn file-id] + (->> [sql:team-by-file file-id] + (db/exec-one! conn))) + +;; --- Query: Team Stats + +(declare retrieve-team-stats) + +(s/def ::team-stats (s/keys :req-un [::profile-id ::team-id])) -(sq/defquery ::team-users +(sq/defquery ::team-stats [{:keys [profile-id team-id]}] (with-open [conn (db/open)] - (check-edition-permissions! conn profile-id team-id) - (db/exec! conn [sql:team-users team-id team-id team-id]))) + (check-read-permissions! conn profile-id team-id) + (retrieve-team-stats conn team-id))) +(def sql:team-stats + "select (select count(*) from project where team_id = ?) as projects, + (select count(*) from file as f join project as p on (p.id = f.project_id) where p.team_id = ?) as files") + +(defn retrieve-team-stats + [conn team-id] + (db/exec-one! conn [sql:team-stats team-id team-id])) diff --git a/backend/src/app/services/queries/viewer.clj b/backend/src/app/services/queries/viewer.clj index 0174f5b12d..38cd87221e 100644 --- a/backend/src/app/services/queries/viewer.clj +++ b/backend/src/app/services/queries/viewer.clj @@ -14,6 +14,7 @@ [app.db :as db] [app.services.queries :as sq] [app.services.queries.files :as files] + [app.services.queries.teams :as teams] [clojure.spec.alpha :as s])) ;; --- Query: Viewer Bundle (by Page ID) @@ -50,13 +51,14 @@ file (merge (dissoc file :data) (select-keys (:data file) [:colors :media :typographies])) libs (files/retrieve-file-libraries conn false file-id) - users (files/retrieve-file-users conn file-id) + users (teams/retrieve-users conn (:team-id project)) bundle {:file file :page page :users users :project project :libraries libs}] + (if (string? token) (do (check-shared-token! conn file-id page-id token) diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 30e1542718..1954e34091 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -460,6 +460,19 @@ "es" : "+ Nuevo proyecto" } }, + + "labels.num-of-projects" : { + "translations" : { + "en" : ["1 project", "%s projects"] + } + }, + + "labels.num-of-files" : { + "translations" : { + "en" : ["1 file", "%s files"] + } + }, + "dashboard.no-matches-for" : { "used-in" : [ "src/app/main/ui/dashboard/search.cljs:48" ], "translations" : { diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 862a90ba77..395503836f 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -11,6 +11,7 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.repo :as rp] + [app.main.data.users :as du] [app.util.router :as rt] [app.util.time :as dt] [app.util.timers :as ts] @@ -89,18 +90,14 @@ (->> (rp/query :team-members {:team-id id}) (rx/map #(partial fetched %))))))) - -(defn fetch-team-users - [{:keys [id] :as params}] +(defn fetch-team-stats + [{:keys [id] :as team}] (us/assert ::us/uuid id) - (letfn [(fetched [users state] - (->> (map #(avatars/assoc-avatar % :fullname) users) - (d/index-by :id) - (assoc-in state [:team-users id])))] - (ptk/reify ::fetch-team-users - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :team-users {:team-id 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 %))))))) ;; --- Fetch Projects @@ -125,7 +122,7 @@ (let [profile (:profile state)] (->> (rx/merge (ptk/watch (fetch-team params) state stream) (ptk/watch (fetch-projects {:team-id id}) state stream) - (ptk/watch (fetch-team-users params) state stream)) + (ptk/watch (du/fetch-users {:team-id id}) state stream)) (rx/catch (fn [{:keys [type code] :as error}] (cond (and (= :not-found type) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 56b624ae98..31da7e8a38 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -2,25 +2,29 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2016-2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL (ns app.main.data.users (:require + [app.config :as cfg] + [app.common.data :as d] + [app.common.spec :as us] + [app.main.data.media :as di] + [app.main.data.messages :as dm] + [app.main.repo :as rp] + [app.main.store :as st] + [app.util.avatars :as avatars] + [app.util.i18n :as i18n :refer [tr]] + [app.util.router :as rt] + [app.util.storage :refer [storage]] + [app.util.theme :as theme] [beicon.core :as rx] [cljs.spec.alpha :as s] [cuerdas.core :as str] - [potok.core :as ptk] - [app.common.spec :as us] - [app.config :as cfg] - [app.main.store :as st] - [app.main.repo :as rp] - [app.main.data.messages :as dm] - [app.main.data.media :as di] - [app.util.router :as rt] - [app.util.i18n :as i18n :refer [tr]] - [app.util.storage :refer [storage]] - [app.util.avatars :as avatars] - [app.util.theme :as theme])) + [potok.core :as ptk])) ;; --- Common Specs @@ -179,3 +183,18 @@ (rx/map (constantly fetch-profile)) (rx/catch on-error)))))) + +(defn fetch-users + [{:keys [team-id] :as params}] + (us/assert ::us/uuid team-id) + (letfn [(fetched [users state] + (->> (map #(avatars/assoc-avatar % :fullname) users) + (d/index-by :id) + (assoc state :users)))] + (ptk/reify ::fetch-team-users + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :team-users {:team-id team-id}) + (rx/map #(partial fetched %))))))) + + diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index df467de3f1..71ee25648d 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -203,7 +203,6 @@ :workspace-file :workspace-project :workspace-media-objects - :workspace-users :workspace-persistence)) ptk/WatchEvent diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index a0ae74a6b5..28008bdfc5 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -163,7 +163,7 @@ (ptk/reify ::handle-presence ptk/UpdateEvent (update [_ state] - (let [profiles (:workspace-users state)] + (let [profiles (:users state)] (update state :workspace-presence update-sessions profiles)))))) (defn handle-pointer-update diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index c36c702c54..098464d405 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -207,7 +207,7 @@ ptk/WatchEvent (watch [_ state stream] (->> (rx/zip (rp/query :file {:id file-id}) - (rp/query :file-users {:id file-id}) + (rp/query :team-users {:file-id file-id}) (rp/query :project {:id project-id}) (rp/query :file-libraries {:file-id file-id})) (rx/first) @@ -238,11 +238,11 @@ (update [_ state] (let [users (map avatars/assoc-profile-avatar users)] (assoc state + :users (d/index-by :id users) :workspace-undo {} :workspace-project project :workspace-file file :workspace-data (:data file) - :workspace-users (d/index-by :id users) :workspace-libraries (d/index-by :id libraries)))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 58fa20a6ed..124235bbbd 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -122,9 +122,6 @@ (def workspace-libraries (l/derived :workspace-libraries st/state)) -(def workspace-users - (l/derived :workspace-users st/state)) - (def workspace-presence (l/derived :workspace-presence st/state)) @@ -214,3 +211,6 @@ (def comments-local (l/derived :comments-local st/state)) +(def users + (l/derived :users st/state)) + diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs index 3febd79e1d..81aeb1c1c5 100644 --- a/frontend/src/app/main/ui/dashboard/comments.cljs +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -35,14 +35,8 @@ [cuerdas.core :as str] [rumext.alpha :as mf])) - -(defn team-members-ref - [{:keys [id] :as team}] - (l/derived (l/in [:team-users id]) st/state)) - (mf/defc comments-section [{:keys [profile team]}] - (mf/use-effect (mf/deps team) (st/emitf (dcm/retrieve-unread-comment-threads (:id team)))) @@ -51,9 +45,7 @@ show-dropdown (mf/use-fn #(reset! show-dropdown? true)) hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) threads-map (mf/deref refs/comment-threads) - - users-ref (mf/use-memo (mf/deps team) #(team-members-ref team)) - users (mf/deref users-ref) + users (mf/deref refs/users) tgroups (->> (vals threads-map) (sort-by :modified-at) @@ -61,7 +53,6 @@ (dcm/apply-filters {} profile) (dcm/group-threads-by-file-and-page)) - on-navigate (mf/use-callback (fn [thread] diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 02cee28008..407a3d239c 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -25,7 +25,7 @@ [app.main.ui.dashboard.team-form] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.time :as dt] [cljs.spec.alpha :as s] @@ -34,7 +34,7 @@ (mf/defc header {::mf/wrap [mf/memo]} - [{:keys [section locale team] :as props}] + [{:keys [section team] :as props}] (let [go-members (mf/use-callback (mf/deps team) @@ -57,19 +57,19 @@ [:header.dashboard-header [:div.dashboard-title [:h1 (cond - members-section? (t locale "labels.members") - settings-section? (t locale "labels.settings") + members-section? (tr "labels.members") + settings-section? (tr "labels.settings") nil)]] [:nav [:ul [:li {:class (when members-section? "active")} - [:a {:on-click go-members} (t locale "labels.members")]] + [:a {:on-click go-members} (tr "labels.members")]] [:li {:class (when settings-section? "active")} - [:a {:on-click go-settings} (t locale "labels.settings")]]]] + [:a {:on-click go-settings} (tr "labels.settings")]]]] (if members-section? [:a.btn-secondary.btn-small {:on-click invite-member} - (t locale "dashboard.invite-profile")] + (tr "dashboard.invite-profile")] [:div])])) (s/def ::email ::us/email) @@ -220,13 +220,12 @@ [:& team-member {:member item :team team :profile profile :key (:id item)}])]])) (defn- members-ref - [team-id] - (l/derived (l/in [:team-members team-id]) st/state)) + [{:keys [id] :as team}] + (l/derived (l/in [:team-members id]) st/state)) (mf/defc team-members-page [{:keys [team profile] :as props}] - (let [locale (mf/deref i18n/locale) - members-ref (mf/use-memo (mf/deps team) #(members-ref (:id team))) + (let [members-ref (mf/use-memo (mf/deps team) #(members-ref team)) members-map (mf/deref members-ref)] (mf/use-effect @@ -234,24 +233,30 @@ (st/emitf (dd/fetch-team-members team))) [:* - [:& header {:locale locale - :section :dashboard-team-members + [:& header {:section :dashboard-team-members :team team}] [:section.dashboard-container.dashboard-team-members - [:& team-members {:locale locale - :profile profile + [:& team-members {:profile profile :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 [locale (mf/deref i18n/locale) - finput (mf/use-ref) + (let [finput (mf/use-ref) - members-ref (mf/use-memo (mf/deps team) #(members-ref (:id team))) + members-ref (mf/use-memo (mf/deps team) #(members-ref team)) members-map (mf/deref members-ref) + owner (->> (vals members-map) + (d/seek :is-owner)) + + stats-ref (mf/use-memo (mf/deps team) #(stats-ref team)) + stats (mf/deref stats-ref) + on-image-click (mf/use-callback #(dom/click (mf/ref-val finput))) @@ -264,17 +269,17 @@ (mf/use-effect (mf/deps team) - (st/emitf (dd/fetch-team-members team))) + (st/emitf (dd/fetch-team-members team) + (dd/fetch-team-stats team))) [:* - [:& header {:locale locale - :section :dashboard-team-settings + [:& header {:section :dashboard-team-settings :team team}] [:section.dashboard-container.dashboard-team-settings [:div.team-settings [:div.horizontal-blocks [:div.block.info-block - [:div.label (t locale "dashboard.team-info")] + [:div.label (tr "dashboard.team-info")] [:div.name (:name team)] [:div.icon [:span.update-overlay {:on-click on-image-click} i/exit] @@ -285,19 +290,19 @@ :on-selected on-file-selected}]]] [:div.block.owner-block - [:div.label (t locale "dashboard.team-members")] + [:div.label (tr "dashboard.team-members")] [:div.owner - [:span.icon [:img {:src (cfg/resolve-media-path (:photo-uri profile))}]] - [:span.text (str (:fullname profile) " (" (t locale "labels.owner") ")") ]] + [:span.icon [:img {:src (cfg/resolve-media-path (:photo owner))}]] + [:span.text (str (:name owner) " (" (tr "labels.owner") ")") ]] [:div.summary [:span.icon i/user] - [:span.text (t locale "dashboard.num-of-members" (count members-map))]]] + [:span.text (tr "dashboard.num-of-members" (count members-map))]]] [:div.block.stats-block - [:div.label (t locale "dashboard.team-projects")] + [:div.label (tr "dashboard.team-projects")] [:div.projects [:span.icon i/folder] - [:span.text "4 projects"]] + [:span.text (tr "labels.num-of-projects" (i18n/c (:projects stats)))]] [:div.files [:span.icon i/file-html] - [:span.text "4 files"]]]]]]])) + [:span.text (tr "labels.num-of-files" (i18n/c (:files stats)))]]]]]]])) diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index a753f60ae4..f7cc52b524 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -36,7 +36,7 @@ pos-y (* (- (:y vbox)) zoom) profile (mf/deref refs/profile) - users (mf/deref refs/workspace-users) + users (mf/deref refs/users) local (mf/deref refs/comments-local) threads-map (mf/deref threads-ref) @@ -132,7 +132,7 @@ [] (let [threads-map (mf/deref threads-ref) profile (mf/deref refs/profile) - users (mf/deref refs/workspace-users) + users (mf/deref refs/users) local (mf/deref refs/comments-local) options? (mf/use-state false)