diff --git a/frontend/resources/styles/main.scss b/frontend/resources/styles/main.scss index 17dc7cdc7..d6748e947 100644 --- a/frontend/resources/styles/main.scss +++ b/frontend/resources/styles/main.scss @@ -66,6 +66,7 @@ @import 'main/partials/loader'; @import 'main/partials/context-menu'; @import 'main/partials/debug-icons-preview'; +@import 'main/partials/editable-label'; //################################################# // Resources diff --git a/frontend/resources/styles/main/layouts/library-page.scss b/frontend/resources/styles/main/layouts/library-page.scss index 3ba00eb4b..34b0c29c1 100644 --- a/frontend/resources/styles/main/layouts/library-page.scss +++ b/frontend/resources/styles/main/layouts/library-page.scss @@ -20,6 +20,19 @@ } } +.library-content-empty { + display: flex; + flex-direction: column; +} + +.library-content-empty-text { + color: #7C7C7C; + border: 1px dashed #AFB2BF; + text-align: center; + padding: 5rem; + margin: 2rem; +} + .library-page #main-bar { position: relative; } @@ -79,16 +92,13 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - - & a { - color: $color-black; - } + color: $color-black; &:hover { background-color: $color-primary-lighter; } - &.current a { + &.current { font-weight: bold; } } @@ -145,6 +155,12 @@ } } +.library-top-menu-actions-delete { + display: flex; + justify-content: center; + flex-direction: column +} + .library-page-cards-container { align-content: flex-start; display: flex; @@ -335,3 +351,4 @@ font-size: 24px; font-weight: normal; } + diff --git a/frontend/resources/styles/main/partials/editable-label.scss b/frontend/resources/styles/main/partials/editable-label.scss new file mode 100644 index 000000000..83f80cbf7 --- /dev/null +++ b/frontend/resources/styles/main/partials/editable-label.scss @@ -0,0 +1,30 @@ +.editable-label { + display: flex; + + &.is-hidden { + display: none; + } +} + +.editable-label-input { + border: 0; + height: 30px; + padding: 5px; + margin: 0; + width: 100%; + background-color: $color-white; +} + +.editable-label-close { + background-color: $color-white; + cursor: pointer; + padding: 3px 5px; + + & svg { + fill: $color-gray; + height: 15px; + transform: rotate(45deg) translateY(7px); + width: 15px; + margin: 0; + } +} diff --git a/frontend/src/uxbox/main/data/colors.cljs b/frontend/src/uxbox/main/data/colors.cljs index 4291bd61d..cefca0b53 100644 --- a/frontend/src/uxbox/main/data/colors.cljs +++ b/frontend/src/uxbox/main/data/colors.cljs @@ -318,12 +318,12 @@ (->> (rp/mutation! :create-color {:library-id library-id :content color :name color}) - (rx/map create-color-result))))) + (rx/map (partial create-color-result library-id)))))) (defn create-color-result - [item] + [library-id item] (ptk/reify ::create-color-result ptk/UpdateEvent (update [_ state] (-> state - (update-in [:library :selected-items] #(into [item] %) ))))) + (update-in [:library :selected-items library-id] #(into [item] %) ))))) diff --git a/frontend/src/uxbox/main/data/icons.cljs b/frontend/src/uxbox/main/data/icons.cljs index e5cc123fa..9866e9246 100644 --- a/frontend/src/uxbox/main/data/icons.cljs +++ b/frontend/src/uxbox/main/data/icons.cljs @@ -94,7 +94,8 @@ (-> state (update-in [:library :icon-libraries] #(into [result] %)))))) - +;; rename-icon-library +;; delete-icon-library ;; (declare fetch-icons) ;; @@ -253,16 +254,16 @@ (rx/merge-map parse) (rx/map prepare) (rx/flat-map #(rp/mutation! :create-icon %)) - (rx/map create-icon-result)))))) + (rx/map (partial create-icon-result library-id))))))) (defn create-icon-result - [item] + [library-id item] (ptk/reify ::create-icon-result ptk/UpdateEvent (update [_ state] (let [{:keys [id] :as item} (assoc item :type :icon)] (-> state - (update-in [:library :selected-items] #(into [item] %))))))) + (update-in [:library :selected-items library-id] #(into [item] %))))))) ;; ;; --- Icon Persisted ;; diff --git a/frontend/src/uxbox/main/data/images.cljs b/frontend/src/uxbox/main/data/images.cljs index 4d3cd53fe..2b8b2cb54 100644 --- a/frontend/src/uxbox/main/data/images.cljs +++ b/frontend/src/uxbox/main/data/images.cljs @@ -444,17 +444,17 @@ (rx/reduce conj []) (rx/do on-success) (rx/mapcat identity) - (rx/map create-images-result) + (rx/map (partial create-images-result library-id)) (rx/catch on-error))))))) ;; --- Image Created (defn create-images-result - [item] + [library-id item] #_(us/verify ::image item) (ptk/reify ::create-images-result ptk/UpdateEvent (update [_ state] (-> state - (update-in [:library :selected-items] #(into [item] %)))))) + (update-in [:library :selected-items library-id] #(into [item] %)))))) diff --git a/frontend/src/uxbox/main/data/library.cljs b/frontend/src/uxbox/main/data/library.cljs new file mode 100644 index 000000000..34192ed3e --- /dev/null +++ b/frontend/src/uxbox/main/data/library.cljs @@ -0,0 +1,175 @@ +(ns uxbox.main.data.library + (:require + [cljs.spec.alpha :as s] + [beicon.core :as rx] + [cuerdas.core :as str] + [potok.core :as ptk] + [uxbox.common.spec :as us] + [uxbox.common.data :as d] + [uxbox.main.repo :as rp] + [uxbox.main.store :as st] + [uxbox.util.dom :as dom] + [uxbox.util.webapi :as wapi] + [uxbox.util.i18n :as i18n :refer [t tr]] + [uxbox.util.router :as r] + [uxbox.util.uuid :as uuid])) + + +;; Retrieve libraries + +(declare retrieve-libraries-result) + +(defn retrieve-libraries + [type team-id] + (s/assert ::us/uuid team-id) + (let [method (case type + :icons :icon-libraries + :images :image-libraries + :palettes :color-libraries)] + (ptk/reify ::retrieve-libraries + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query! method {:team-id team-id}) + (rx/map (partial retrieve-libraries-result type))))))) + +(defn retrieve-libraries-result [type result] + (ptk/reify ::retrieve-libraries-result + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:library type] result))))) + +;; Retrieve library data + +(declare retrieve-library-data-result) + +(defn retrieve-library-data + [type library-id] + (ptk/reify ::retrieve-library-data + ptk/WatchEvent + (watch [_ state stream] + (let [method (case type + :icons :icons + :images :images + :palettes :colors)] + (->> (rp/query! method {:library-id library-id}) + (rx/map (partial retrieve-library-data-result library-id))))))) + +(defn retrieve-library-data-result + [library-id data] + (ptk/reify ::retrieve-library-data-result + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:library :selected-items library-id] data))))) + + +;; Create library + +(declare create-library-result) + +(defn create-library + [type team-id name] + (ptk/reify ::create-library + ptk/WatchEvent + (watch [_ state stream] + (let [method (case type + :icons :create-icon-library + :images :create-image-library + :palettes :create-color-library)] + (->> (rp/mutation! method {:team-id team-id + :name name}) + (rx/map (partial create-library-result type))))))) + +(defn create-library-result + [type result] + (ptk/reify ::create-library-result + ptk/UpdateEvent + (update [_ state] + (-> state + (update-in [:library type] #(into [result] %)))))) + +;; Rename library + +(declare rename-library-result) + +(defn rename-library + [type library-id name] + (ptk/reify ::rename-library + ptk/WatchEvent + (watch [_ state stream] + (let [method (case type + :icons :rename-icon-library + :images :rename-image-library + :palettes :rename-color-library)] + (->> (rp/mutation! method {:id library-id + :name name}) + (rx/map #(rename-library-result type library-id name))))))) + +(defn rename-library-result + [type library-id name] + (ptk/reify ::rename-library-result + ptk/UpdateEvent + (update [_ state] + (letfn [(change-name + [library] (if (= library-id (:id library)) + (assoc library :name name) + library)) + (update-fn [libraries] (map change-name libraries))] + + (-> state + (update-in [:library type] update-fn)))))) + +;; Delete library + +(declare delete-library-result) + +(defn delete-library + [type library-id] + (ptk/reify ::delete-library + ptk/WatchEvent + (watch [_ state stream] + (let [method (case type + :icons :delete-icon-library + :images :delete-image-library + :palettes :delete-color-library)] + (->> (rp/mutation! method {:id library-id}) + (rx/map #(delete-library-result type library-id))))))) + +(defn delete-library-result + [type library-id] + (ptk/reify ::create-library-result + ptk/UpdateEvent + (update [_ state] + (let [update-fn (fn [libraries] + (filter #(not= library-id (:id %)) libraries))] + (-> state + (update-in [:library type] update-fn)))))) + +;; Delete library item + +(declare delete-item-result) + +(defn delete-item + [type library-id item-id] + (ptk/reify ::delete-item + ptk/WatchEvent + (watch [_ state stream] + (let [method (case type + :icons :delete-icon + :images :delete-image + :palettes :delete-color)] + (->> (rp/mutation! method {:id item-id}) + (rx/map #(delete-item-result type library-id item-id))))))) + +(defn delete-item-result + [type library-id item-id] + (ptk/reify ::delete-item-result + ptk/UpdateEvent + (update [_ state] + (let [update-fn (fn [items] + (filter #(not= item-id (:id %)) items))] + (-> state + (update-in [:library :selected-items library-id] update-fn)))))) + +;; Rename library item diff --git a/frontend/src/uxbox/main/ui/components/editable_label.cljs b/frontend/src/uxbox/main/ui/components/editable_label.cljs new file mode 100644 index 000000000..2f1f23e7a --- /dev/null +++ b/frontend/src/uxbox/main/ui/components/editable_label.cljs @@ -0,0 +1,41 @@ +(ns uxbox.main.ui.components.editable-label + (:require + [rumext.alpha :as mf] + [uxbox.builtins.icons :as i] + [uxbox.main.ui.keyboard :as kbd] + [uxbox.util.dom :as dom] + [uxbox.util.data :refer [classnames]])) + +(mf/defc editable-label + [{:keys [ value on-change on-cancel edit readonly class-name]}] + (let [input (mf/use-ref nil) + state (mf/use-state (:editing false)) + is-editing (or edit (:editing @state)) + start-editing (fn [] + (swap! state assoc :editing true) + (dom/timeout 100 #(dom/focus! (mf/ref-node input)))) + stop-editing (fn [] (swap! state assoc :editing false)) + cancel-editing (fn [] + (stop-editing) + (when on-cancel (on-cancel))) + on-dbl-click (fn [e] (when (not readonly) (start-editing))) + on-key-up (fn [e] + (cond + (kbd/esc? e) + (cancel-editing) + + (kbd/enter? e) + (let [value (-> e dom/get-target dom/get-value)] + (on-change value) + (stop-editing)))) + ] + + (if is-editing + [:div.editable-label {:class class-name} + [:input.editable-label-input {:ref input + :default-value value + :on-key-down on-key-up}] + [:span.editable-label-close {:on-click cancel-editing} i/close]] + [:span.editable-label {:class class-name + :on-double-click on-dbl-click} value] + ))) diff --git a/frontend/src/uxbox/main/ui/dashboard/library.cljs b/frontend/src/uxbox/main/ui/dashboard/library.cljs index dd9ef5227..d4f7ac3f2 100644 --- a/frontend/src/uxbox/main/ui/dashboard/library.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/library.cljs @@ -17,6 +17,7 @@ [uxbox.util.i18n :as i18n :refer [t tr]] [uxbox.util.color :as uc] [uxbox.util.dom :as dom] + [uxbox.main.data.library :as dlib] [uxbox.main.data.icons :as dico] [uxbox.main.data.images :as dimg] [uxbox.main.data.colors :as dcol] @@ -27,6 +28,7 @@ [uxbox.main.ui.modal :as modal] [uxbox.main.ui.confirm :refer [confirm-dialog]] [uxbox.main.ui.colorpicker :refer [colorpicker most-used-colors]] + [uxbox.main.ui.components.editable-label :refer [editable-label]] )) (mf/defc modal-create-color @@ -53,19 +55,9 @@ [:a.close {:href "#" :on-click cancel} i/close]]))) - -(defmulti create-library (fn [x _] x)) -(defmethod create-library :icons [_ team-id] - (let [name (str "Icon Library "(gensym "l"))] - (st/emit! (dico/create-icon-library team-id name)))) - -(defmethod create-library :images [_ team-id] - (let [name (str "Image Library "(gensym "l"))] - (st/emit! (dimg/create-image-library team-id name)))) - -(defmethod create-library :palettes [_ team-id] - (let [name (str "Image Library "(gensym "l"))] - (st/emit! (dcol/create-color-library team-id name)))) +(defn create-library [section team-id] + (let [name (str (str (str/title (name section)) " " (gensym "Library ")))] + (st/emit! (dlib/create-library section team-id name)))) (defmulti create-item (fn [x _ _] x)) @@ -128,26 +120,42 @@ (let [path (keyword (str "dashboard-library-" (name section)))] (dico/fetch-icon-library (:id item)) (st/emit! (rt/nav path {:team-id team-id :library-id (:id item)}))))} - [:a (:name item)]])]])) + [:& editable-label {:value (:name item) + :on-change #(st/emit! (dlib/rename-library section library-id %))}] + ])]])) (mf/defc library-top-menu - [{:keys [selected section library-id]}] - (let [state (mf/use-state {:is-open false}) - locale (i18n/use-locale)] + [{:keys [selected section library-id team-id]}] + (let [state (mf/use-state {:is-open false + :editing-name false}) + locale (i18n/use-locale) + stop-editing #(swap! state assoc :editing-name false)] [:header.library-top-menu [:div.library-top-menu-current-element - [:h2.library-top-menu-current-element-name (:name selected)] + [:& editable-label {:edit (:editing-name @state) + :on-change #(do + (stop-editing) + (st/emit! (dlib/rename-library section library-id %))) + :on-cancel #(swap! state assoc :editing-name false) + :class-name "library-top-menu-current-element-name" + :value (:name selected)}] [:a.library-top-menu-current-action { :on-click #(swap! state update :is-open not)} [:span i/arrow-down]] [:& context-menu {:show (:is-open @state) :on-close #(swap! state update :is-open not) - :options [[(t locale "ds.button.rename") #(println "Rename")] - [(t locale "ds.button.delete") #(println "Delete")]]}]] + :options [[(t locale "ds.button.rename") + #(swap! state assoc :editing-name true)] + + [(t locale "ds.button.delete") + #(let [path (keyword (str "dashboard-library-" (name section) "-index"))] + (do + (st/emit! (dlib/delete-library section library-id)) + (st/emit! (rt/nav path {:team-id team-id}))))]]}]] [:div.library-top-menu-actions - [:a i/trash] + [:a.library-top-menu-actions-delete i/trash] (if (= section :palettes) [:button.btn-dashboard @@ -158,12 +166,17 @@ [:label {:for "file-upload" :class-name "btn-dashboard"} (t locale (str "dashboard.library.add-item." (name section)))] [:input {:on-change #(create-item section library-id %) - :id "file-upload" :type "file" :style {:display "none"}}]] - - )]])) + :id "file-upload" + :type "file" + :multiple true + :accept (case section + :images "image" + :icons "image/svg+xml" + "") + :style {:display "none"}}]])]])) (mf/defc library-icon-card - [{:keys [id name url content metadata]}] + [{:keys [id name url content metadata library-id]}] (let [locale (i18n/use-locale) state (mf/use-state {:is-open false})] [:div.library-card.library-icon @@ -189,10 +202,11 @@ [:& context-menu {:show (:is-open @state) :on-close #(swap! state update :is-open not) - :options [[(t locale "ds.button.delete") #(println "Delete")]]}]]])) + :options [[(t locale "ds.button.delete") + #(st/emit! (dlib/delete-item :icons library-id id))]]}]]])) (mf/defc library-image-card - [{:keys [id name thumb-uri]}] + [{:keys [id name thumb-uri library-id]}] (let [locale (i18n/use-locale) state (mf/use-state {:is-open false})] [:div.library-card.library-image @@ -213,10 +227,11 @@ [:& context-menu {:show (:is-open @state) :on-close #(swap! state update :is-open not) - :options [[(t locale "ds.button.delete") #(println "Delete")]]}]]])) + :options [[(t locale "ds.button.delete") + #(st/emit! (dlib/delete-item :images library-id id))]]}]]])) (mf/defc library-color-card - [{ :keys [ id content ] }] + [{ :keys [ id content library-id] }] (when content (let [locale (i18n/use-locale) state (mf/use-state {:is-open false})] @@ -240,54 +255,61 @@ [:& context-menu {:show (:is-open @state) :on-close #(swap! state update :is-open not) - :options [[(t locale "ds.button.delete") #(println "Delete")]]}]]]))) + :options [[(t locale "ds.button.delete") + #(st/emit! (dlib/delete-item :palettes library-id id))]]}]]]))) -(def icon-libraries-ref - (-> (comp (l/key :library) (l/key :icon-libraries)) +(defn libraries-ref [section] + (-> (comp (l/key :library) (l/key section)) (l/derive st/state))) -(def image-libraries-ref - (-> (comp (l/key :library) (l/key :image-libraries)) - (l/derive st/state))) - -(def color-libraries-ref - (-> (comp (l/key :library) (l/key :color-libraries)) - (l/derive st/state))) - -(def selected-items-ref - (-> (comp (l/key :library) (l/key :selected-items)) +(defn selected-items-ref [library-id] + (-> (comp (l/key :library) (l/key :selected-items) (l/key library-id)) (l/derive st/state))) (mf/defc library-page [{:keys [team-id library-id section]}] - (mf/use-effect {:fn #(case section - :icons (st/emit! (dico/fetch-icon-libraries team-id)) - :images (st/emit! (dimg/fetch-image-libraries team-id)) - :palettes (st/emit! (dcol/fetch-color-libraries team-id))) - :deps (mf/deps section team-id)}) - (mf/use-effect {:fn #(when library-id - (case section - :icons (st/emit! (dico/fetch-icon-library library-id)) - :images (st/emit! (dimg/fetch-image-library library-id)) - :palettes (st/emit! (dcol/fetch-color-library library-id)))) - :deps (mf/deps library-id)}) - (let [libraries (case section - :icons (mf/deref icon-libraries-ref) - :images (mf/deref image-libraries-ref) - :palettes (mf/deref color-libraries-ref)) - items (mf/deref selected-items-ref) + (let [libraries (mf/deref (libraries-ref section)) + items (mf/deref (selected-items-ref library-id)) selected-library (first (filter #(= (:id %) library-id) libraries))] + + (mf/use-effect {:fn #(st/emit! (dlib/retrieve-libraries section team-id)) + :deps (mf/deps section team-id)}) + (mf/use-effect {:fn #(when library-id + (st/emit! (dlib/retrieve-library-data section library-id))) + :deps (mf/deps library-id)}) + (mf/use-effect {:fn #(if (and (nil? library-id) (> (count libraries) 0)) + (let [path (keyword (str "dashboard-library-" (name section)))] + (st/emit! (rt/nav path {:team-id team-id :library-id (:id (first libraries))})))) + :deps (mf/deps library-id section team-id)}) + [:div.library-page [:& library-header {:section section :team-id team-id}] [:& library-sidebar {:items libraries :team-id team-id :library-id library-id :section section}] - (when library-id + (if library-id [:section.library-content - [:& library-top-menu {:selected selected-library :section section :library-id library-id}] - [:div.library-page-cards-container - (for [item items] - (let [item (assoc item :key (:id item))] - (case section - :icons [:& library-icon-card item] - :images [:& library-image-card item] - :palettes [:& library-color-card item ])))]])])) + [:& library-top-menu {:selected selected-library :section section :library-id library-id :team-id team-id}] + [:* + ;; TODO: Fix the chunked list + #_[:& chunked-list {:items items + :initial-size 30 + :chunk-size 30} + (fn [item] + (let [item (assoc item :key (:id item))] + (case section + :icons [:& library-icon-card item] + :images [:& library-image-card item] + :palettes [:& library-color-card item ])))] + (if (> (count items) 0) + [:div.library-page-cards-container + (for [item items] + (let [item (assoc item :key (:id item))] + (case section + :icons [:& library-icon-card item] + :images [:& library-image-card item] + :palettes [:& library-color-card item ])))] + [:div.library-content-empty + [:p.library-content-empty-text "You still have no elements in this library"]])]] + + [:div.library-content-empty + [:p.library-content-empty-text "You still have no image libraries."]])])) diff --git a/frontend/src/uxbox/util/data.cljs b/frontend/src/uxbox/util/data.cljs index 0ea8ba097..4dd2035f5 100644 --- a/frontend/src/uxbox/util/data.cljs +++ b/frontend/src/uxbox/util/data.cljs @@ -149,7 +149,7 @@ [& params] {:pre [(even? (count params))]} (str/join " " (reduce (fn [acc [k v]] - (if (true? v) + (if (and k (true? v)) (conj acc (name k)) acc)) [] diff --git a/frontend/src/uxbox/util/dom.cljs b/frontend/src/uxbox/util/dom.cljs index a7730eb5e..3c153c42c 100644 --- a/frontend/src/uxbox/util/dom.cljs +++ b/frontend/src/uxbox/util/dom.cljs @@ -142,8 +142,7 @@ y (.-clientY event)] (gpt/point x y))) -(defn get-offset-position - [event] - (let [x (.-offsetX event) - y (.-offsetY event)] - (gpt/point x y))) +(defn focus! + [node] + (.focus node)) +