mirror of
https://github.com/penpot/penpot.git
synced 2025-06-03 19:32:52 +02:00
Merge pull request #6309 from penpot/niwinz-staging-bugfixes-2
🐛 Several bugfixes
This commit is contained in:
commit
a72c07b657
8 changed files with 138 additions and 69 deletions
|
@ -9,6 +9,9 @@
|
||||||
- Fix unexpected exception on path editor on merge segments when undo stack is empty
|
- Fix unexpected exception on path editor on merge segments when undo stack is empty
|
||||||
- Fix pricing CTA to be under a config flag [Taiga #10808](https://tree.taiga.io/project/penpot/issue/10808)
|
- Fix pricing CTA to be under a config flag [Taiga #10808](https://tree.taiga.io/project/penpot/issue/10808)
|
||||||
- Fix allow moving a main component into another [Taiga #10818](https://tree.taiga.io/project/penpot/issue/10818)
|
- Fix allow moving a main component into another [Taiga #10818](https://tree.taiga.io/project/penpot/issue/10818)
|
||||||
|
- Fix several issues with internal srepl helpers
|
||||||
|
- Fix unexpected exception on template import from libraries
|
||||||
|
- Fix incorrect uuid parsing from different parts of code
|
||||||
|
|
||||||
## 2.6.1
|
## 2.6.1
|
||||||
|
|
||||||
|
|
|
@ -273,7 +273,7 @@
|
||||||
|
|
||||||
(defn- http-handler
|
(defn- http-handler
|
||||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||||
(let [session-id (some-> params :session-id sm/parse-uuid)]
|
(let [session-id (some-> params :session-id uuid/parse*)]
|
||||||
(when-not (uuid? session-id)
|
(when-not (uuid? session-id)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :missing-session-id
|
:code :missing-session-id
|
||||||
|
|
|
@ -390,20 +390,22 @@
|
||||||
(register! :merge (mu/-merge))
|
(register! :merge (mu/-merge))
|
||||||
(register! :union (mu/-union))
|
(register! :union (mu/-union))
|
||||||
|
|
||||||
(def uuid-rx
|
(defn- parse-uuid
|
||||||
#"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
|
||||||
|
|
||||||
(defn parse-uuid
|
|
||||||
[s]
|
[s]
|
||||||
(try
|
(if (uuid? s)
|
||||||
(uuid/parse s)
|
s
|
||||||
(catch #?(:clj Exception :cljs :default) _cause
|
(if (str/empty? s)
|
||||||
s)))
|
nil
|
||||||
|
(try
|
||||||
|
(uuid/parse s)
|
||||||
|
(catch #?(:clj Exception :cljs :default) _cause
|
||||||
|
s)))))
|
||||||
|
|
||||||
(defn encode-uuid
|
(defn- encode-uuid
|
||||||
[v]
|
[v]
|
||||||
(when (uuid? v)
|
(if (uuid? v)
|
||||||
(str v)))
|
(str v)
|
||||||
|
v))
|
||||||
|
|
||||||
(register!
|
(register!
|
||||||
{:type ::uuid
|
{:type ::uuid
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
(let [smallest (-> params :shrunk :smallest vec)]
|
(let [smallest (-> params :shrunk :smallest vec)]
|
||||||
(println)
|
(println)
|
||||||
(println "Condition failed with the following params:")
|
(println "Condition failed with the following params:")
|
||||||
|
(println "Seed:" (:seed params))
|
||||||
(println)
|
(println)
|
||||||
(pp/pprint smallest)))
|
(pp/pprint smallest)))
|
||||||
|
|
||||||
|
|
|
@ -109,13 +109,27 @@
|
||||||
(def check-animation!
|
(def check-animation!
|
||||||
(sm/check-fn schema:animation))
|
(sm/check-fn schema:animation))
|
||||||
|
|
||||||
|
(def schema:interaction-attrs
|
||||||
|
[:map {:title "InteractionAttrs"}
|
||||||
|
[:action-type {:optional true} [::sm/one-of action-types]]
|
||||||
|
[:event-type {:optional true} [::sm/one-of event-types]]
|
||||||
|
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||||
|
[:preserve-scroll {:optional true} :boolean]
|
||||||
|
[:animation {:optional true} schema:animation]
|
||||||
|
[:overlay-position {:optional true} ::gpt/point]
|
||||||
|
[:overlay-pos-type {:optional true} [::sm/one-of overlay-positioning-types]]
|
||||||
|
[:close-click-outside {:optional true} :boolean]
|
||||||
|
[:background-overlay {:optional true} :boolean]
|
||||||
|
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]
|
||||||
|
[:url {:optional true} :string]])
|
||||||
|
|
||||||
(def schema:navigate-interaction
|
(def schema:navigate-interaction
|
||||||
[:map
|
[:map
|
||||||
[:action-type [:= :navigate]]
|
[:action-type [:= :navigate]]
|
||||||
[:event-type [::sm/one-of event-types]]
|
[:event-type [::sm/one-of event-types]]
|
||||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||||
[:preserve-scroll {:optional true} :boolean]
|
[:preserve-scroll {:optional true} :boolean]
|
||||||
[:animation {:optional true} ::animation]])
|
[:animation {:optional true} schema:animation]])
|
||||||
|
|
||||||
(def schema:open-overlay-interaction
|
(def schema:open-overlay-interaction
|
||||||
[:map
|
[:map
|
||||||
|
@ -126,7 +140,7 @@
|
||||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||||
[:close-click-outside {:optional true} :boolean]
|
[:close-click-outside {:optional true} :boolean]
|
||||||
[:background-overlay {:optional true} :boolean]
|
[:background-overlay {:optional true} :boolean]
|
||||||
[:animation {:optional true} ::animation]
|
[:animation {:optional true} schema:animation]
|
||||||
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
|
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
|
||||||
|
|
||||||
(def schema:toggle-overlay-interaction
|
(def schema:toggle-overlay-interaction
|
||||||
|
@ -138,7 +152,7 @@
|
||||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||||
[:close-click-outside {:optional true} :boolean]
|
[:close-click-outside {:optional true} :boolean]
|
||||||
[:background-overlay {:optional true} :boolean]
|
[:background-overlay {:optional true} :boolean]
|
||||||
[:animation {:optional true} ::animation]
|
[:animation {:optional true} schema:animation]
|
||||||
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
|
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
|
||||||
|
|
||||||
(def schema:close-overlay-interaction
|
(def schema:close-overlay-interaction
|
||||||
|
@ -146,7 +160,7 @@
|
||||||
[:action-type [:= :close-overlay]]
|
[:action-type [:= :close-overlay]]
|
||||||
[:event-type [::sm/one-of event-types]]
|
[:event-type [::sm/one-of event-types]]
|
||||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||||
[:animation {:optional true} ::animation]
|
[:animation {:optional true} schema:animation]
|
||||||
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
|
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
|
||||||
|
|
||||||
(def schema:prev-scren-interaction
|
(def schema:prev-scren-interaction
|
||||||
|
@ -161,21 +175,21 @@
|
||||||
[:url :string]])
|
[:url :string]])
|
||||||
|
|
||||||
(def schema:interaction
|
(def schema:interaction
|
||||||
[:multi {:dispatch :action-type
|
[:and {:title "Interaction"
|
||||||
:title "Interaction"
|
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
|
||||||
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
|
(sg/generator schema:open-overlay-interaction)
|
||||||
(sg/generator schema:open-overlay-interaction)
|
(sg/generator schema:close-overlay-interaction)
|
||||||
(sg/generator schema:close-overlay-interaction)
|
(sg/generator schema:toggle-overlay-interaction)
|
||||||
(sg/generator schema:toggle-overlay-interaction)
|
(sg/generator schema:prev-scren-interaction)
|
||||||
(sg/generator schema:prev-scren-interaction)
|
(sg/generator schema:open-url-interaction))}
|
||||||
(sg/generator schema:open-url-interaction))
|
schema:interaction-attrs
|
||||||
:decode/json #(update % :action-type keyword)}
|
[:multi {:dispatch :action-type}
|
||||||
[:navigate schema:navigate-interaction]
|
[:navigate schema:navigate-interaction]
|
||||||
[:open-overlay schema:open-overlay-interaction]
|
[:open-overlay schema:open-overlay-interaction]
|
||||||
[:toggle-overlay schema:toggle-overlay-interaction]
|
[:toggle-overlay schema:toggle-overlay-interaction]
|
||||||
[:close-overlay schema:close-overlay-interaction]
|
[:close-overlay schema:close-overlay-interaction]
|
||||||
[:prev-screen schema:prev-scren-interaction]
|
[:prev-screen schema:prev-scren-interaction]
|
||||||
[:open-url schema:open-url-interaction]])
|
[:open-url schema:open-url-interaction]]])
|
||||||
|
|
||||||
(sm/register! ::interaction schema:interaction)
|
(sm/register! ::interaction schema:interaction)
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
[app.main.ui.dashboard.file-menu :refer [file-menu*]]
|
[app.main.ui.dashboard.file-menu :refer [file-menu*]]
|
||||||
[app.main.ui.dashboard.import :refer [use-import-file]]
|
[app.main.ui.dashboard.import :refer [use-import-file]]
|
||||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||||
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
|
[app.main.ui.dashboard.placeholder :refer [empty-grid-placeholder* loading-placeholder*]]
|
||||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||||
[app.main.ui.hooks :as h]
|
[app.main.ui.hooks :as h]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
|
@ -511,7 +511,7 @@
|
||||||
:ref node-ref}
|
:ref node-ref}
|
||||||
(cond
|
(cond
|
||||||
(nil? files)
|
(nil? files)
|
||||||
[:& loading-placeholder]
|
[:> loading-placeholder*]
|
||||||
|
|
||||||
(seq files)
|
(seq files)
|
||||||
(for [[index slice] (d/enumerate (partition-all limit files))]
|
(for [[index slice] (d/enumerate (partition-all limit files))]
|
||||||
|
@ -528,12 +528,13 @@
|
||||||
:can-edit can-edit}])])
|
:can-edit can-edit}])])
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[:& empty-placeholder
|
[:> empty-grid-placeholder*
|
||||||
{:limit limit
|
{:limit limit
|
||||||
:can-edit can-edit
|
:can-edit can-edit
|
||||||
:create-fn create-fn
|
:create-fn create-fn
|
||||||
:origin origin
|
:origin origin
|
||||||
:project-id project-id
|
:project-id project-id
|
||||||
|
:team-id team-id
|
||||||
:on-finish-import on-finish-import}])]))
|
:on-finish-import on-finish-import}])]))
|
||||||
|
|
||||||
(mf/defc line-grid-row
|
(mf/defc line-grid-row
|
||||||
|
@ -645,7 +646,7 @@
|
||||||
:on-drop on-drop}
|
:on-drop on-drop}
|
||||||
(cond
|
(cond
|
||||||
(nil? files)
|
(nil? files)
|
||||||
[:& loading-placeholder]
|
[:> loading-placeholder*]
|
||||||
|
|
||||||
(seq files)
|
(seq files)
|
||||||
[:& line-grid-row {:files files
|
[:& line-grid-row {:files files
|
||||||
|
@ -656,10 +657,11 @@
|
||||||
:limit limit}]
|
:limit limit}]
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[:& empty-placeholder
|
[:> empty-grid-placeholder*
|
||||||
{:dragging? @dragging?
|
{:is-dragging @dragging?
|
||||||
:limit limit
|
:limit limit
|
||||||
:can-edit can-edit
|
:can-edit can-edit
|
||||||
:create-fn create-fn
|
:create-fn create-fn
|
||||||
:project-id project-id
|
:project-id project-id
|
||||||
|
:team-id team-id
|
||||||
:on-finish-import on-finish-import}])]))
|
:on-finish-import on-finish-import}])]))
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
[app.main.refs :as refs]
|
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.dashboard.import :as udi]
|
[app.main.ui.dashboard.import :as udi]
|
||||||
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
|
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
|
||||||
|
@ -16,51 +15,92 @@
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
[okulary.core :as l]
|
||||||
[potok.v2.core :as ptk]
|
[potok.v2.core :as ptk]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(mf/defc empty-placeholder-projects*
|
(mf/defc empty-project-placeholder*
|
||||||
{::mf/wrap-props false}
|
{::mf/private true}
|
||||||
[{:keys [on-create on-finish-import project-id] :as props}]
|
[{:keys [on-create on-finish-import project-id]}]
|
||||||
(let [file-input (mf/use-ref nil)
|
(let [file-input (mf/use-ref nil)
|
||||||
on-add-library (mf/use-fn
|
|
||||||
(fn [_]
|
on-add-library
|
||||||
(st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click"
|
(mf/use-fn
|
||||||
::ev/origin "dashboard"
|
(fn [_]
|
||||||
:section "empty-placeholder-projects"}))
|
(st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click"
|
||||||
(dom/open-new-window "https://penpot.app/penpothub/libraries-templates")))
|
::ev/origin "dashboard"
|
||||||
on-import-files (mf/use-fn #(dom/click (mf/ref-val file-input)))]
|
:section "empty-placeholder-projects"}))
|
||||||
|
(dom/open-new-window "https://penpot.app/penpothub/libraries-templates")))
|
||||||
|
|
||||||
|
on-import
|
||||||
|
(mf/use-fn #(dom/click (mf/ref-val file-input)))]
|
||||||
|
|
||||||
[:div {:class (stl/css :empty-project-container)}
|
[:div {:class (stl/css :empty-project-container)}
|
||||||
[:div {:class (stl/css :empty-project-card) :on-click on-create :title (tr "dashboard.add-file")}
|
[:div {:class (stl/css :empty-project-card)
|
||||||
[:div {:class (stl/css :empty-project-card-title)} (tr "dashboard.empty-project.create")]
|
:on-click on-create
|
||||||
[:div {:class (stl/css :empty-project-card-subtitle)} (tr "dashboard.empty-project.start")]]
|
:title (tr "dashboard.add-file")}
|
||||||
|
[:div {:class (stl/css :empty-project-card-title)}
|
||||||
|
(tr "dashboard.empty-project.create")]
|
||||||
|
[:div {:class (stl/css :empty-project-card-subtitle)}
|
||||||
|
(tr "dashboard.empty-project.start")]]
|
||||||
|
|
||||||
[:div {:class (stl/css :empty-project-card) :on-click on-import-files :title (tr "dashboard.empty-project.import")}
|
[:div {:class (stl/css :empty-project-card)
|
||||||
[:div {:class (stl/css :empty-project-card-title)} (tr "dashboard.empty-project.import")]
|
:on-click on-import
|
||||||
[:div {:class (stl/css :empty-project-card-subtitle)} (tr "dashboard.empty-project.import-penpot")]]
|
:title (tr "dashboard.empty-project.import")}
|
||||||
|
[:div {:class (stl/css :empty-project-card-title)}
|
||||||
|
(tr "dashboard.empty-project.import")]
|
||||||
|
[:div {:class (stl/css :empty-project-card-subtitle)}
|
||||||
|
(tr "dashboard.empty-project.import-penpot")]]
|
||||||
|
|
||||||
[:div {:class (stl/css :empty-project-card) :on-click on-add-library :title (tr "dashboard.empty-project.go-to-libraries")}
|
[:div {:class (stl/css :empty-project-card)
|
||||||
[:div {:class (stl/css :empty-project-card-title)} (tr "dashboard.empty-project.add-library")]
|
:on-click on-add-library
|
||||||
[:div {:class (stl/css :empty-project-card-subtitle)} (tr "dashboard.empty-project.explore")]]
|
:title (tr "dashboard.empty-project.go-to-libraries")}
|
||||||
|
[:div {:class (stl/css :empty-project-card-title)}
|
||||||
|
(tr "dashboard.empty-project.add-library")]
|
||||||
|
[:div {:class (stl/css :empty-project-card-subtitle)}
|
||||||
|
(tr "dashboard.empty-project.explore")]]
|
||||||
|
|
||||||
[:& udi/import-form {:ref file-input
|
[:& udi/import-form {:ref file-input
|
||||||
:project-id project-id
|
:project-id project-id
|
||||||
:on-finish-import on-finish-import}]]))
|
:on-finish-import on-finish-import}]]))
|
||||||
|
|
||||||
(mf/defc empty-placeholder
|
(defn- make-has-other-files-or-projects-ref
|
||||||
[{:keys [dragging? limit origin create-fn can-edit project-id on-finish-import]}]
|
"Return a ref that resolves to true or false if there are at least some
|
||||||
|
file or some project (a part of the default) exists; this determines
|
||||||
|
if we need to show a complete placeholder or the small one."
|
||||||
|
[team-id]
|
||||||
|
(l/derived (fn [state]
|
||||||
|
(or (let [projects (get state :projects)]
|
||||||
|
(some (fn [[_ project]]
|
||||||
|
(and (= (:team-id project) team-id)
|
||||||
|
(not (:is-default project))))
|
||||||
|
projects))
|
||||||
|
(let [files (get state :files)]
|
||||||
|
(some (fn [[_ file]]
|
||||||
|
(= (:team-id file) team-id))
|
||||||
|
files))))
|
||||||
|
st/state))
|
||||||
|
|
||||||
|
(mf/defc empty-grid-placeholder*
|
||||||
|
[{:keys [is-dragging limit origin create-fn can-edit team-id project-id on-finish-import]}]
|
||||||
(let [on-click
|
(let [on-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps create-fn)
|
(mf/deps create-fn)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(create-fn "dashboard:empty-folder-placeholder")))
|
(create-fn "dashboard:empty-folder-placeholder")))
|
||||||
show-text (mf/use-state nil)
|
|
||||||
on-mouse-enter (mf/use-fn #(reset! show-text true))
|
show-text* (mf/use-state nil)
|
||||||
on-mouse-leave (mf/use-fn #(reset! show-text nil))
|
show-text? (deref show-text*)
|
||||||
files (mf/deref refs/files)]
|
|
||||||
|
on-mouse-enter (mf/use-fn #(reset! show-text* true))
|
||||||
|
on-mouse-leave (mf/use-fn #(reset! show-text* nil))
|
||||||
|
|
||||||
|
has-other* (mf/with-memo [team-id]
|
||||||
|
(make-has-other-files-or-projects-ref team-id))
|
||||||
|
has-other? (mf/deref has-other*)]
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
(true? dragging?)
|
(true? is-dragging)
|
||||||
[:ul
|
[:ul
|
||||||
{:class (stl/css :grid-row :no-wrap)
|
{:class (stl/css :grid-row :no-wrap)
|
||||||
:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}}
|
:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}}
|
||||||
|
@ -80,18 +120,24 @@
|
||||||
:tag-name "span"}])]
|
:tag-name "span"}])]
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(if (= (count files) 0)
|
(if-not has-other?
|
||||||
[:> empty-placeholder-projects* {:on-create on-click :on-finish-import on-finish-import :project-id project-id}]
|
[:> empty-project-placeholder*
|
||||||
|
{:on-create on-click
|
||||||
|
:on-finish-import on-finish-import
|
||||||
|
:project-id project-id}]
|
||||||
[:div {:class (stl/css :grid-empty-placeholder)}
|
[:div {:class (stl/css :grid-empty-placeholder)}
|
||||||
[:button {:class (stl/css :create-new)
|
[:button {:class (stl/css :create-new)
|
||||||
:on-click on-click
|
:on-click on-click
|
||||||
:on-mouse-enter on-mouse-enter
|
:on-mouse-enter on-mouse-enter
|
||||||
:on-mouse-leave on-mouse-leave}
|
:on-mouse-leave on-mouse-leave}
|
||||||
(if @show-text (tr "dashboard.empty-project.create") i/add)]]))))
|
(if show-text?
|
||||||
|
(tr "dashboard.empty-project.create")
|
||||||
|
i/add)]]))))
|
||||||
|
|
||||||
(mf/defc loading-placeholder
|
(mf/defc loading-placeholder*
|
||||||
[]
|
[]
|
||||||
[:> loader* {:width 32
|
[:> loader* {:width 32
|
||||||
:title (tr "labels.loading")
|
:title (tr "labels.loading")
|
||||||
:class (stl/css :placeholder-loader)}
|
:class (stl/css :placeholder-loader)}
|
||||||
[:span {:class (stl/css :placeholder-text)} (tr "dashboard.loading-files")]])
|
[:span {:class (stl/css :placeholder-text)}
|
||||||
|
(tr "dashboard.loading-files")]])
|
||||||
|
|
|
@ -371,6 +371,7 @@
|
||||||
show-team-hero?
|
show-team-hero?
|
||||||
can-invite))}
|
can-invite))}
|
||||||
(for [{:keys [id] :as project} projects]
|
(for [{:keys [id] :as project} projects]
|
||||||
|
;; FIXME: refactor this, looks inneficient
|
||||||
(let [files (when recent-map
|
(let [files (when recent-map
|
||||||
(->> (vals recent-map)
|
(->> (vals recent-map)
|
||||||
(filterv #(= id (:project-id %)))
|
(filterv #(= id (:project-id %)))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue