mirror of
https://github.com/penpot/penpot.git
synced 2025-05-05 08:05:55 +02:00
642 lines
24 KiB
Clojure
642 lines
24 KiB
Clojure
;; 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/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns app.main.ui.dashboard.grid
|
|
(:require-macros [app.main.style :as stl])
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.geom.point :as gpt]
|
|
[app.common.logging :as log]
|
|
[app.config :as cf]
|
|
[app.main.data.common :as dcm]
|
|
[app.main.data.dashboard :as dd]
|
|
[app.main.data.notifications :as ntf]
|
|
[app.main.data.project :as dpj]
|
|
[app.main.data.team :as dtm]
|
|
[app.main.features :as features]
|
|
[app.main.fonts :as fonts]
|
|
[app.main.rasterizer :as thr]
|
|
[app.main.refs :as refs]
|
|
[app.main.render :as render]
|
|
[app.main.repo :as rp]
|
|
[app.main.store :as st]
|
|
[app.main.ui.components.color-bullet :as bc]
|
|
[app.main.ui.dashboard.file-menu :refer [file-menu*]]
|
|
[app.main.ui.dashboard.import :refer [use-import-file]]
|
|
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
|
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
|
|
[app.main.ui.ds.product.loader :refer [loader*]]
|
|
[app.main.ui.hooks :as h]
|
|
[app.main.ui.icons :as i]
|
|
[app.main.worker :as wrk]
|
|
[app.util.color :as uc]
|
|
[app.util.dom :as dom]
|
|
[app.util.dom.dnd :as dnd]
|
|
[app.util.i18n :as i18n :refer [tr]]
|
|
[app.util.keyboard :as kbd]
|
|
[app.util.time :as dt]
|
|
[app.util.timers :as ts]
|
|
[beicon.v2.core :as rx]
|
|
[cuerdas.core :as str]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(log/set-level! :debug)
|
|
|
|
;; --- Grid Item Thumbnail
|
|
|
|
(defn- persist-thumbnail
|
|
[file-id revn blob]
|
|
(let [params {:file-id file-id :revn revn :media blob}]
|
|
(->> (rp/cmd! :create-file-thumbnail params)
|
|
(rx/map :id))))
|
|
|
|
(defn render-thumbnail
|
|
[file-id revn]
|
|
(->> (wrk/ask! {:cmd :thumbnails/generate-for-file
|
|
:revn revn
|
|
:file-id file-id
|
|
:features (features/get-team-enabled-features @st/state)})
|
|
(rx/mapcat (fn [{:keys [fonts] :as result}]
|
|
(->> (fonts/render-font-styles fonts)
|
|
(rx/map (fn [styles]
|
|
(assoc result
|
|
:styles styles
|
|
:width 252))))))))
|
|
|
|
(defn- ask-for-thumbnail
|
|
"Creates some hooks to handle the files thumbnails cache"
|
|
[file-id revn]
|
|
(->> (render-thumbnail file-id revn)
|
|
(rx/mapcat thr/render)
|
|
(rx/mapcat (partial persist-thumbnail file-id revn))))
|
|
|
|
(mf/defc grid-item-thumbnail*
|
|
{::mf/props :obj
|
|
::mf/private true}
|
|
[{:keys [can-edit file]}]
|
|
(let [file-id (get file :id)
|
|
revn (get file :revn)
|
|
thumbnail-id (get file :thumbnail-id)
|
|
|
|
bg-color (dm/get-in file [:data :background])
|
|
|
|
container (mf/use-ref)
|
|
visible? (h/use-visible container :once? true)]
|
|
|
|
(mf/with-effect [file-id revn visible? thumbnail-id]
|
|
(when (and visible? (not thumbnail-id))
|
|
(->> (ask-for-thumbnail file-id revn)
|
|
(rx/subs! (fn [thumbnail-id]
|
|
(st/emit! (dd/set-file-thumbnail file-id thumbnail-id)))
|
|
(fn [cause]
|
|
(log/error :hint "unable to render thumbnail"
|
|
:file-if file-id
|
|
:revn revn
|
|
:message (ex-message cause)))))))
|
|
|
|
[:div {:class (stl/css :grid-item-th)
|
|
:style {:background-color bg-color}
|
|
:ref container}
|
|
(when visible?
|
|
(if thumbnail-id
|
|
[:img {:class (stl/css :grid-item-thumbnail-image)
|
|
:draggable (dm/str can-edit)
|
|
:src (cf/resolve-media thumbnail-id)
|
|
:loading "lazy"
|
|
:decoding "async"}]
|
|
[:> loader* {:class (stl/css :grid-loader)
|
|
:draggable (dm/str can-edit)
|
|
:overlay true
|
|
:title (tr "labels.loading")}]))]))
|
|
|
|
;; --- Grid Item Library
|
|
|
|
(def ^:private menu-icon
|
|
(i/icon-xref :menu (stl/css :menu-icon)))
|
|
|
|
(mf/defc grid-item-library*
|
|
{::mf/props :obj}
|
|
[{:keys [file]}]
|
|
(mf/with-effect [file]
|
|
(when file
|
|
(let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))]
|
|
(run! fonts/ensure-loaded! font-ids))))
|
|
|
|
[:div {:class (stl/css :grid-item-th :library)}
|
|
(if (nil? file)
|
|
[:> loader* {:class (stl/css :grid-loader)
|
|
:overlay true
|
|
:title (tr "labels.loading")}]
|
|
(let [summary (:library-summary file)
|
|
components (:components summary)
|
|
colors (:colors summary)
|
|
typographies (:typographies summary)]
|
|
[:*
|
|
(when (and (zero? (:count components)) (zero? (:count colors)) (zero? (:count typographies)))
|
|
[:*
|
|
[:div {:class (stl/css :asset-section)}
|
|
[:div {:class (stl/css :asset-title)}
|
|
[:span (tr "workspace.assets.components")]
|
|
[:span {:class (stl/css :num-assets)} (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space
|
|
[:div {:class (stl/css :asset-section)}
|
|
[:div {:class (stl/css :asset-title)}
|
|
[:span (tr "workspace.assets.colors")]
|
|
[:span {:class (stl/css :num-assets)} (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space
|
|
[:div {:class (stl/css :asset-section)}
|
|
[:div {:class (stl/css :asset-title)}
|
|
[:span (tr "workspace.assets.typography")]
|
|
[:span {:class (stl/css :num-assets)} (str "\u00A0(") 0 ")"]]]]) ;; Unicode 00A0 is non-breaking space
|
|
|
|
|
|
(when (pos? (:count components))
|
|
[:div {:class (stl/css :asset-section)}
|
|
[:div {:class (stl/css :asset-title)}
|
|
[:span (tr "workspace.assets.components")]
|
|
[:span {:class (stl/css :num-assets)} (str "\u00A0(") (:count components) ")"]] ;; Unicode 00A0 is non-breaking space
|
|
[:div {:class (stl/css :asset-list)}
|
|
(for [component (:sample components)]
|
|
(let [root-id (or (:main-instance-id component) (:id component))] ;; Check for components-v2 in library
|
|
[:div {:class (stl/css :asset-list-item)
|
|
:key (str "assets-component-" (:id component))}
|
|
[:& render/component-svg {:root-shape (get-in component [:objects root-id])
|
|
:objects (:objects component)}] ;; Components in the summary come loaded with objects, even in v2
|
|
[:div {:class (stl/css :name-block)}
|
|
[:span {:class (stl/css :item-name)
|
|
:title (:name component)}
|
|
(:name component)]]]))
|
|
(when (> (:count components) (count (:sample components)))
|
|
[:div {:class (stl/css :asset-list-item)}
|
|
[:div {:class (stl/css :name-block)}
|
|
[:span {:class (stl/css :item-name)} "(...)"]]])]])
|
|
|
|
(when (pos? (:count colors))
|
|
[:div {:class (stl/css :asset-section)}
|
|
[:div {:class (stl/css :asset-title)}
|
|
[:span (tr "workspace.assets.colors")]
|
|
[:span {:class (stl/css :num-assets)} (str "\u00A0(") (:count colors) ")"]] ;; Unicode 00A0 is non-breaking space
|
|
[:div {:class (stl/css :asset-list)}
|
|
(for [color (:sample colors)]
|
|
(let [default-name (cond
|
|
(:gradient color) (uc/gradient-type->string (get-in color [:gradient :type]))
|
|
(:color color) (:color color)
|
|
:else (:value color))]
|
|
[:div {:class (stl/css :asset-list-item :color-item)
|
|
:key (str "assets-color-" (:id color))}
|
|
[:& bc/color-bullet {:color {:color (:color color)
|
|
:id (:id color)
|
|
:opacity (:opacity color)}
|
|
:mini true}]
|
|
[:div {:class (stl/css :name-block)}
|
|
[:span {:class (stl/css :color-name)} (:name color)]
|
|
(when-not (= (:name color) default-name)
|
|
[:span {:class (stl/css :color-value)} (:color color)])]]))
|
|
|
|
(when (> (:count colors) (count (:sample colors)))
|
|
[:div {:class (stl/css :asset-list-item)}
|
|
[:div {:class (stl/css :name-block)}
|
|
[:span {:class (stl/css :item-name)} "(...)"]]])]])
|
|
|
|
(when (pos? (:count typographies))
|
|
[:div {:class (stl/css :asset-section)}
|
|
[:div {:class (stl/css :asset-title)}
|
|
[:span (tr "workspace.assets.typography")]
|
|
[:span {:class (stl/css :num-assets)} (str "\u00A0(") (:count typographies) ")"]] ;; Unicode 00A0 is non-breaking space
|
|
[:div {:class (stl/css :asset-list)}
|
|
(for [typography (:sample typographies)]
|
|
[:div {:class (stl/css :asset-list-item)
|
|
:key (str "assets-typography-" (:id typography))}
|
|
[:div {:class (stl/css :typography-sample)
|
|
:style {:font-family (:font-family typography)
|
|
:font-weight (:font-weight typography)
|
|
:font-style (:font-style typography)}}
|
|
(tr "workspace.assets.typography.sample")]
|
|
[:div {:class (stl/css :name-block)}
|
|
[:span {:class (stl/css :item-name)
|
|
:title (:name typography)}
|
|
(:name typography)]]])
|
|
|
|
(when (> (:count typographies) (count (:sample typographies)))
|
|
[:div {:class (stl/css :asset-list-item)}
|
|
[:div {:class (stl/css :name-block)}
|
|
[:span {:class (stl/css :item-name)} "(...)"]]])]])]))])
|
|
|
|
;; --- Grid Item
|
|
|
|
(mf/defc grid-item-metadata
|
|
[{:keys [modified-at]}]
|
|
|
|
(let [locale (mf/deref i18n/locale)
|
|
time (dt/timeago modified-at {:locale locale})]
|
|
[:span {:class (stl/css :date)} time]))
|
|
|
|
(defn create-counter-element
|
|
[_element file-count]
|
|
(let [counter-el (dom/create-element "div")]
|
|
(dom/set-property! counter-el "class" (stl/css :drag-counter))
|
|
(dom/set-text! counter-el (str file-count))
|
|
counter-el))
|
|
|
|
(mf/defc grid-item*
|
|
{::mf/props :obj}
|
|
[{:keys [file origin can-edit selected-files]}]
|
|
(let [file-id (:id file)
|
|
|
|
is-library-view (= origin :libraries)
|
|
|
|
dashboard-local (mf/deref refs/dashboard-local)
|
|
file-menu-open? (:menu-open dashboard-local)
|
|
|
|
selected? (contains? selected-files file-id)
|
|
|
|
node-ref (mf/use-ref)
|
|
menu-ref (mf/use-ref)
|
|
|
|
on-menu-close
|
|
(mf/use-fn
|
|
(fn [_]
|
|
(st/emit! (dd/hide-file-menu))))
|
|
|
|
on-select
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(when (or (not selected?) (> (count selected-files) 1))
|
|
(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))))))
|
|
|
|
on-navigate
|
|
(mf/use-fn
|
|
(mf/deps file-id)
|
|
(fn [event]
|
|
(let [menu-icon (mf/ref-val menu-ref)
|
|
target (dom/get-target event)]
|
|
(when-not (dom/child? target menu-icon)
|
|
(st/emit! (dcm/go-to-workspace :file-id file-id))))))
|
|
|
|
on-drag-start
|
|
(mf/use-fn
|
|
(mf/deps selected-files can-edit)
|
|
(fn [event]
|
|
(st/emit! (dd/hide-file-menu))
|
|
(when can-edit
|
|
(let [offset (dom/get-offset-position (dom/event->native-event event))
|
|
|
|
select-current? (not (contains? selected-files (:id file)))
|
|
|
|
item-el (mf/ref-val node-ref)
|
|
counter-el (create-counter-element
|
|
item-el
|
|
(if select-current?
|
|
1
|
|
(count selected-files)))]
|
|
(when select-current?
|
|
(st/emit! (dd/clear-selected-files))
|
|
(st/emit! (dd/toggle-file-select file)))
|
|
|
|
(dnd/set-data! event "penpot/files" "dummy")
|
|
(dnd/set-allowed-effect! event "move")
|
|
|
|
;; set-drag-image requires that the element is rendered and
|
|
;; visible to the user at the moment of creating the ghost
|
|
;; image (to make a snapshot), but you may remove it right
|
|
;; afterwards, in the next render cycle.
|
|
(dom/append-child! item-el counter-el)
|
|
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
|
|
(ts/raf #(.removeChild ^js item-el counter-el))))))
|
|
|
|
on-menu-click
|
|
(mf/use-fn
|
|
(mf/deps file selected?)
|
|
(fn [event]
|
|
(dom/stop-propagation event)
|
|
(dom/prevent-default event)
|
|
(when-not selected?
|
|
(when-not (kbd/shift? event)
|
|
(st/emit! (dd/clear-selected-files)))
|
|
(st/emit! (dd/toggle-file-select file)))
|
|
|
|
(let [client-position (dom/get-client-position event)
|
|
position (if (and (nil? (:y client-position)) (nil? (:x client-position)))
|
|
(let [target-element (dom/get-target event)
|
|
points (dom/get-bounding-rect target-element)
|
|
y (:top points)
|
|
x (:left points)]
|
|
(gpt/point x y))
|
|
client-position)]
|
|
(st/emit! (dd/show-file-menu-with-position file-id position)))))
|
|
|
|
on-context-menu
|
|
(mf/use-fn
|
|
(mf/deps is-library-view)
|
|
(fn [event]
|
|
(dom/stop-propagation event)
|
|
(dom/prevent-default event)
|
|
(when-not is-library-view
|
|
(on-menu-click event))))
|
|
|
|
edit
|
|
(mf/use-fn
|
|
(mf/deps file)
|
|
(fn [name]
|
|
(let [name (str/trim name)]
|
|
(when (not= name "")
|
|
(st/emit! (dd/rename-file (assoc file :name name)))))
|
|
(st/emit! (dd/stop-edit-file-name))))
|
|
|
|
on-edit
|
|
(mf/use-fn
|
|
(mf/deps file)
|
|
(fn [event]
|
|
(dom/stop-propagation event)
|
|
(st/emit! (dd/start-edit-file-name file-id))))
|
|
|
|
handle-key-down
|
|
(mf/use-callback
|
|
(mf/deps on-navigate on-select)
|
|
(fn [event]
|
|
(dom/stop-propagation event)
|
|
(when (kbd/enter? event)
|
|
(on-navigate event))
|
|
(when (kbd/shift? event)
|
|
(when (or (kbd/down-arrow? event) (kbd/left-arrow? event) (kbd/up-arrow? event) (kbd/right-arrow? event))
|
|
(on-select event)) ;; TODO Fix this
|
|
)))]
|
|
|
|
[:li {:class (stl/css-case :grid-item true
|
|
:project-th true
|
|
:library is-library-view)}
|
|
[:div
|
|
{:class (stl/css-case :selected selected?
|
|
:library is-library-view)
|
|
:ref node-ref
|
|
:role "button"
|
|
:title (:name file)
|
|
:draggable (dm/str can-edit)
|
|
:on-click on-select
|
|
:on-key-down handle-key-down
|
|
:on-double-click on-navigate
|
|
:on-drag-start on-drag-start
|
|
:on-context-menu on-context-menu}
|
|
|
|
[:div {:class (stl/css :overlay)}]
|
|
|
|
(if ^boolean is-library-view
|
|
[:> grid-item-library* {:file file}]
|
|
[:> grid-item-thumbnail* {:file file :can-edit can-edit}])
|
|
|
|
(when (and (:is-shared file) (not is-library-view))
|
|
[:div {:class (stl/css :item-badge)} i/library])
|
|
|
|
[:div {:class (stl/css :info-wrapper)}
|
|
[:div {:class (stl/css :item-info)}
|
|
(if (and (= file-id (:file-id dashboard-local)) (:edition dashboard-local))
|
|
[:& inline-edition {:content (:name file)
|
|
:on-end edit}]
|
|
[:h3 (:name file)])
|
|
[:& grid-item-metadata {:modified-at (:modified-at file)}]]
|
|
|
|
(when-not is-library-view
|
|
[:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))}
|
|
[:div
|
|
{:class (stl/css :project-th-icon :menu)
|
|
:tab-index "0"
|
|
:ref menu-ref
|
|
:id (str file-id "-action-menu")
|
|
:on-click on-menu-click
|
|
:on-key-down (fn [event]
|
|
(when (kbd/enter? event)
|
|
(dom/stop-propagation event)
|
|
(on-menu-click event)))}
|
|
menu-icon
|
|
(when (and selected? file-menu-open?)
|
|
;; When the menu is open we disable events in the dashboard. We need to force pointer events
|
|
;; so the menu can be handled
|
|
[:div {:style {:pointer-events "all"}}
|
|
[:> file-menu* {:files (vals selected-files)
|
|
:left (+ 24 (:x (:menu-pos dashboard-local)))
|
|
:top (:y (:menu-pos dashboard-local))
|
|
:can-edit can-edit
|
|
:navigate true
|
|
:on-edit on-edit
|
|
:on-menu-close on-menu-close
|
|
:origin origin
|
|
:parent-id (dm/str file-id "-action-menu")}]])]])]]]))
|
|
|
|
(mf/defc grid
|
|
{::mf/props :obj}
|
|
[{:keys [files project origin limit create-fn can-edit selected-files]}]
|
|
(let [dragging? (mf/use-state false)
|
|
project-id (:id project)
|
|
node-ref (mf/use-var nil)
|
|
|
|
on-finish-import
|
|
(mf/use-fn
|
|
(fn []
|
|
(st/emit! (dpj/fetch-files project-id)
|
|
(dtm/fetch-shared-files)
|
|
(dd/clear-selected-files))))
|
|
|
|
|
|
|
|
import-files (use-import-file project-id on-finish-import)
|
|
|
|
on-drag-enter
|
|
(mf/use-fn
|
|
(fn [e]
|
|
(when can-edit
|
|
(when (and (not (dnd/has-type? e "penpot/files"))
|
|
(or (dnd/has-type? e "Files")
|
|
(dnd/has-type? e "application/x-moz-file")))
|
|
(dom/prevent-default e)
|
|
(reset! dragging? true)))))
|
|
|
|
on-drag-over
|
|
(mf/use-fn
|
|
(fn [e]
|
|
(when (or (dnd/has-type? e "Files")
|
|
(dnd/has-type? e "application/x-moz-file"))
|
|
(dom/prevent-default e))))
|
|
|
|
on-drag-leave
|
|
(mf/use-fn
|
|
(fn [e]
|
|
(when-not (dnd/from-child? e)
|
|
(reset! dragging? false))))
|
|
|
|
on-drop
|
|
(mf/use-fn
|
|
(fn [e]
|
|
(if can-edit
|
|
(when (and (not (dnd/has-type? e "penpot/files"))
|
|
(or (dnd/has-type? e "Files")
|
|
(dnd/has-type? e "application/x-moz-file")))
|
|
(dom/prevent-default e)
|
|
(reset! dragging? false)
|
|
(import-files (.-files (.-dataTransfer e))))
|
|
(dom/prevent-default e))))]
|
|
|
|
[:div {:class (stl/css :dashboard-grid)
|
|
:dragabble (dm/str can-edit)
|
|
:on-drag-enter on-drag-enter
|
|
:on-drag-over on-drag-over
|
|
:on-drag-leave on-drag-leave
|
|
:on-drop on-drop
|
|
:ref node-ref}
|
|
(cond
|
|
(nil? files)
|
|
[:& loading-placeholder]
|
|
|
|
(seq files)
|
|
(for [[index slice] (d/enumerate (partition-all limit files))]
|
|
|
|
[:ul {:class (stl/css :grid-row) :key (dm/str index)}
|
|
(when @dragging?
|
|
[:li {:class (stl/css :grid-item)}])
|
|
(for [item slice]
|
|
[:> grid-item*
|
|
{:file item
|
|
:key (dm/str (:id item))
|
|
:origin origin
|
|
:selected-files selected-files
|
|
:can-edit can-edit}])])
|
|
|
|
:else
|
|
[:& empty-placeholder
|
|
{:limit limit
|
|
:can-edit can-edit
|
|
:create-fn create-fn
|
|
:origin origin
|
|
:project-id project-id
|
|
:on-finish-import on-finish-import}])]))
|
|
|
|
(mf/defc line-grid-row
|
|
[{:keys [files selected-files dragging? limit can-edit] :as props}]
|
|
(let [elements limit
|
|
limit (if dragging? (dec limit) limit)]
|
|
[:ul {:class (stl/css :grid-row :no-wrap)
|
|
:style {:grid-template-columns (dm/str "repeat(" elements ", 1fr)")}}
|
|
|
|
(when dragging?
|
|
[:li {:class (stl/css :grid-item :dragged)}])
|
|
|
|
(for [item (take limit files)]
|
|
[:> grid-item*
|
|
{:id (:id item)
|
|
:file item
|
|
:selected-files selected-files
|
|
:can-edit can-edit
|
|
:key (dm/str (:id item))}])]))
|
|
|
|
(mf/defc line-grid
|
|
[{:keys [project team files limit create-fn can-edit] :as props}]
|
|
(let [dragging? (mf/use-state false)
|
|
project-id (:id project)
|
|
team-id (:id team)
|
|
|
|
selected-files (mf/deref refs/selected-files)
|
|
selected-project (mf/deref refs/selected-project)
|
|
|
|
on-finish-import
|
|
(mf/use-fn
|
|
(fn []
|
|
(st/emit! (dd/fetch-recent-files)
|
|
(dd/clear-selected-files))))
|
|
|
|
import-files (use-import-file project-id on-finish-import)
|
|
|
|
on-drag-enter
|
|
(mf/use-fn
|
|
(mf/deps selected-project can-edit)
|
|
(fn [e]
|
|
(when can-edit
|
|
(cond
|
|
(dnd/has-type? e "penpot/files")
|
|
(do
|
|
(dom/prevent-default e)
|
|
(when-not (or (dnd/from-child? e)
|
|
(dnd/broken-event? e))
|
|
(when (not= selected-project project-id)
|
|
(reset! dragging? true))))
|
|
|
|
(or (dnd/has-type? e "Files")
|
|
(dnd/has-type? e "application/x-moz-file"))
|
|
(do
|
|
(dom/prevent-default e)
|
|
(reset! dragging? true))))))
|
|
|
|
on-drag-over
|
|
(mf/use-fn
|
|
(fn [e]
|
|
(when (or (dnd/has-type? e "penpot/files")
|
|
(dnd/has-type? e "Files")
|
|
(dnd/has-type? e "application/x-moz-file"))
|
|
(dom/prevent-default e))))
|
|
|
|
on-drag-leave
|
|
(mf/use-fn
|
|
(fn [e]
|
|
(when-not (dnd/from-child? e)
|
|
(reset! dragging? false))))
|
|
|
|
on-drop-success
|
|
(mf/use-fn
|
|
(fn []
|
|
(st/emit! (ntf/success (tr "dashboard.success-move-file"))
|
|
(dd/fetch-recent-files)
|
|
(dd/clear-selected-files))))
|
|
|
|
on-drop
|
|
(mf/use-fn
|
|
(mf/deps files selected-files can-edit)
|
|
(fn [e]
|
|
(if can-edit
|
|
(cond
|
|
(dnd/has-type? e "penpot/files")
|
|
(do
|
|
(reset! dragging? false)
|
|
(when (not= selected-project project-id)
|
|
(let [data {:ids (into #{} (keys selected-files))
|
|
:project-id project-id}
|
|
mdata {:on-success on-drop-success}]
|
|
(st/emit! (dd/move-files (with-meta data mdata))))))
|
|
|
|
(or (dnd/has-type? e "Files")
|
|
(dnd/has-type? e "application/x-moz-file"))
|
|
(do
|
|
(dom/prevent-default e)
|
|
(reset! dragging? false)
|
|
(import-files (.-files (.-dataTransfer e)))))
|
|
(dom/prevent-default e))))]
|
|
|
|
[:div {:class (stl/css :dashboard-grid)
|
|
:dragabble (dm/str can-edit)
|
|
:on-drag-enter on-drag-enter
|
|
:on-drag-over on-drag-over
|
|
:on-drag-leave on-drag-leave
|
|
:on-drop on-drop}
|
|
(cond
|
|
(nil? files)
|
|
[:& loading-placeholder]
|
|
|
|
(seq files)
|
|
[:& line-grid-row {:files files
|
|
:team-id team-id
|
|
:selected-files selected-files
|
|
:dragging? @dragging?
|
|
:can-edit can-edit
|
|
:limit limit}]
|
|
|
|
:else
|
|
[:& empty-placeholder
|
|
{:dragging? @dragging?
|
|
:limit limit
|
|
:can-edit can-edit
|
|
:create-fn create-fn
|
|
:project-id project-id
|
|
:on-finish-import on-finish-import}])]))
|