diff --git a/backend/src/uxbox/services/queries/files.clj b/backend/src/uxbox/services/queries/files.clj index 2e93672cc..3b50e9429 100644 --- a/backend/src/uxbox/services/queries/files.clj +++ b/backend/src/uxbox/services/queries/files.clj @@ -28,6 +28,46 @@ (s/def ::project-id ::us/uuid) (s/def ::file-id ::us/uuid) (s/def ::profile-id ::us/uuid) +(s/def ::team-id ::us/uuid) +(s/def ::search-term ::us/string) + +;; --- Query: Files search + +(def ^:private sql:search-files + "with projects as ( + select p.* + from project as p + inner join team_profile_rel as tpr on (tpr.team_id = p.team_id) + where tpr.profile_id = $1 + and p.team_id = $2 + and p.deleted_at is null + and (tpr.is_admin = true or + tpr.is_owner = true or + tpr.can_edit = true) + union + select p.* + from project as p + inner join project_profile_rel as ppr on (ppr.project_id = p.id) + where ppr.profile_id = $1 + and p.team_id = $2 + and p.deleted_at is null + and (ppr.is_admin = true or + ppr.is_owner = true or + ppr.can_edit = true) + ) + select file.* + from file + inner join projects as pr on (file.project_id = pr.id) + where file.name ilike ('%' || $3 || '%') + order by file.created_at asc") + +(s/def ::search-files + (s/keys :req-un [::profile-id ::team-id ::search-term])) + +(sq/defquery ::search-files + [{:keys [profile-id team-id search-term] :as params}] + (-> (db/query db/pool [sql:search-files profile-id team-id search-term]) + (p/then (partial mapv decode-row)))) ;; --- Query: Draft Files diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index f3a51bed4..1ae0c5323 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -54,14 +54,38 @@ }, "unused" : true }, + "dashboard.search.no-matches-for" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/search.cljs:47" ], + "translations" : { + "en" : "No matches found for \"%s\"" + } + }, + "dashboard.search.results-for" : { + "translations" : { + "en" : "Search results for \"%s\"" + }, + "unused" : true + }, + "dashboard.search.searching-for" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/search.cljs:43" ], + "translations" : { + "en" : "Searching for %s..." + } + }, + "dashboard.search.type-something" : { + "used-in" : [ "src/uxbox/main/ui/dashboard/search.cljs:39" ], + "translations" : { + "en" : "Type to search results" + } + }, "dashboard.sidebar.drafts" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:112" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:113" ], "translations" : { "en" : "Drafts" } }, "dashboard.sidebar.libraries" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:117" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:118" ], "translations" : { "en" : "Libraries" } @@ -73,7 +97,7 @@ "unused" : true }, "dashboard.sidebar.recent" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:105" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:106" ], "translations" : { "en" : "Recent" } @@ -142,7 +166,7 @@ } }, "ds.default-library-title" : { - "used-in" : [ "src/uxbox/main/data/colors.cljs:68", "src/uxbox/main/data/icons.cljs:90", "src/uxbox/main/data/images.cljs:110" ], + "used-in" : [ "src/uxbox/main/data/icons.cljs:90", "src/uxbox/main/data/colors.cljs:68", "src/uxbox/main/data/images.cljs:110" ], "translations" : { "en" : "Unnamed Collection (%s)", "fr" : "Collection sans nom (%s)" @@ -220,7 +244,7 @@ } }, "ds.multiselect-bar.delete" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/colors.cljs:214", "src/uxbox/main/ui/dashboard/images.cljs:187", "src/uxbox/main/ui/dashboard/icons.cljs:221" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/icons.cljs:221", "src/uxbox/main/ui/dashboard/colors.cljs:214", "src/uxbox/main/ui/dashboard/images.cljs:187" ], "translations" : { "en" : "Delete", "fr" : "Supprimer" @@ -276,7 +300,7 @@ "unused" : true }, "ds.search.placeholder" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:140" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:168" ], "translations" : { "en" : "Search...", "fr" : "Rechercher..." @@ -332,7 +356,7 @@ } }, "ds.uploaded-at" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/images.cljs:271", "src/uxbox/main/ui/dashboard/icons.cljs:309" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/icons.cljs:309", "src/uxbox/main/ui/dashboard/images.cljs:271" ], "translations" : { "en" : "Uploaded at %s", "fr" : "Mise en ligne : %s" @@ -402,14 +426,14 @@ } }, "errors.generic" : { - "used-in" : [ "src/uxbox/main/ui.cljs:131" ], + "used-in" : [ "src/uxbox/main/ui.cljs:135" ], "translations" : { "en" : "Something wrong has happened.", "fr" : "Quelque chose c'est mal passé." } }, "errors.network" : { - "used-in" : [ "src/uxbox/main/ui.cljs:125" ], + "used-in" : [ "src/uxbox/main/ui.cljs:129" ], "translations" : { "en" : "Unable to connect to backend server.", "fr" : "Impossible de se connecter au serveur principal." @@ -514,7 +538,7 @@ } }, "profile.recovery.go-to-login" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:81", "src/uxbox/main/ui/profile/recovery_request.cljs:65" ], + "used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:65", "src/uxbox/main/ui/profile/recovery.cljs:81" ], "translations" : { "en" : "Go back!", "fr" : "Retour!" @@ -871,7 +895,7 @@ } }, "workspace.options.color" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:47", "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:124", "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:81" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:124", "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:47", "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:81" ], "translations" : { "en" : "Color", "fr" : "Couleur" @@ -913,7 +937,7 @@ } }, "workspace.options.measures" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:64", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:55", "src/uxbox/main/ui/workspace/sidebar/options/icon.cljs:66", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:62", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:66", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:69" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:69", "src/uxbox/main/ui/workspace/sidebar/options/icon.cljs:66", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:62", "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:64", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:55", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:66" ], "translations" : { "en" : "Size, position & rotation", "fr" : "Taille, position et rotation" @@ -927,21 +951,21 @@ } }, "workspace.options.position" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:92", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:84", "src/uxbox/main/ui/workspace/sidebar/options/icon.cljs:95", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:91", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:95", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:98" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:98", "src/uxbox/main/ui/workspace/sidebar/options/icon.cljs:95", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:91", "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:92", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:84", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:95" ], "translations" : { "en" : "Position", "fr" : "Position" } }, "workspace.options.rotation-radius" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:107", "src/uxbox/main/ui/workspace/sidebar/options/icon.cljs:112", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:108", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:112", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:115" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:115", "src/uxbox/main/ui/workspace/sidebar/options/icon.cljs:112", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:108", "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:107", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:112" ], "translations" : { "en" : "Rotation & Radius", "fr" : "TODO" } }, "workspace.options.size" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:68", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:57", "src/uxbox/main/ui/workspace/sidebar/options/icon.cljs:68", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:64", "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:114", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:68", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:71" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:114", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:71", "src/uxbox/main/ui/workspace/sidebar/options/icon.cljs:68", "src/uxbox/main/ui/workspace/sidebar/options/image.cljs:64", "src/uxbox/main/ui/workspace/sidebar/options/circle.cljs:68", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:57", "src/uxbox/main/ui/workspace/sidebar/options/rect.cljs:68" ], "translations" : { "en" : "Size", "fr" : "Taille" diff --git a/frontend/resources/styles/main/layouts/search-page.scss b/frontend/resources/styles/main/layouts/search-page.scss new file mode 100644 index 000000000..146e3269e --- /dev/null +++ b/frontend/resources/styles/main/layouts/search-page.scss @@ -0,0 +1,3 @@ +.search-page { + padding: 1rem; +} diff --git a/frontend/src/uxbox/main/data/dashboard.cljs b/frontend/src/uxbox/main/data/dashboard.cljs index bab495213..bdf5fc060 100644 --- a/frontend/src/uxbox/main/data/dashboard.cljs +++ b/frontend/src/uxbox/main/data/dashboard.cljs @@ -55,6 +55,23 @@ ;; Initialization ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(declare search-files) + +(defn initialize-search + [team-id search-term] + (ptk/reify ::initialize-search + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-local assoc + :search-result nil)) + + ptk/WatchEvent + (watch [_ state stream] + (let [local (:dashboard-local state)] + (when-not (empty? search-term) + (rx/of (search-files team-id search-term))))))) + + (declare fetch-files) (declare fetch-projects) (declare fetch-recent-files) @@ -135,6 +152,29 @@ (let [assoc-project #(assoc-in %1 [:projects (:id %2)] %2)] (reduce assoc-project state projects))))) +;; --- Search Files + +(declare files-searched) + +(defn search-files + [team-id search-term] + (us/assert ::us/uuid team-id) + (us/assert ::us/string search-term) + (ptk/reify ::search-files + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :search-files {:team-id team-id :search-term search-term}) + (rx/map files-searched))))) + +(defn files-searched + [files] + (us/verify (s/every ::file) files) + (ptk/reify ::files-searched + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-local assoc + :search-result files)))) + ;; --- Fetch Files (declare files-fetched) diff --git a/frontend/src/uxbox/main/repo.cljs b/frontend/src/uxbox/main/repo.cljs index 35c5fc368..da3628dde 100644 --- a/frontend/src/uxbox/main/repo.cljs +++ b/frontend/src/uxbox/main/repo.cljs @@ -47,7 +47,7 @@ request {:method method :url url :headers headers - :query-string (when query (encode-query query)) + :query query :body (if (map? body) (t/encode body) body)} options {:response-type response-type :credentials? true}] diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index 58d55926c..67ba8a166 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -58,8 +58,11 @@ ["/password" :settings-password]] ["/dashboard" - ["/:team-id" :dashboard-team] - ["/:team-id/:project-id" :dashboard-project]] + ["/team/:team-id" + ["/" :dashboard-team] + ["/search" :dashboard-search] + ["/project/:project-id" :dashboard-project] + ["/library" :dashboard-library]]] ["/workspace/:file-id" :workspace]]) @@ -84,7 +87,8 @@ :settings-password) (mf/element settings/settings #js {:route route}) - (:dashboard-team + (:dashboard-search + :dashboard-team :dashboard-project) (mf/element dashboard #js {:route route}) diff --git a/frontend/src/uxbox/main/ui/dashboard.cljs b/frontend/src/uxbox/main/ui/dashboard.cljs index 87438ed31..6e56b153e 100644 --- a/frontend/src/uxbox/main/ui/dashboard.cljs +++ b/frontend/src/uxbox/main/ui/dashboard.cljs @@ -18,6 +18,7 @@ [uxbox.main.refs :as refs] [uxbox.main.ui.dashboard.header :refer [header]] [uxbox.main.ui.dashboard.sidebar :refer [sidebar]] + [uxbox.main.ui.dashboard.search :refer [search-page]] [uxbox.main.ui.dashboard.project :refer [project-page]] [uxbox.main.ui.dashboard.recent-files :refer [recent-files-page]] [uxbox.main.ui.dashboard.profile :refer [profile-section]] @@ -30,9 +31,12 @@ (defn- parse-params [route profile] - (let [team-id (get-in route [:params :path :team-id]) + (let [search-term (get-in route [:params :query :search-term]) + team-id (get-in route [:params :path :team-id]) project-id (get-in route [:params :path :project-id])] - (cond-> {} + (cond-> + {:search-term search-term} + (uuid-str? team-id) (assoc :team-id (uuid team-id)) @@ -51,7 +55,7 @@ [{:keys [route] :as props}] (let [profile (mf/deref refs/profile) section (get-in route [:data :name]) - {:keys [team-id project-id]} (parse-params route profile)] + {:keys [search-term team-id project-id]} (parse-params route profile)] [:main.dashboard-main [:& messages-widget] [:section.dashboard-layout @@ -59,10 +63,14 @@ [:& profile-section {:profile profile}] [:& sidebar {:team-id team-id :project-id project-id + :search-term search-term :section section}] [:div.dashboard-content [:& header] (case section + :dashboard-search + (mf/element search-page #js {:team-id team-id :search-term search-term}) + :dashboard-team (mf/element recent-files-page #js {:team-id team-id}) diff --git a/frontend/src/uxbox/main/ui/dashboard/search.cljs b/frontend/src/uxbox/main/ui/dashboard/search.cljs new file mode 100644 index 000000000..9fdde9560 --- /dev/null +++ b/frontend/src/uxbox/main/ui/dashboard/search.cljs @@ -0,0 +1,51 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2015-2017 Juan de la Cruz +;; Copyright (c) 2015-2020 Andrey Antukh + +(ns uxbox.main.ui.dashboard.search + (:require + [lentes.core :as l] + [rumext.alpha :as mf] + [uxbox.main.store :as st] + [uxbox.main.data.dashboard :as dsh] + [uxbox.util.i18n :as i18n :refer [t]] + [uxbox.main.ui.dashboard.grid :refer [grid]])) + +;; --- Component: Search + +(def search-result-ref + (-> (l/in [:dashboard-local :search-result]) + (l/derive st/state))) + +(mf/defc search-page + [{:keys [team-id search-term] :as props}] + (let [search-result (mf/deref search-result-ref) + locale (i18n/use-locale)] + (mf/use-effect + {:fn #(st/emit! (dsh/initialize-search team-id search-term)) + :deps (mf/deps search-term)}) + [:section.search-page + [:section.dashboard-grid + [:div.dashboard-grid-content + (cond + (empty? search-term) + [:div.grid-files-empty + [:div.grid-files-desc (t locale "dashboard.search.type-something")]] + + (nil? search-result) + [:div.grid-files-empty + [:div.grid-files-desc (t locale "dashboard.search.searching-for" search-term)]] + + (empty? search-result) + [:div.grid-files-empty + [:div.grid-files-desc (t locale "dashboard.search.no-matches-for" search-term)]] + + :else + [:& grid { :files search-result :hide-new? true}])]]])) + diff --git a/frontend/src/uxbox/main/ui/dashboard/sidebar.cljs b/frontend/src/uxbox/main/ui/dashboard/sidebar.cljs index 7e6da96cd..5bdcc5deb 100644 --- a/frontend/src/uxbox/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/sidebar.cljs @@ -13,6 +13,7 @@ [cuerdas.core :as str] [lentes.core :as l] [rumext.alpha :as mf] + [goog.functions :as f] [uxbox.builtins.icons :as i] [uxbox.main.constants :as c] [uxbox.main.data.dashboard :as dsh] @@ -84,7 +85,7 @@ :team-id team-id }]))) -(mf/defc sidear-team +(mf/defc sidebar-team [{:keys [profile team-id selected-section @@ -125,21 +126,61 @@ :selected-project-id selected-project-id :team-id team-id}]])) + +(def debounced-emit! (f/debounce st/emit! 500)) + (mf/defc sidebar - [{:keys [section team-id project-id] :as props}] + [{:keys [section team-id project-id search-term] :as props}] (let [locale (i18n/use-locale) - profile (mf/deref refs/profile)] + profile (mf/deref refs/profile) + search-term-not-nil (or search-term "") + + on-search-focus + (fn [event] + (let [target (dom/get-target event) + value (dom/get-value target)] + (.select target) + (if (empty? value) + (debounced-emit! (rt/nav :dashboard-search {:team-id team-id} {})) + (debounced-emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value}))))) + + on-search-blur + (fn [event] + (let [target (dom/get-target event)] + (dom/clean-value! target) + (debounced-emit! (rt/nav :dashboard-team {:team-id team-id})))) + + on-search-change + (fn [event] + (let [value (-> (dom/get-target event) + (dom/get-value))] + (debounced-emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value})))) + + on-clear-click + (fn [event] + (let [search-input (dom/get-element "search-input")] + (dom/clean-value! search-input) + (.focus search-input) + (debounced-emit! (rt/nav :dashboard-search {:team-id team-id} {}))))] + [:div.library-bar [:div.library-bar-inside [:form.dashboard-search [:input.input-text {:key :images-search-box + :id "search-input" :type "text" - :auto-focus true - :placeholder (t locale "ds.search.placeholder")}] - [:div.clear-search i/close]] - [:& sidear-team {:selected-team-id team-id - :selected-project-id project-id - :selected-section section - :profile profile - :team-id "self"}]]])) + :placeholder (t locale "ds.search.placeholder") + :default-value search-term-not-nil + :autoComplete "off" + :on-focus on-search-focus + :on-blur on-search-blur + :on-change on-search-change}] + [:div.clear-search + {:on-click on-clear-click} + i/close]] + [:& sidebar-team {:selected-team-id team-id + :selected-project-id project-id + :selected-section section + :profile profile + :team-id "self"}]]]))