mirror of
https://github.com/penpot/penpot.git
synced 2025-06-05 08:51:38 +02:00
🎉 Add common
directory tree for code sharing between front and back.
This commit is contained in:
parent
ca3a42f680
commit
5b96e1e9fd
14 changed files with 288 additions and 41 deletions
|
@ -44,7 +44,7 @@
|
||||||
|
|
||||||
mount/mount {:mvn/version "0.1.16"}
|
mount/mount {:mvn/version "0.1.16"}
|
||||||
environ/environ {:mvn/version "1.1.0"}}
|
environ/environ {:mvn/version "1.1.0"}}
|
||||||
:paths ["src" "vendor" "resources"]
|
:paths ["src" "vendor" "resources" "../common"]
|
||||||
:aliases
|
:aliases
|
||||||
{:dev
|
{:dev
|
||||||
{:extra-deps
|
{:extra-deps
|
||||||
|
|
|
@ -43,8 +43,8 @@
|
||||||
width 200
|
width 200
|
||||||
height 200}
|
height 200}
|
||||||
:as opts}]
|
:as opts}]
|
||||||
{:pre [(us/valid? ::thumbnail-opts opts)
|
(s/assert ::thumbnail-opts opts)
|
||||||
(fs/path? input)]}
|
(s/assert fs/path? input)
|
||||||
(let [tmp (fs/create-tempfile :suffix (str "." format))
|
(let [tmp (fs/create-tempfile :suffix (str "." format))
|
||||||
opr (doto (IMOperation.)
|
opr (doto (IMOperation.)
|
||||||
(.addImage)
|
(.addImage)
|
||||||
|
@ -60,9 +60,7 @@
|
||||||
|
|
||||||
(defn make-thumbnail
|
(defn make-thumbnail
|
||||||
[input {:keys [width height format quality] :as opts}]
|
[input {:keys [width height format quality] :as opts}]
|
||||||
{:pre [(us/valid? ::thumbnail-opts opts)
|
(s/assert ::thumbnail-opts opts)
|
||||||
(or (string? input)
|
|
||||||
(fs/path input))]}
|
|
||||||
(let [[filename ext] (fs/split-ext (fs/name input))
|
(let [[filename ext] (fs/split-ext (fs/name input))
|
||||||
suffix (->> [width height quality format]
|
suffix (->> [width height quality format]
|
||||||
(interpose ".")
|
(interpose ".")
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
(s/def ::name ::us/string)
|
(s/def ::name ::us/string)
|
||||||
(s/def ::path ::us/string)
|
(s/def ::path ::us/string)
|
||||||
(s/def ::regex us/regex?)
|
(s/def ::regex #(instance? java.util.regex.Pattern %))
|
||||||
(s/def ::import-item
|
(s/def ::import-item
|
||||||
(s/keys :req-un [::name ::path ::regex]))
|
(s/keys :req-un [::name ::path ::regex]))
|
||||||
|
|
||||||
|
|
|
@ -13,22 +13,22 @@
|
||||||
[uxbox.services.mutations.project-files :as files]
|
[uxbox.services.mutations.project-files :as files]
|
||||||
[uxbox.services.queries.project-pages :refer [decode-row]]
|
[uxbox.services.queries.project-pages :refer [decode-row]]
|
||||||
[uxbox.services.util :as su]
|
[uxbox.services.util :as su]
|
||||||
|
[uxbox.common.pages :as cp]
|
||||||
|
[uxbox.common.spec :as cs]
|
||||||
|
[uxbox.util.exceptions :as ex]
|
||||||
[uxbox.util.blob :as blob]
|
[uxbox.util.blob :as blob]
|
||||||
[uxbox.util.spec :as us]
|
|
||||||
[uxbox.util.sql :as sql]
|
[uxbox.util.sql :as sql]
|
||||||
[uxbox.util.uuid :as uuid]))
|
[uxbox.util.uuid :as uuid]))
|
||||||
|
|
||||||
;; --- Helpers & Specs
|
;; --- Helpers & Specs
|
||||||
|
|
||||||
;; TODO: validate `:data` and `:metadata`
|
(s/def ::id ::cs/uuid)
|
||||||
|
(s/def ::name ::cs/string)
|
||||||
(s/def ::id ::us/uuid)
|
(s/def ::data ::cp/data)
|
||||||
(s/def ::name ::us/string)
|
(s/def ::user ::cs/uuid)
|
||||||
(s/def ::data any?)
|
(s/def ::project-id ::cs/uuid)
|
||||||
(s/def ::user ::us/uuid)
|
(s/def ::metadata ::cp/metadata)
|
||||||
(s/def ::project-id ::us/uuid)
|
(s/def ::ordering ::cs/number)
|
||||||
(s/def ::metadata any?)
|
|
||||||
(s/def ::ordering ::us/number)
|
|
||||||
|
|
||||||
;; --- Mutation: Create Page
|
;; --- Mutation: Create Page
|
||||||
|
|
||||||
|
|
|
@ -26,9 +26,6 @@
|
||||||
Buffer
|
Buffer
|
||||||
(->bytes [data] (.getBytes ^Buffer data))
|
(->bytes [data] (.getBytes ^Buffer data))
|
||||||
|
|
||||||
;; org.jooq.JSONB
|
|
||||||
;; (->bytes [data] (->bytes (.toString data)))
|
|
||||||
|
|
||||||
String
|
String
|
||||||
(->bytes [data] (.getBytes ^String data "UTF-8")))
|
(->bytes [data] (.getBytes ^String data "UTF-8")))
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
(:require [clojure.walk :as walk]
|
(:require [clojure.walk :as walk]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
;; TODO: move to uxbox.common.helpers
|
||||||
|
|
||||||
(defn dissoc-in
|
(defn dissoc-in
|
||||||
[m [k & ks :as keys]]
|
[m [k & ks :as keys]]
|
||||||
(if ks
|
(if ks
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
"A helpers for work with exceptions."
|
"A helpers for work with exceptions."
|
||||||
(:require [clojure.spec.alpha :as s]))
|
(:require [clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
;; TODO: moved to uxbox.common.exceptions
|
||||||
|
|
||||||
(s/def ::type keyword?)
|
(s/def ::type keyword?)
|
||||||
(s/def ::code keyword?)
|
(s/def ::code keyword?)
|
||||||
(s/def ::mesage string?)
|
(s/def ::mesage string?)
|
||||||
|
|
|
@ -65,10 +65,6 @@
|
||||||
(with-open [input (ByteArrayInputStream. data)]
|
(with-open [input (ByteArrayInputStream. data)]
|
||||||
(read! (reader input opts)))
|
(read! (reader input opts)))
|
||||||
|
|
||||||
;; ;; TODO: temporal
|
|
||||||
;; (instance? org.jooq.JSONB data)
|
|
||||||
;; (decode (.toString data) opts)
|
|
||||||
|
|
||||||
(string? data)
|
(string? data)
|
||||||
(decode (.getBytes data "UTF-8") opts)
|
(decode (.getBytes data "UTF-8") opts)
|
||||||
|
|
||||||
|
|
|
@ -12,19 +12,17 @@
|
||||||
[clojure.pprint :refer [pprint]]
|
[clojure.pprint :refer [pprint]]
|
||||||
[clojure.test :as test]
|
[clojure.test :as test]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
|
[clojure.repl :refer :all]
|
||||||
[criterium.core :refer [quick-bench bench with-progress-reporting]]
|
[criterium.core :refer [quick-bench bench with-progress-reporting]]
|
||||||
[expound.alpha :as expound]
|
[expound.alpha :as expound]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[sieppari.core :as sp]
|
;; [sieppari.core :as sp]
|
||||||
[sieppari.context :as spx]
|
;; [sieppari.context :as spx]
|
||||||
[buddy.core.codecs :as codecs]
|
;; [buddy.core.codecs :as codecs]
|
||||||
[buddy.core.codecs.base64 :as b64]
|
;; [buddy.core.codecs.base64 :as b64]
|
||||||
[buddy.core.nonce :as nonce]
|
;; [buddy.core.nonce :as nonce]
|
||||||
[mount.core :as mount]
|
[mount.core :as mount]
|
||||||
[uxbox.main]
|
[uxbox.main]))
|
||||||
[uxbox.util.sql :as sql]
|
|
||||||
[uxbox.util.blob :as blob])
|
|
||||||
(:gen-class))
|
|
||||||
|
|
||||||
(defmacro run-quick-bench
|
(defmacro run-quick-bench
|
||||||
[& exprs]
|
[& exprs]
|
||||||
|
@ -51,11 +49,11 @@
|
||||||
|
|
||||||
;; --- Development Stuff
|
;; --- Development Stuff
|
||||||
|
|
||||||
(defn- make-secret
|
;; (defn- make-secret
|
||||||
[]
|
;; []
|
||||||
(-> (nonce/random-bytes 64)
|
;; (-> (nonce/random-bytes 64)
|
||||||
(b64/encode true)
|
;; (b64/encode true)
|
||||||
(codecs/bytes->str)))
|
;; (codecs/bytes->str)))
|
||||||
|
|
||||||
(defn- start
|
(defn- start
|
||||||
[]
|
[]
|
||||||
|
|
32
common/uxbox/common/exceptions.cljc
Normal file
32
common/uxbox/common/exceptions.cljc
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
;; 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) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
|
||||||
|
(ns uxbox.util.exceptions
|
||||||
|
"A helpers for work with exceptions."
|
||||||
|
(:require [clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
(s/def ::type keyword?)
|
||||||
|
(s/def ::code keyword?)
|
||||||
|
(s/def ::mesage string?)
|
||||||
|
(s/def ::hint string?)
|
||||||
|
|
||||||
|
(s/def ::error
|
||||||
|
(s/keys :req-un [::type]
|
||||||
|
:opt-un [::code
|
||||||
|
::hint
|
||||||
|
::mesage]))
|
||||||
|
|
||||||
|
(defn error
|
||||||
|
[& {:keys [type code message hint cause] :as params}]
|
||||||
|
(s/assert ::error params)
|
||||||
|
(let [message (or message hint "")
|
||||||
|
payload (dissoc params :cause :message)]
|
||||||
|
(ex-info message payload cause)))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(defmacro raise
|
||||||
|
[& args]
|
||||||
|
`(throw (error ~@args))))
|
96
common/uxbox/common/pages.cljc
Normal file
96
common/uxbox/common/pages.cljc
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
(ns uxbox.common.pages
|
||||||
|
"A common (clj/cljs) functions and specs for pages."
|
||||||
|
(:require
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[uxbox.common.spec :as cs]))
|
||||||
|
|
||||||
|
;; --- Specs
|
||||||
|
|
||||||
|
(s/def ::id ::cs/uuid)
|
||||||
|
(s/def ::name string?)
|
||||||
|
(s/def ::type keyword?)
|
||||||
|
|
||||||
|
;; Metadata related
|
||||||
|
(s/def ::grid-x-axis ::cs/number)
|
||||||
|
(s/def ::grid-y-axis ::cs/number)
|
||||||
|
(s/def ::grid-color string?)
|
||||||
|
(s/def ::background string?)
|
||||||
|
(s/def ::background-opacity ::cs/number)
|
||||||
|
|
||||||
|
;; Page related
|
||||||
|
(s/def ::file-id ::cs/uuid)
|
||||||
|
(s/def ::user ::cs/uuid)
|
||||||
|
(s/def ::created-at ::cs/inst)
|
||||||
|
(s/def ::modified-at ::cs/inst)
|
||||||
|
(s/def ::version ::cs/number)
|
||||||
|
(s/def ::ordering ::cs/number)
|
||||||
|
|
||||||
|
;; Page Data related
|
||||||
|
(s/def ::shape
|
||||||
|
(s/keys :req-un [::type ::name]
|
||||||
|
:opt-un [::id]))
|
||||||
|
|
||||||
|
(s/def ::shapes (s/coll-of ::cs/uuid :kind vector?))
|
||||||
|
(s/def ::canvas (s/coll-of ::cs/uuid :kind vector?))
|
||||||
|
|
||||||
|
(s/def ::shapes-by-id
|
||||||
|
(s/map-of uuid? ::shape))
|
||||||
|
|
||||||
|
;; Main
|
||||||
|
|
||||||
|
(s/def ::data
|
||||||
|
(s/keys :req-un [::shapes ::canvas ::shapes-by-id]))
|
||||||
|
|
||||||
|
|
||||||
|
(s/def ::metadata
|
||||||
|
(s/keys :opt-un [::grid-y-axis
|
||||||
|
::grid-x-axis
|
||||||
|
::grid-color
|
||||||
|
::background
|
||||||
|
::background-opacity]))
|
||||||
|
|
||||||
|
(s/def ::opeation
|
||||||
|
(s/or :mod-shape (s/cat :name #(= % :mod-shape)
|
||||||
|
:id uuid?
|
||||||
|
:attr keyword?
|
||||||
|
:value any?)
|
||||||
|
:add-shape (s/cat :name #(= % :add-shape)
|
||||||
|
:id uuid?
|
||||||
|
:data any?)
|
||||||
|
:del-shape (s/cat :name #(= % :del-shape)
|
||||||
|
:id uuid?)))
|
||||||
|
|
||||||
|
;; --- Operations Processing Impl
|
||||||
|
|
||||||
|
(declare process-operation)
|
||||||
|
(declare process-add-shape)
|
||||||
|
(declare process-mod-shape)
|
||||||
|
(declare process-del-shape)
|
||||||
|
|
||||||
|
(defn process-ops
|
||||||
|
[data operations]
|
||||||
|
(reduce process-operation data operations))
|
||||||
|
|
||||||
|
(defn- process-operation
|
||||||
|
[data operation]
|
||||||
|
(case (first operation)
|
||||||
|
:add-shape (process-add-shape data operation)
|
||||||
|
:mod-shape (process-mod-shape data operation)
|
||||||
|
:del-shape (process-del-shape data operation)))
|
||||||
|
|
||||||
|
(defn- process-add-shape
|
||||||
|
[data {:keys [id data]}]
|
||||||
|
(-> data
|
||||||
|
(update :shapes conj id)
|
||||||
|
(update :shapes-by-id assoc id data)))
|
||||||
|
|
||||||
|
(defn- process-mod-shape
|
||||||
|
[data {:keys [id attr value]}]
|
||||||
|
(update-in data [:shapes-by-id id] assoc attr value))
|
||||||
|
|
||||||
|
(defn- process-del-shape
|
||||||
|
[data {:keys [id attr value]}]
|
||||||
|
(-> data
|
||||||
|
(update :shapes (fn [s] (filterv #(not= % id) s)))
|
||||||
|
(update :shapes-by-id dissoc id)))
|
||||||
|
|
120
common/uxbox/common/spec.cljc
Normal file
120
common/uxbox/common/spec.cljc
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
;; 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) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
|
||||||
|
(ns uxbox.common.spec
|
||||||
|
(:require
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
#?(:clj [datoteka.core :as fs])))
|
||||||
|
|
||||||
|
(s/check-asserts true)
|
||||||
|
|
||||||
|
;; --- Constants
|
||||||
|
|
||||||
|
(def email-rx
|
||||||
|
#"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
|
||||||
|
|
||||||
|
(def uuid-rx
|
||||||
|
#"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
|
||||||
|
|
||||||
|
(def number-rx
|
||||||
|
#"^[+-]?([0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)([eE][+-]?[0-9]+)?$")
|
||||||
|
|
||||||
|
;; --- Predicates
|
||||||
|
|
||||||
|
(defn email?
|
||||||
|
[v]
|
||||||
|
(and (string? v)
|
||||||
|
(re-matches email-rx v)))
|
||||||
|
|
||||||
|
;; --- Conformers
|
||||||
|
|
||||||
|
(defn- uuid-conformer
|
||||||
|
[v]
|
||||||
|
(cond
|
||||||
|
(uuid? v) v
|
||||||
|
(string? v)
|
||||||
|
(cond
|
||||||
|
(re-matches uuid-rx v)
|
||||||
|
#?(:clj (java.util.UUID/fromString v)
|
||||||
|
:cljs (uuid v))
|
||||||
|
|
||||||
|
(str/empty? v)
|
||||||
|
nil
|
||||||
|
|
||||||
|
:else
|
||||||
|
::s/invalid)
|
||||||
|
:else ::s/invalid))
|
||||||
|
|
||||||
|
(defn- integer-conformer
|
||||||
|
[v]
|
||||||
|
(cond
|
||||||
|
(integer? v) v
|
||||||
|
(string? v)
|
||||||
|
(if (re-matches #"^[-+]?\d+$" v)
|
||||||
|
(Long/parseLong v)
|
||||||
|
::s/invalid)
|
||||||
|
:else ::s/invalid))
|
||||||
|
|
||||||
|
(defn boolean-conformer
|
||||||
|
[v]
|
||||||
|
(cond
|
||||||
|
(boolean? v) v
|
||||||
|
(string? v)
|
||||||
|
(if (re-matches #"^(?:t|true|false|f|0|1)$" v)
|
||||||
|
(contains? #{"t" "true" "1"} v)
|
||||||
|
::s/invalid)
|
||||||
|
:else ::s/invalid))
|
||||||
|
|
||||||
|
(defn boolean-unformer
|
||||||
|
[v]
|
||||||
|
(if v "true" "false"))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(defn path-conformer
|
||||||
|
[v]
|
||||||
|
(cond
|
||||||
|
(string? v) (fs/path v)
|
||||||
|
(fs/path? v) v
|
||||||
|
:else ::s/invalid)))
|
||||||
|
|
||||||
|
(defn- number-conformer
|
||||||
|
[v]
|
||||||
|
(cond
|
||||||
|
(number? v) v
|
||||||
|
|
||||||
|
(str/numeric? v)
|
||||||
|
#?(:clj (Double/parseDouble v)
|
||||||
|
:cljs (js/parseFloat v))
|
||||||
|
|
||||||
|
:else ::s/invalid))
|
||||||
|
|
||||||
|
;; --- Default Specs
|
||||||
|
|
||||||
|
(s/def ::string string?)
|
||||||
|
(s/def ::integer (s/conformer integer-conformer str))
|
||||||
|
(s/def ::uuid (s/conformer uuid-conformer str))
|
||||||
|
(s/def ::boolean (s/conformer boolean-conformer boolean-unformer))
|
||||||
|
(s/def ::number (s/conformer number-conformer str))
|
||||||
|
|
||||||
|
(s/def ::inst inst?)
|
||||||
|
(s/def ::positive pos?)
|
||||||
|
(s/def ::negative neg?)
|
||||||
|
(s/def ::uploaded-file any?)
|
||||||
|
(s/def ::email email?)
|
||||||
|
(s/def ::file any?)
|
||||||
|
|
||||||
|
;; Clojure Specific
|
||||||
|
#?(:clj
|
||||||
|
(do
|
||||||
|
(s/def ::bytes bytes?)
|
||||||
|
(s/def ::name ::string)
|
||||||
|
(s/def ::size ::integer)
|
||||||
|
(s/def ::mtype ::string)
|
||||||
|
(s/def ::path (s/conformer path-conformer str))
|
||||||
|
(s/def ::upload
|
||||||
|
(s/keys :req-un [::name ::path ::size ::mtype]))))
|
||||||
|
|
|
@ -30,12 +30,14 @@
|
||||||
(s/def ::version ::us/number)
|
(s/def ::version ::us/number)
|
||||||
(s/def ::width (s/and ::us/number ::us/positive))
|
(s/def ::width (s/and ::us/number ::us/positive))
|
||||||
(s/def ::height (s/and ::us/number ::us/positive))
|
(s/def ::height (s/and ::us/number ::us/positive))
|
||||||
|
|
||||||
(s/def ::grid-x-axis ::us/number)
|
(s/def ::grid-x-axis ::us/number)
|
||||||
(s/def ::grid-y-axis ::us/number)
|
(s/def ::grid-y-axis ::us/number)
|
||||||
(s/def ::grid-color ::us/string)
|
(s/def ::grid-color ::us/string)
|
||||||
(s/def ::ordering ::us/number)
|
|
||||||
(s/def ::background ::us/string)
|
(s/def ::background ::us/string)
|
||||||
(s/def ::background-opacity ::us/number)
|
(s/def ::background-opacity ::us/number)
|
||||||
|
|
||||||
|
(s/def ::ordering ::us/number)
|
||||||
(s/def ::user ::us/uuid)
|
(s/def ::user ::us/uuid)
|
||||||
|
|
||||||
(s/def ::metadata
|
(s/def ::metadata
|
||||||
|
@ -45,12 +47,14 @@
|
||||||
::background
|
::background
|
||||||
::background-opacity]))
|
::background-opacity]))
|
||||||
|
|
||||||
|
;; TODO: start using uxbox.common.pagedata/data spec ...
|
||||||
|
|
||||||
(s/def ::minimal-shape
|
(s/def ::minimal-shape
|
||||||
(s/keys :req-un [::type ::name]
|
(s/keys :req-un [::type ::name]
|
||||||
:opt-un [::id]))
|
:opt-un [::id]))
|
||||||
|
|
||||||
(s/def ::shapes (s/every ::us/uuid :kind vector? :into []))
|
(s/def ::shapes (s/coll-of ::us/uuid :kind vector?))
|
||||||
(s/def ::canvas (s/every ::us/uuid :kind vector? :into []))
|
(s/def ::canvas (s/coll-of ::us/uuid :kind vector?))
|
||||||
|
|
||||||
(s/def ::shapes-by-id
|
(s/def ::shapes-by-id
|
||||||
(s/map-of ::us/uuid ::minimal-shape))
|
(s/map-of ::us/uuid ::minimal-shape))
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
(:require [cljs.reader :as r]
|
(:require [cljs.reader :as r]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
;; TODO: partially move to uxbox.common.helpers
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Data structure manipulation
|
;; Data structure manipulation
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue