mirror of
https://github.com/penpot/penpot.git
synced 2025-05-24 04:16:10 +02:00
⚡ Improve rendering performance.
This commit is contained in:
parent
2c321cbdb8
commit
bebe220aa0
10 changed files with 83 additions and 81 deletions
|
@ -120,7 +120,11 @@
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [page-id (get-in state [:workspace :current])]
|
(let [page-id (get-in state [:workspace :current])]
|
||||||
(update-in state [:workspace page-id :flags] conj flag))))
|
(update-in state [:workspace page-id :flags]
|
||||||
|
(fn [flags]
|
||||||
|
(if (contains? flags flag)
|
||||||
|
flags
|
||||||
|
(conj flags flag)))))))
|
||||||
|
|
||||||
(defn activate-flag
|
(defn activate-flag
|
||||||
[flag]
|
[flag]
|
||||||
|
@ -574,7 +578,6 @@
|
||||||
;; --- Apply Displacement
|
;; --- Apply Displacement
|
||||||
|
|
||||||
(defrecord ApplyDisplacement [id]
|
(defrecord ApplyDisplacement [id]
|
||||||
udp/IPageUpdate
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [pid (get-in state [:workspace :current])
|
(let [pid (get-in state [:workspace :current])
|
||||||
|
@ -657,7 +660,6 @@
|
||||||
[]
|
[]
|
||||||
(StartMoveSelected.))
|
(StartMoveSelected.))
|
||||||
|
|
||||||
|
|
||||||
;; --- Start shape "edition mode"
|
;; --- Start shape "edition mode"
|
||||||
|
|
||||||
(defn start-edition-mode
|
(defn start-edition-mode
|
||||||
|
|
|
@ -35,7 +35,8 @@
|
||||||
[uxbox.main.user-events :as uev]
|
[uxbox.main.user-events :as uev]
|
||||||
[uxbox.util.data :refer [classnames]]
|
[uxbox.util.data :refer [classnames]]
|
||||||
[uxbox.util.dom :as dom]
|
[uxbox.util.dom :as dom]
|
||||||
[uxbox.util.geom.point :as gpt]))
|
[uxbox.util.geom.point :as gpt]
|
||||||
|
[uxbox.util.rdnd :as rdnd]))
|
||||||
|
|
||||||
;; --- Workspace
|
;; --- Workspace
|
||||||
|
|
||||||
|
@ -77,8 +78,9 @@
|
||||||
|
|
||||||
(mf/defc workspace
|
(mf/defc workspace
|
||||||
[{:keys [page wst] :as props}]
|
[{:keys [page wst] :as props}]
|
||||||
(let [flags (:flags wst)
|
(let [flags (mf/deref refs/flags)
|
||||||
canvas (mf/use-ref* nil)
|
canvas (mf/use-ref* nil)
|
||||||
|
|
||||||
left-sidebar? (not (empty? (keep flags [:layers :sitemap
|
left-sidebar? (not (empty? (keep flags [:layers :sitemap
|
||||||
:document-history])))
|
:document-history])))
|
||||||
right-sidebar? (not (empty? (keep flags [:icons :drawtools
|
right-sidebar? (not (empty? (keep flags [:icons :drawtools
|
||||||
|
@ -87,6 +89,8 @@
|
||||||
:no-tool-bar-right (not right-sidebar?)
|
:no-tool-bar-right (not right-sidebar?)
|
||||||
:no-tool-bar-left (not left-sidebar?)
|
:no-tool-bar-left (not left-sidebar?)
|
||||||
:scrolling (:viewport-positionig workspace))]
|
:scrolling (:viewport-positionig workspace))]
|
||||||
|
(prn "workspace.render")
|
||||||
|
|
||||||
(mf/use-effect {:deps (:id page)
|
(mf/use-effect {:deps (:id page)
|
||||||
:init #(subscibe canvas page)
|
:init #(subscibe canvas page)
|
||||||
:end unsubscribe})
|
:end unsubscribe})
|
||||||
|
@ -109,43 +113,34 @@
|
||||||
|
|
||||||
;; Rules
|
;; Rules
|
||||||
(when (contains? flags :rules)
|
(when (contains? flags :rules)
|
||||||
[:& horizontal-rule {:zoom (:zoom wst)}])
|
[:& horizontal-rule])
|
||||||
|
|
||||||
(when (contains? flags :rules)
|
(when (contains? flags :rules)
|
||||||
[:& vertical-rule {:zoom (:zoom wst)}])
|
[:& vertical-rule])
|
||||||
|
|
||||||
;; Canvas
|
;; Canvas
|
||||||
[:section.workspace-canvas {:id "workspace-canvas"
|
[:section.workspace-canvas {:id "workspace-canvas" :ref canvas}
|
||||||
:ref canvas}
|
[:& viewport {:page page :key (:id page)}]]]
|
||||||
[:& viewport {:page page
|
|
||||||
:wst wst
|
|
||||||
:key (:id page)}]]]
|
|
||||||
|
|
||||||
;; Aside
|
;; Aside
|
||||||
(when left-sidebar?
|
(when left-sidebar?
|
||||||
[:& left-sidebar {:page page
|
[:& left-sidebar {:page page :flags flags}])
|
||||||
:selected (:selected wst)
|
|
||||||
:flags (:flags wst)}])
|
|
||||||
(when right-sidebar?
|
(when right-sidebar?
|
||||||
[:& right-sidebar {:wst wst :page page}])]]))
|
[:& right-sidebar {:page page :flags flags}])]]))
|
||||||
|
|
||||||
|
(mf/defc workspace-page
|
||||||
|
[{:keys [project-id page-id] :as props}]
|
||||||
|
(let [page-iref (mf/use-memo {:deps #js [project-id page-id]
|
||||||
|
:init #(-> (l/in [:pages page-id])
|
||||||
|
(l/derive st/state))})
|
||||||
|
page (mf/deref page-iref)]
|
||||||
|
|
||||||
;; TODO: consider using `derive-state` instead of `key` for
|
(mf/use-effect
|
||||||
;; performance reasons
|
{:deps #js [project-id page-id]
|
||||||
|
:init #(st/emit! (dw/initialize project-id page-id))})
|
||||||
|
|
||||||
(mf/def workspace-page
|
;; (prn "workspace-page.render" (:id page) props)
|
||||||
:mixins [mf/reactive]
|
|
||||||
:init
|
[:> rdnd/provider {:backend rdnd/html5}
|
||||||
(fn [own {:keys [project-id page-id] :as props}]
|
(when page
|
||||||
(st/emit! (dw/initialize project-id page-id))
|
[:& workspace {:page page}])]))
|
||||||
(assoc own
|
|
||||||
::page-ref (-> (l/in [:pages page-id])
|
|
||||||
(l/derive st/state))
|
|
||||||
::workspace-ref (-> (l/in [:workspace page-id])
|
|
||||||
(l/derive st/state))))
|
|
||||||
:render
|
|
||||||
(fn [own props]
|
|
||||||
(let [wst (mf/react (::workspace-ref own))
|
|
||||||
page (mf/react (::page-ref own))]
|
|
||||||
(when page
|
|
||||||
[:& workspace {:page page :wst wst}]))))
|
|
||||||
|
|
|
@ -13,8 +13,7 @@
|
||||||
[uxbox.main.ui.shapes :as uus]
|
[uxbox.main.ui.shapes :as uus]
|
||||||
[uxbox.main.ui.workspace.drawarea :refer [draw-area]]
|
[uxbox.main.ui.workspace.drawarea :refer [draw-area]]
|
||||||
[uxbox.main.ui.workspace.selection :refer [selection-handlers]]
|
[uxbox.main.ui.workspace.selection :refer [selection-handlers]]
|
||||||
[uxbox.util.geom.point :as gpt])
|
[uxbox.util.geom.point :as gpt]))
|
||||||
(:import goog.events.EventType))
|
|
||||||
|
|
||||||
;; --- Background
|
;; --- Background
|
||||||
|
|
||||||
|
@ -32,7 +31,6 @@
|
||||||
|
|
||||||
(mf/defc canvas
|
(mf/defc canvas
|
||||||
[{:keys [page wst] :as props}]
|
[{:keys [page wst] :as props}]
|
||||||
(prn "canvas")
|
|
||||||
(let [{:keys [metadata id]} page
|
(let [{:keys [metadata id]} page
|
||||||
zoom (:zoom wst 1) ;; NOTE: maybe forward wst to draw-area
|
zoom (:zoom wst 1) ;; NOTE: maybe forward wst to draw-area
|
||||||
width (:width metadata)
|
width (:width metadata)
|
||||||
|
|
|
@ -125,8 +125,10 @@
|
||||||
;; --- Horizontal Rule (Component)
|
;; --- Horizontal Rule (Component)
|
||||||
|
|
||||||
(mf/defc horizontal-rule
|
(mf/defc horizontal-rule
|
||||||
[{:keys [zoom] :as props}]
|
{:wrap [mf/wrap-memo]}
|
||||||
|
[props]
|
||||||
(let [scroll (mf/deref refs/workspace-scroll)
|
(let [scroll (mf/deref refs/workspace-scroll)
|
||||||
|
zoom (mf/deref refs/selected-zoom)
|
||||||
scroll-x (:x scroll)
|
scroll-x (:x scroll)
|
||||||
translate-x (- (- c/canvas-scroll-padding) (:x scroll))]
|
translate-x (- (- c/canvas-scroll-padding) (:x scroll))]
|
||||||
[:svg.horizontal-rule
|
[:svg.horizontal-rule
|
||||||
|
@ -140,8 +142,10 @@
|
||||||
;; --- Vertical Rule (Component)
|
;; --- Vertical Rule (Component)
|
||||||
|
|
||||||
(mf/defc vertical-rule
|
(mf/defc vertical-rule
|
||||||
[{:keys [zoom] :as props}]
|
{:wrap [mf/wrap-memo]}
|
||||||
|
[props]
|
||||||
(let [scroll (mf/deref refs/workspace-scroll)
|
(let [scroll (mf/deref refs/workspace-scroll)
|
||||||
|
zoom (mf/deref refs/selected-zoom)
|
||||||
scroll-y (:y scroll)
|
scroll-y (:y scroll)
|
||||||
translate-y (- (- c/canvas-scroll-padding) (:y scroll))]
|
translate-y (- (- c/canvas-scroll-padding) (:y scroll))]
|
||||||
[:svg.vertical-rule
|
[:svg.vertical-rule
|
||||||
|
|
|
@ -212,19 +212,19 @@
|
||||||
(geom/selection-rect))]
|
(geom/selection-rect))]
|
||||||
[:& controls {:shape shape :zoom zoom :on-click on-click}]))
|
[:& controls {:shape shape :zoom zoom :on-click on-click}]))
|
||||||
|
|
||||||
;; (mx/defc text-edition-selection-handlers
|
(mf/defc text-edition-selection-handlers
|
||||||
;; {:mixins [mx/static]}
|
[{:keys [shape modifiers zoom] :as props}]
|
||||||
;; [{:keys [id] :as shape} zoom]
|
(let [{:keys [x1 y1 width height] :as shape} (-> (assoc shape :modifiers modifiers)
|
||||||
;; (let [{:keys [x1 y1 width height] :as shape} (geom/selection-rect shape)]
|
(geom/selection-rect))]
|
||||||
;; [:g.controls
|
[:g.controls
|
||||||
;; [:rect.main {:x x1 :y y1
|
[:rect.main {:x x1 :y y1
|
||||||
;; :width width
|
:width width
|
||||||
;; :height height
|
:height height
|
||||||
;; ;; :stroke-dasharray (str (/ 5.0 zoom) "," (/ 5 zoom))
|
;; :stroke-dasharray (str (/ 5.0 zoom) "," (/ 5 zoom))
|
||||||
;; :style {:stroke "#333"
|
:style {:stroke "#333"
|
||||||
;; :stroke-width "0.5"
|
:stroke-width "0.5"
|
||||||
;; :stroke-opacity "0.5"
|
:stroke-opacity "0.5"
|
||||||
;; :fill "transparent"}}]]))
|
:fill "transparent"}}]]))
|
||||||
|
|
||||||
(def ^:private shapes-map-iref
|
(def ^:private shapes-map-iref
|
||||||
(-> (l/key :shapes)
|
(-> (l/key :shapes)
|
||||||
|
@ -250,16 +250,18 @@
|
||||||
:modifiers modifiers
|
:modifiers modifiers
|
||||||
:zoom zoom}]
|
:zoom zoom}]
|
||||||
|
|
||||||
;; (and (= type :text) edition?)
|
(and (= type :text)
|
||||||
;; (-> (assoc shape :modifiers (get modifiers id))
|
(= edition? (:id shape)))
|
||||||
;; (text-edition-selection-handlers zoom))
|
[:& text-edition-selection-handlers {:shape shape
|
||||||
|
:modifiers (get modifiers id)
|
||||||
|
:zoom zoom}]
|
||||||
(and (= type :path)
|
(and (= type :path)
|
||||||
(= edition? (:id shape)))
|
(= edition? (:id shape)))
|
||||||
[:& path-edition-selection-handlers {:shape shape
|
[:& path-edition-selection-handlers {:shape shape
|
||||||
:zoom zoom
|
:zoom zoom
|
||||||
:modifiers (get modifiers id)}]
|
:modifiers (get modifiers id)}]
|
||||||
|
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[:& single-selection-handlers {:shape shape
|
[:& single-selection-handlers {:shape shape
|
||||||
:modifiers (get modifiers id)
|
:modifiers (get modifiers id)
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
(mf/defc left-sidebar
|
(mf/defc left-sidebar
|
||||||
{:wrap [mf/wrap-memo]}
|
{:wrap [mf/wrap-memo]}
|
||||||
[{:keys [flags selected page] :as props}]
|
[{:keys [flags page] :as props}]
|
||||||
[:aside#settings-bar.settings-bar.settings-bar-left
|
[:aside#settings-bar.settings-bar.settings-bar-left
|
||||||
[:> rdnd/provider {:backend rdnd/html5}
|
[:> rdnd/provider {:backend rdnd/html5}
|
||||||
[:div.settings-bar-inside
|
[:div.settings-bar-inside
|
||||||
|
@ -31,22 +31,18 @@
|
||||||
#_(when (contains? flags :document-history)
|
#_(when (contains? flags :document-history)
|
||||||
(history-toolbox page-id))
|
(history-toolbox page-id))
|
||||||
(when (contains? flags :layers)
|
(when (contains? flags :layers)
|
||||||
[:& layers-toolbox {:page page
|
[:& layers-toolbox {:page page}])]]])
|
||||||
:selected selected}])]]])
|
|
||||||
|
|
||||||
;; --- Right Sidebar (Component)
|
;; --- Right Sidebar (Component)
|
||||||
|
|
||||||
(mf/defc right-sidebar
|
(mf/defc right-sidebar
|
||||||
[{:keys [wst page] :as props}]
|
[{:keys [flags page] :as props}]
|
||||||
(let [flags (:flags wst)
|
[:aside#settings-bar.settings-bar
|
||||||
dtool (:drawing-tool wst)]
|
[:div.settings-bar-inside
|
||||||
[:aside#settings-bar.settings-bar
|
(when (contains? flags :drawtools)
|
||||||
[:div.settings-bar-inside
|
[:& draw-toolbox {:flags flags}])
|
||||||
(when (contains? flags :drawtools)
|
(when (contains? flags :element-options)
|
||||||
[:& draw-toolbox {:flags flags :drawing-tool dtool}])
|
[:& options-toolbox {:page page}])
|
||||||
(when (contains? flags :element-options)
|
(when (contains? flags :icons)
|
||||||
[:& options-toolbox {:page page
|
#_(icons-toolbox))]])
|
||||||
:selected (:selected wst)}])
|
|
||||||
(when (contains? flags :icons)
|
|
||||||
#_(icons-toolbox))]]))
|
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
[uxbox.main.data.shapes :as uds]
|
[uxbox.main.data.shapes :as uds]
|
||||||
[uxbox.main.data.workspace :as udw]
|
[uxbox.main.data.workspace :as udw]
|
||||||
[uxbox.main.data.workspace-drawing :as udwd]
|
[uxbox.main.data.workspace-drawing :as udwd]
|
||||||
|
[uxbox.main.refs :as refs]
|
||||||
[uxbox.main.store :as st]
|
[uxbox.main.store :as st]
|
||||||
[uxbox.main.user-events :as uev]
|
[uxbox.main.user-events :as uev]
|
||||||
[uxbox.util.i18n :refer (tr)]
|
[uxbox.util.i18n :refer (tr)]
|
||||||
|
@ -79,8 +80,9 @@
|
||||||
|
|
||||||
(mf/defc draw-toolbox
|
(mf/defc draw-toolbox
|
||||||
{:wrap [mf/wrap-memo]}
|
{:wrap [mf/wrap-memo]}
|
||||||
[{:keys [flags drawing-tool] :as props}]
|
[{:keys [flags] :as props}]
|
||||||
(let [close #(st/emit! (udw/toggle-flag :drawtools))
|
(let [close #(st/emit! (udw/toggle-flag :drawtools))
|
||||||
|
dtool (mf/deref refs/selected-drawing-tool)
|
||||||
tools (->> (into [] +draw-tools+)
|
tools (->> (into [] +draw-tools+)
|
||||||
(sort-by (comp :priority second)))
|
(sort-by (comp :priority second)))
|
||||||
|
|
||||||
|
@ -98,7 +100,7 @@
|
||||||
[:div.tool-window-close {:on-click close} i/close]]
|
[:div.tool-window-close {:on-click close} i/close]]
|
||||||
[:div.tool-window-content
|
[:div.tool-window-content
|
||||||
(for [[i props] (map-indexed vector tools)]
|
(for [[i props] (map-indexed vector tools)]
|
||||||
(let [selected? (= drawing-tool (:shape props))]
|
(let [selected? (= dtool (:shape props))]
|
||||||
[:div.tool-btn.tooltip.tooltip-hover
|
[:div.tool-btn.tooltip.tooltip-hover
|
||||||
{:alt (tr (:help props))
|
{:alt (tr (:help props))
|
||||||
:class (when selected? "selected")
|
:class (when selected? "selected")
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
[uxbox.main.data.pages :as udp]
|
[uxbox.main.data.pages :as udp]
|
||||||
[uxbox.main.data.shapes :as uds]
|
[uxbox.main.data.shapes :as uds]
|
||||||
[uxbox.main.data.workspace :as udw]
|
[uxbox.main.data.workspace :as udw]
|
||||||
|
[uxbox.main.refs :as refs]
|
||||||
[uxbox.main.store :as st]
|
[uxbox.main.store :as st]
|
||||||
[uxbox.main.ui.keyboard :as kbd]
|
[uxbox.main.ui.keyboard :as kbd]
|
||||||
[uxbox.main.ui.shapes.icon :as icon]
|
[uxbox.main.ui.shapes.icon :as icon]
|
||||||
|
@ -164,7 +165,8 @@
|
||||||
|
|
||||||
(mf/defc layers-toolbox
|
(mf/defc layers-toolbox
|
||||||
[{:keys [page selected] :as props}]
|
[{:keys [page selected] :as props}]
|
||||||
(let [on-click #(st/emit! (udw/toggle-flag :layers))]
|
(let [on-click #(st/emit! (udw/toggle-flag :layers))
|
||||||
|
selected (mf/deref refs/selected-shapes)]
|
||||||
[:div#layers.tool-window
|
[:div#layers.tool-window
|
||||||
[:div.tool-window-bar
|
[:div.tool-window-bar
|
||||||
[:div.tool-window-icon i/layers]
|
[:div.tool-window-icon i/layers]
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
[uxbox.main.data.workspace :as udw]
|
[uxbox.main.data.workspace :as udw]
|
||||||
[uxbox.main.geom :as geom]
|
[uxbox.main.geom :as geom]
|
||||||
[uxbox.main.store :as st]
|
[uxbox.main.store :as st]
|
||||||
|
[uxbox.main.refs :as refs]
|
||||||
[uxbox.main.ui.shapes.attrs :refer [shape-default-attrs]]
|
[uxbox.main.ui.shapes.attrs :refer [shape-default-attrs]]
|
||||||
[uxbox.main.ui.workspace.sidebar.options.circle-measures :as options-circlem]
|
[uxbox.main.ui.workspace.sidebar.options.circle-measures :as options-circlem]
|
||||||
[uxbox.main.ui.workspace.sidebar.options.fill :as options-fill]
|
[uxbox.main.ui.workspace.sidebar.options.fill :as options-fill]
|
||||||
|
@ -110,7 +111,8 @@
|
||||||
(mf/defc options-toolbox
|
(mf/defc options-toolbox
|
||||||
{:wrap [mf/wrap-memo]}
|
{:wrap [mf/wrap-memo]}
|
||||||
[{:keys [page selected] :as props}]
|
[{:keys [page selected] :as props}]
|
||||||
(let [close #(st/emit! (udw/toggle-flag :element-options))]
|
(let [close #(st/emit! (udw/toggle-flag :element-options))
|
||||||
|
selected (mf/deref refs/selected-shapes)]
|
||||||
[:div.elementa-options.tool-window
|
[:div.elementa-options.tool-window
|
||||||
[:div.tool-window-bar
|
[:div.tool-window-bar
|
||||||
[:div.tool-window-icon i/options]
|
[:div.tool-window-icon i/options]
|
||||||
|
|
|
@ -156,13 +156,14 @@
|
||||||
(events/unlistenByKey (::key3 own))
|
(events/unlistenByKey (::key3 own))
|
||||||
(dissoc own ::key1 ::key2 ::key3))
|
(dissoc own ::key1 ::key2 ::key3))
|
||||||
|
|
||||||
|
:mixins [mf/reactive]
|
||||||
|
|
||||||
:render
|
:render
|
||||||
(fn [own {:keys [page wst] :as props}]
|
(fn [own {:keys [page] :as props}]
|
||||||
(let [{:keys [drawing-tool tooltip zoom flags edition]} wst
|
(let [{:keys [drawing-tool tooltip zoom flags edition] :as wst} (mf/react refs/workspace)
|
||||||
tooltip (or tooltip (get-shape-tooltip drawing-tool))
|
tooltip (or tooltip (get-shape-tooltip drawing-tool))
|
||||||
zoom (or zoom 1)]
|
zoom (or zoom 1)]
|
||||||
(letfn [(on-mouse-down [event]
|
(letfn [(on-mouse-down [event]
|
||||||
(prn "viewport.on-mouse-down")
|
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(let [ctrl? (kbd/ctrl? event)
|
(let [ctrl? (kbd/ctrl? event)
|
||||||
shift? (kbd/shift? event)
|
shift? (kbd/shift? event)
|
||||||
|
@ -189,7 +190,6 @@
|
||||||
:ctrl? ctrl?}]
|
:ctrl? ctrl?}]
|
||||||
(st/emit! (uev/mouse-event :up ctrl? shift?))))
|
(st/emit! (uev/mouse-event :up ctrl? shift?))))
|
||||||
(on-click [event]
|
(on-click [event]
|
||||||
(js/console.log "viewport.on-click" event)
|
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(let [ctrl? (kbd/ctrl? event)
|
(let [ctrl? (kbd/ctrl? event)
|
||||||
shift? (kbd/shift? event)
|
shift? (kbd/shift? event)
|
||||||
|
@ -216,8 +216,7 @@
|
||||||
:on-click on-click
|
:on-click on-click
|
||||||
:on-double-click on-double-click
|
:on-double-click on-double-click
|
||||||
:on-mouse-down on-mouse-down
|
:on-mouse-down on-mouse-down
|
||||||
:on-mouse-up on-mouse-up
|
:on-mouse-up on-mouse-up}
|
||||||
}
|
|
||||||
[:g.zoom {:transform (str "scale(" zoom ", " zoom ")")}
|
[:g.zoom {:transform (str "scale(" zoom ", " zoom ")")}
|
||||||
(when page
|
(when page
|
||||||
[:& canvas {:page page :wst wst}])
|
[:& canvas {:page page :wst wst}])
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue