diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 2deac1e470..c28e791a39 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -26,10 +26,10 @@ [app.main.ui.ds.foundations.typography :as t] [app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.text :refer [text*]] - [app.main.ui.ds.product.loader :refer [loader*]] - [app.main.ui.icons :as deprecated-icon] - [app.main.ui.notifications.context-notification :refer [context-notification]] [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] + [app.main.ui.ds.product.loader :refer [loader*]] + [app.main.ui.hooks :as hooks] + [app.main.ui.icons :as deprecated-icon] [app.main.worker :as mw] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -224,13 +224,13 @@ ;; FIXME: rename to format format (:type entry) - loading? false #_(or (= :analyze status) + loading? (or (= :analyze status) (= :import-progress status) (and is-progress (= :import-ready status))) - analyze-error? true #_(= :analyze-error status) - import-success? false #_(= :import-success status) - import-error? false #_(= :import-error status) - import-ready? false #_(= :import-ready status) + analyze-error? (= :analyze-error status) + import-success? (= :import-success status) + import-error? (= :import-error status) + import-ready? (= :import-ready status) level (cond import-success? :success @@ -319,19 +319,20 @@ (when ^boolean is-shared? [:> icon* {:icon-id i/library :class (stl/css :icon)}])]])]) - [:div {:class (stl/css :edit-entry-buttons)} - (when ^boolean editable? - [:> icon-button* {:on-click on-edit' - :variant "ghost" - :icon-size "s" - :aria-label (tr "labels.edit") - :icon i/curve}]) - (when ^boolean can-be-deleted - [:> icon-button* {:on-click on-delete' - :variant "ghost" - :icon-size "s" - :aria-label (tr "labels.delete") - :icon i/delete}])]] + (when ^boolean (or editable? can-be-deleted) + [:div {:class (stl/css :edit-entry-buttons)} + (when ^boolean editable? + [:> icon-button* {:on-click on-edit' + :variant "ghost" + :icon-size "s" + :aria-label (tr "labels.edit") + :icon i/curve}]) + (when ^boolean can-be-deleted + [:> icon-button* {:on-click on-delete' + :variant "ghost" + :icon-size "s" + :aria-label (tr "labels.delete") + :icon i/delete}])])] (cond analyze-error? [:> text* {:class (stl/css :error-message) @@ -401,8 +402,10 @@ (on-select id (:id first-c)))))) [:div {:class (stl/css :library-resolution)} - [:p {:class (stl/css :library-resolution-message)} - (tr "dashboard.import.resolve-libraries")] + [:> text* {:class (stl/css :library-resolution-message) + :as "p" + :typography t/body-large} + "Some libraries couldn't be linked automatically. Select the correct library for each:"] (for [{:keys [id name candidates]} candidates] (let [options (mapv (fn [c] @@ -415,7 +418,9 @@ [:div {:class (stl/css :library-resolution-item-name)} name] [:> select* {:options options + :class (stl/css :library-resolution-select) :default-selected (or (some-> selected str) "") + :has-portal true :on-change (partial on-select id)}]]))])) (mf/defc library-resolution-summary-file* @@ -735,26 +740,26 @@ [:div {:class (stl/css :modal-content)} (cond (and (= :analyze status) errors?) - [:& context-notification + [:> context-notification* {:level :warning - :class (stl/css :context-notification-error) - :content (tr "dashboard.import.import-warning")}] + :class (stl/css :context-notification-error)} + (tr "dashboard.import.import-warning")] (= :import-success status) [:* - [:& context-notification - {:level (if (zero? import-success-total) :warning :success) - :content (tr "dashboard.import.import-message" (i18n/c import-success-total))}] + [:> context-notification* + {:level (if (zero? import-success-total) :warning :success)} + (tr "dashboard.import.import-message" (i18n/c import-success-total))] (when (pos? auto-linked-count) - [:& context-notification - {:level :success - :content (tr "dashboard.import.auto-linked-libraries" (i18n/c auto-linked-count))}])] + [:> context-notification* + {:level :success} + (tr "dashboard.import.auto-linked-libraries" (i18n/c auto-linked-count))])] (= :import-error status) - [:& context-notification + [:> context-notification* {:level :error - :class (stl/css :context-notification-error) - :content (tr "dashboard.import.import-error.disclaimer")}] + :class (stl/css :context-notification-error)} + (tr "dashboard.import.import-error.disclaimer")] ;; :resolution — wizard step (current derived file) (= :library-resolution status) diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index 3be5fd9816..a0417a38f4 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -426,11 +426,6 @@ } .library-resolution-message { - // TODO: Typography does not match any existing DS typography token. - font-family: "worksans", "vazirmatn", sans-serif; - font-size: px2rem(12); - font-weight: 400; - line-height: 1.4; color: var(--color-foreground-secondary); margin-block-end: var(--sp-s); } diff --git a/frontend/src/app/main/ui/ds/controls/select.cljs b/frontend/src/app/main/ui/ds/controls/select.cljs index 337d9f3b51..4189ec45a8 100644 --- a/frontend/src/app/main/ui/ds/controls/select.cljs +++ b/frontend/src/app/main/ui/ds/controls/select.cljs @@ -13,9 +13,11 @@ [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]] + [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.object :as obj] + [app.util.timers :as timers] [clojure.string :as str] [rumext.v2 :as mf] [rumext.v2.util :as mfu])) @@ -58,11 +60,12 @@ [:empty-to-end {:optional true} [:maybe :boolean]] [:on-change {:optional true} fn?] [:dropdown-alignment {:optional true} [:maybe [:enum :left :right]]] - [:variant {:optional true} [:maybe [:enum "default" "ghost" "icon-only"]]]]) + [:variant {:optional true} [:maybe [:enum "default" "ghost" "icon-only"]]] + [:has-portal {:optional true} :boolean]]) (mf/defc select* {::mf/schema schema:select} - [{:keys [options class disabled default-selected empty-to-end on-change variant wrapper-class dropdown-alignment] :rest props}] + [{:keys [options class disabled default-selected empty-to-end on-change variant wrapper-class dropdown-alignment has-portal] :rest props}] (let [;; NOTE: we use mfu/bean here for transparently handle ;; options provide as clojure data structures or javascript ;; plain objects and lists. @@ -88,6 +91,9 @@ options-ref (mf/use-ref nil) select-ref (mf/use-ref nil) + container (hooks/use-portal-container :popup) + dropdown-wrapper-ref (mf/use-ref nil) + empty-selected-id? (str/blank? selected-id) @@ -208,10 +214,63 @@ (reset! selected-id* (get-selected-option-id options default-selected))) + ;; Portal mode: click-outside + floating positioning + (mf/with-effect [is-open has-portal] + (when (and is-open has-portal) + (let [handler + (fn [event] + (let [wrapper-node (mf/ref-val select-ref) + dropdown-node (mf/ref-val dropdown-wrapper-ref) + target (dom/get-target event)] + (when (and wrapper-node dropdown-node + (not (dom/child? target wrapper-node)) + (not (dom/child? target dropdown-node))) + (reset! is-open* false) + (reset! focused-id* nil)))) + + calculate + (fn [] + (timers/raf + (fn [] + (when-let [select-node (mf/ref-val select-ref)] + (when-let [dropdown-node (mf/ref-val dropdown-wrapper-ref)] + (let [select-rect (dom/get-bounding-rect select-node) + dropdown-rect (dom/get-bounding-rect dropdown-node) + window-height (.-innerHeight js/window) + space-below (- window-height (:bottom select-rect)) + open-up? (> (:height dropdown-rect) space-below)] + (if open-up? + (let [bottom (+ (- window-height (:top select-rect)) 4)] + (dom/set-css-property! dropdown-node "top" "unset") + (dom/set-css-property! dropdown-node "bottom" (str bottom "px"))) + (let [top (+ (:bottom select-rect) 4)] + (dom/set-css-property! dropdown-node "bottom" "unset") + (dom/set-css-property! dropdown-node "top" (str top "px")))) + (dom/set-css-property! dropdown-node "left" (str (:left select-rect) "px")) + (dom/set-css-property! dropdown-node "width" (str (:width select-rect) "px")) + (dom/set-css-property! dropdown-node "position" "fixed")))))))] + + (.addEventListener js/document "mousedown" handler) + + (let [ro (js/ResizeObserver. (fn [_] (calculate)))] + (when-let [node (mf/ref-val select-ref)] + (.observe ro node)) + + (.addEventListener js/window "resize" calculate) + (.addEventListener js/window "scroll" calculate true) + + (calculate) + + (fn [] + (.removeEventListener js/document "mousedown" handler) + (.disconnect ro) + (.removeEventListener js/window "resize" calculate) + (.removeEventListener js/window "scroll" calculate true)))))) + [:div {:class [wrapper-class (stl/css :select-wrapper)] :on-click on-click :ref select-ref - :on-blur on-blur} + :on-blur (when-not has-portal on-blur)} [:> :button props [:span {:class (stl/css-case :select-header true @@ -241,11 +300,24 @@ :aria-hidden true}]] (when ^boolean is-open - [:> options-dropdown* {:on-click on-option-click - :id listbox-id - :options options - :selected selected-id - :focused focused-id - :align dropdown-alignment - :empty-to-end empty-to-end - :ref set-option-ref}])])) + (if has-portal + (mf/portal + (mf/html + [:> options-dropdown* {:on-click on-option-click + :id listbox-id + :options options + :selected selected-id + :focused focused-id + :align dropdown-alignment + :empty-to-end empty-to-end + :ref set-option-ref + :wrapper-ref dropdown-wrapper-ref}]) + container) + [:> options-dropdown* {:on-click on-option-click + :id listbox-id + :options options + :selected selected-id + :focused focused-id + :align dropdown-alignment + :empty-to-end empty-to-end + :ref set-option-ref}]))]))