From a9882922531411af0dab9cec24d282fa9820c1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Wed, 17 Mar 2021 15:43:28 +0100 Subject: [PATCH] :tada: Multi select file menu --- frontend/resources/locales.json | 49 ++++++++ frontend/src/app/main/refs.cljs | 13 +- .../src/app/main/ui/components/dropdown.cljs | 5 +- .../src/app/main/ui/dashboard/file_menu.cljs | 115 +++++++++++------- frontend/src/app/main/ui/dashboard/grid.cljs | 41 ++++--- 5 files changed, 159 insertions(+), 64 deletions(-) diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 17197f5c5..f18cab16f 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -505,6 +505,13 @@ }, "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] }, + "dashboard.duplicate-multi" : { + "translations" : { + "en" : "Duplicate %s files", + "es" : "Duplicar %s archivos" + }, + "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] + }, "dashboard.empty-files" : { "translations" : { "ca" : "Encara no hi ha cap arxiu aquí", @@ -570,6 +577,13 @@ }, "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs", "src/app/main/ui/dashboard/file_menu.cljs" ] }, + "dashboard.move-to-multi" : { + "translations" : { + "en" : "Move %s files to", + "es" : "Mover %s archivos a" + }, + "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] + }, "dashboard.move-to-other-team" : { "translations" : { "de" : "Zu anderem Team verschieben", @@ -854,6 +868,13 @@ }, "used-in" : [ "src/app/main/ui/dashboard/project_menu.cljs" ] }, + "dashboard.success-move-files" : { + "translations" : { + "en" : "Your files has been moved successfully", + "es" : "Tus archivos han sido movidos con éxito" + }, + "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] + }, "dashboard.switch-team" : { "translations" : { "ca" : "Cambiar d'equip", @@ -2163,6 +2184,13 @@ }, "used-in" : [ "src/app/main/ui/comments.cljs" ] }, + "labels.delete-multi-files" : { + "translations" : { + "en" : "Delete %s files", + "es" : "Borrar %s archivos" + }, + "used-in" : [ "src/app/main/ui/dashboard/file-menu.cljs" ] + }, "labels.drafts" : { "translations" : { "de" : "Entwürfe", @@ -2882,6 +2910,27 @@ }, "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] }, + "modals.delete-file-multi-confirm.accept" : { + "translations" : { + "en" : "Delete files", + "es" : "Eliminar archivos" + }, + "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] + }, + "modals.delete-file-multi-confirm.message" : { + "translations" : { + "en" : "Are you sure you want to delete %s files?", + "es" : "¿Seguro que quieres eliminar %s archivos?" + }, + "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] + }, + "modals.delete-file-multi-confirm.title" : { + "translations" : { + "en" : "Deleting %s files", + "es" : "Eliminando %s archivos" + }, + "used-in" : [ "src/app/main/ui/dashboard/file_menu.cljs" ] + }, "modals.delete-page.body" : { "translations" : { "de" : "Sind Sie sicher, dass Sie diese Seite löschen wollen?", diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 73c52b381..a515463b6 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -40,14 +40,23 @@ (def dashboard-local (l/derived :dashboard-local st/state)) +(def dashboard-selected-project + (l/derived (fn [state] + (get-in state [:dashboard-local :selected-project])) + st/state)) + (def dashboard-selected-files (l/derived (fn [state] (get-in state [:dashboard-local :selected-files] #{})) st/state)) -(def dashboard-selected-project +(def dashboard-selected-file-objs (l/derived (fn [state] - (get-in state [:dashboard-local :selected-project])) + (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)) ;; ---- Workspace refs diff --git a/frontend/src/app/main/ui/components/dropdown.cljs b/frontend/src/app/main/ui/components/dropdown.cljs index b1efae131..b7bb1f2f2 100644 --- a/frontend/src/app/main/ui/components/dropdown.cljs +++ b/frontend/src/app/main/ui/components/dropdown.cljs @@ -35,8 +35,9 @@ (let [keys [(events/listen js/document EventType.CLICK on-click) (events/listen js/document EventType.CONTEXTMENU on-click) (events/listen js/document EventType.KEYUP on-keyup)]] - #(doseq [key keys] - (events/unlistenByKey key))))] + #(do (on-close) + (doseq [key keys] + (events/unlistenByKey key)))))] (mf/use-effect on-mount) children)) diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index fb36ab552..58113df79 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -23,8 +23,8 @@ [rumext.alpha :as mf])) (mf/defc file-menu - [{:keys [file show? on-edit on-menu-close top left navigate?] :as props}] - (assert (some? file) "missing `file` prop") + [{:keys [files show? on-edit on-menu-close top left navigate?] :as props}] + (assert (seq files) "missing `files` prop") (assert (boolean? show?) "missing `show?` prop") (assert (fn? on-edit) "missing `on-edit` prop") (assert (fn? on-menu-close) "missing `on-menu-close` prop") @@ -32,6 +32,10 @@ (let [top (or top 0) left (or left 0) + file (first files) + file-count (count files) + multi? (> file-count 1) + current-team-id (mf/use-ctx ctx/current-team-id) teams (mf/use-state nil) current-team (get @teams current-team-id) @@ -61,38 +65,49 @@ on-duplicate (mf/use-callback - (mf/deps file) - (st/emitf (dm/success (tr "dashboard.success-duplicate-file")) - (dd/duplicate-file file))) + (mf/deps files) + (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 file) - (st/emitf (dm/success (tr "dashboard.success-delete-file")) - (dd/delete-file file))) + (mf/deps files) + (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 file) + (mf/deps files) (fn [event] (dom/stop-propagation event) - (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})))) + (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}))))) on-move (mf/use-callback (mf/deps file) (fn [team-id project-id] - (let [data {:ids #{(:id file)} + (let [data {:ids (set (map :id files)) :project-id project-id} mdata {:on-success #(do - (st/emit! (dm/success (tr "dashboard.success-move-file"))) + (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 @@ -164,32 +179,42 @@ (reset! teams []))))) (when current-team - [:& context-menu {:on-close on-menu-close - :show show? - :fixed? (or (not= top 0) (not= left 0)) - :min-width? true - :top top - :left left - :options [[(tr "dashboard.open-in-new-tab") on-new-tab] - [(tr "labels.rename") on-edit] - [(tr "dashboard.duplicate") on-duplicate] - (when (or (seq current-projects) (seq other-teams)) - [(tr "dashboard.move-to") nil - (conj (vec (for [project current-projects] - [(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 - (for [sub-project (:projects team)] - [(project-name sub-project) - (on-move (:id team) - (:id sub-project))])])]))]) - (if (:is-shared file) - [(tr "dashboard.remove-shared") on-del-shared] - [(tr "dashboard.add-shared") on-add-shared]) - [:separator] - [(tr "labels.delete") on-delete]]}]))) + (let [sub-options (conj (vec (for [project current-projects] + [(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 + (for [sub-project (:projects team)] + [(project-name sub-project) + (on-move (:id team) + (:id sub-project))])])])) + + options (if multi? + [[(tr "dashboard.duplicate-multi" file-count) on-duplicate] + (when (or (seq current-projects) (seq other-teams)) + [(tr "dashboard.move-to-multi" file-count) nil sub-options]) + [:separator] + [(tr "labels.delete-multi-files" file-count) on-delete]] + + [[(tr "dashboard.open-in-new-tab") on-new-tab] + [(tr "labels.rename") on-edit] + [(tr "dashboard.duplicate") on-duplicate] + (when (or (seq current-projects) (seq other-teams)) + [(tr "dashboard.move-to") nil sub-options]) + (if (:is-shared file) + [(tr "dashboard.remove-shared") on-del-shared] + [(tr "dashboard.add-shared") on-add-shared]) + [:separator] + [(tr "labels.delete") on-delete]])] + + [:& context-menu {:on-close on-menu-close + :show show? + :fixed? (or (not= top 0) (not= left 0)) + :min-width? true + :top top + :left left + :options options}])))) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 2d548c14e..56c73d75c 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -72,20 +72,25 @@ 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 + on-menu-close (mf/use-callback #(swap! local assoc :menu-open false)) on-select (mf/use-callback - (mf/deps id) + (mf/deps id selected? selected-files @local) (fn [event] - (dom/prevent-default event) - (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}))))) + (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})))))) on-navigate (mf/use-callback @@ -138,9 +143,14 @@ on-menu-click (mf/use-callback - (mf/deps file) + (mf/deps file selected?) (fn [event] (dom/prevent-default event) + (when-not selected? + (let [shift? (kbd/shift? event)] + (when-not shift? + (st/emit! (dd/clear-selected-files))) + (st/emit! (dd/toggle-file-select {:file file})))) (let [position (dom/get-client-position event)] (swap! local assoc :menu-open true :menu-pos position)))) @@ -185,13 +195,14 @@ {:ref menu-ref :on-click on-menu-click} i/actions - [:& file-menu {:file file - :show? (:menu-open @local) - :left (:x (:menu-pos @local)) - :top (:y (:menu-pos @local)) - :navigate? navigate? - :on-edit on-edit - :on-menu-close on-menu-close}]]]])) + (when selected? + [:& file-menu {:files selected-file-objs + :show? (:menu-open @local) + :left (:x (:menu-pos @local)) + :top (:y (:menu-pos @local)) + :navigate? navigate? + :on-edit on-edit + :on-menu-close on-menu-close}])]]])) (mf/defc empty-placeholder [{:keys [dragging?] :as props}]