mirror of
https://github.com/penpot/penpot.git
synced 2026-04-27 12:18:32 +00:00
this allows almost all api operations to success usin application/json encoding with the exception of the update-file, which we need to approach a bit differently; the reason update-file is different, is because the operations vector is right now defined without the context of shape type, so we are just unable to properly parse the value to correct type using the schema decoding mechanism
312 lines
9.7 KiB
Clojure
312 lines
9.7 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.render
|
|
"The main entry point for UI part needed by the exporter."
|
|
(:require
|
|
[app.common.geom.shapes.bounds :as gsb]
|
|
[app.common.logging :as log]
|
|
[app.common.math :as mth]
|
|
[app.common.schema :as sm]
|
|
[app.common.types.components-list :as ctkl]
|
|
[app.common.uri :as u]
|
|
[app.main.data.fonts :as df]
|
|
[app.main.data.users :as du]
|
|
[app.main.features :as features]
|
|
[app.main.render :as render]
|
|
[app.main.repo :as repo]
|
|
[app.main.store :as st]
|
|
[app.util.dom :as dom]
|
|
[app.util.globals :as glob]
|
|
[beicon.v2.core :as rx]
|
|
[cuerdas.core :as str]
|
|
[garden.core :refer [css]]
|
|
[okulary.core :as l]
|
|
[potok.v2.core :as ptk]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(log/setup! {:app :info})
|
|
|
|
(defn- fetch-team
|
|
[& {:keys [file-id]}]
|
|
(ptk/reify ::fetch-team
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(->> (repo/cmd! :get-team {:file-id file-id})
|
|
(rx/mapcat (fn [team]
|
|
(rx/of (du/set-current-team team)
|
|
(ptk/data-event ::team-fetched team))))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; COMPONENTS
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def ^:private ref:objects
|
|
(l/derived :objects st/state))
|
|
|
|
(mf/defc object-svg
|
|
{::mf/wrap-props false}
|
|
[{:keys [object-id embed]}]
|
|
(let [objects (mf/deref ref:objects)]
|
|
|
|
;; Set the globa CSS to assign the page size, needed for PDF
|
|
;; exportation process.
|
|
(mf/with-effect [objects]
|
|
(when-let [object (get objects object-id)]
|
|
(let [{:keys [width height]} (gsb/get-object-bounds [objects] object)]
|
|
(dom/set-page-style!
|
|
{:size (str/concat
|
|
(mth/ceil width) "px "
|
|
(mth/ceil height) "px")}))))
|
|
|
|
(when objects
|
|
[:& render/object-svg
|
|
{:objects objects
|
|
:object-id object-id
|
|
:embed embed}])))
|
|
|
|
(mf/defc objects-svg
|
|
{::mf/wrap-props false}
|
|
[{:keys [object-ids embed]}]
|
|
(when-let [objects (mf/deref ref:objects)]
|
|
(for [object-id object-ids]
|
|
(let [objects (render/adapt-objects-for-shape objects object-id)]
|
|
[:& render/object-svg
|
|
{:objects objects
|
|
:key (str object-id)
|
|
:object-id object-id
|
|
:embed embed}]))))
|
|
|
|
(defn- fetch-objects-bundle
|
|
[& {:keys [file-id page-id share-id object-id] :as options}]
|
|
(ptk/reify ::fetch-objects-bundle
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [features (features/get-team-enabled-features state)]
|
|
(->> (rx/zip
|
|
(repo/cmd! :get-font-variants {:file-id file-id :share-id share-id})
|
|
(repo/cmd! :get-page {:file-id file-id
|
|
:page-id page-id
|
|
:share-id share-id
|
|
:object-id object-id
|
|
:features features}))
|
|
(rx/tap (fn [[fonts]]
|
|
(when (seq fonts)
|
|
(st/emit! (df/fonts-fetched fonts)))))
|
|
(rx/observe-on :async)
|
|
(rx/map (comp :objects second))
|
|
(rx/map (fn [objects]
|
|
(let [objects (render/adapt-objects-for-shape objects object-id)]
|
|
#(assoc % :objects objects)))))))))
|
|
|
|
(def ^:private schema:render-objects
|
|
[:map {:title "render-objets"}
|
|
[:page-id ::sm/uuid]
|
|
[:file-id ::sm/uuid]
|
|
[:share-id {:optional true} ::sm/uuid]
|
|
[:embed {:optional true} :boolean]
|
|
[:object-id
|
|
[:or
|
|
::sm/uuid
|
|
::sm/coll-of-uuid]]])
|
|
|
|
(def ^:private render-objects-decoder
|
|
(sm/lazy-decoder schema:render-objects
|
|
sm/string-transformer))
|
|
|
|
(def ^:private render-objects-validator
|
|
(sm/lazy-validator schema:render-objects))
|
|
|
|
(defn- render-objects
|
|
[params]
|
|
(let [{:keys [file-id page-id embed share-id object-id] :as params} (render-objects-decoder params)]
|
|
(if-not (render-objects-validator params)
|
|
(do
|
|
(js/console.error "invalid arguments")
|
|
(sm/pretty-explain schema:render-objects params)
|
|
nil)
|
|
|
|
(do
|
|
(st/emit! (ptk/reify ::initialize-render-objects
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
(rx/merge
|
|
(rx/of (fetch-team :file-id file-id))
|
|
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::team-fetched))
|
|
(rx/observe-on :async)
|
|
(rx/map (constantly params))
|
|
(rx/map fetch-objects-bundle))))))
|
|
|
|
(if (uuid? object-id)
|
|
(mf/html
|
|
[:& object-svg
|
|
{:file-id file-id
|
|
:page-id page-id
|
|
:share-id share-id
|
|
:object-id object-id
|
|
:embed embed}])
|
|
|
|
(mf/html
|
|
[:& objects-svg
|
|
{:file-id file-id
|
|
:page-id page-id
|
|
:share-id share-id
|
|
:object-ids (into #{} object-id)
|
|
:embed embed}]))))))
|
|
|
|
;; ---- COMPONENTS SPRITE
|
|
|
|
(mf/defc components-svg
|
|
{::mf/wrap-props false}
|
|
[{:keys [embed component-id]}]
|
|
(let [file-ref (mf/with-memo [] (l/derived :file st/state))
|
|
state (mf/use-state {:component-id component-id})]
|
|
(when-let [file (mf/deref file-ref)]
|
|
[:*
|
|
[:style
|
|
(css [[:body
|
|
{:margin 0
|
|
:overflow "hidden"
|
|
:width "100vw"
|
|
:height "100vh"}]
|
|
|
|
[:main
|
|
{:overflow "auto"
|
|
:display "flex"
|
|
:justify-content "center"
|
|
:align-items "center"
|
|
:height "calc(100vh - 200px)"}
|
|
[:svg {:width "50%"
|
|
:height "50%"}]]
|
|
[:.nav
|
|
{:display "flex"
|
|
:margin 0
|
|
:padding "10px"
|
|
:flex-direction "column"
|
|
:flex-wrap "wrap"
|
|
:height "200px"
|
|
:list-style "none"
|
|
:overflow-x "scroll"
|
|
:border-bottom "1px dotted #e6e6e6"}
|
|
[:a {:cursor :pointer
|
|
:text-overflow "ellipsis"
|
|
:white-space "nowrap"
|
|
:overflow "hidden"
|
|
:text-decoration "underline"}]
|
|
[:li {:display "flex"
|
|
:width "150px"
|
|
:padding "5px"
|
|
:border "0px solid black"}]]])]
|
|
|
|
[:ul.nav
|
|
(for [[id data] (ctkl/components (:data file))]
|
|
(let [on-click (fn [event]
|
|
(dom/prevent-default event)
|
|
(swap! state assoc :component-id id))]
|
|
[:li {:key (str id)}
|
|
[:a {:on-click on-click} (:name data)]]))]
|
|
|
|
[:main
|
|
[:& render/components-svg
|
|
{:data (:data file)
|
|
:embed embed}
|
|
|
|
(when-let [component-id (:component-id @state)]
|
|
[:use {:x 0 :y 0 :href (str "#" component-id)}])]]])))
|
|
|
|
(defn- fetch-components-bundle
|
|
[& {:keys [file-id]}]
|
|
(ptk/reify ::fetch-components-bundle
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [features (features/get-team-enabled-features state)]
|
|
(->> (repo/cmd! :get-file {:id file-id :features features})
|
|
(rx/map (fn [file] #(assoc % :file file))))))))
|
|
|
|
(def ^:private schema:render-components
|
|
[:map {:title "render-components"}
|
|
[:file-id ::sm/uuid]
|
|
[:embed {:optional true} :boolean]
|
|
[:component-id {:optional true} ::sm/uuid]])
|
|
|
|
(def ^:private render-components-decoder
|
|
(sm/lazy-decoder schema:render-components
|
|
sm/string-transformer))
|
|
|
|
(def ^:private render-components-validator
|
|
(sm/lazy-validator schema:render-components))
|
|
|
|
(defn render-components
|
|
[params]
|
|
(let [{:keys [file-id component-id embed] :as params} (render-components-decoder params)]
|
|
(if-not (render-components-validator params)
|
|
(do
|
|
(js/console.error "invalid arguments")
|
|
(sm/pretty-explain schema:render-components params)
|
|
nil)
|
|
|
|
(do
|
|
(st/emit! (ptk/reify ::initialize-render-components
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
(rx/merge
|
|
(rx/of (fetch-team :file-id file-id))
|
|
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::team-fetched))
|
|
(rx/observe-on :async)
|
|
(rx/map (constantly params))
|
|
(rx/map fetch-components-bundle))))))
|
|
|
|
(mf/html
|
|
[:& components-svg
|
|
{:component-id component-id
|
|
:embed embed}])))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; SETUP
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defonce app-root
|
|
(let [el (dom/get-element "app")]
|
|
(mf/create-root el)))
|
|
|
|
(declare ^:private render-single-object)
|
|
(declare ^:private render-components)
|
|
(declare ^:private render-objects)
|
|
|
|
(defn- parse-params
|
|
[loc]
|
|
(let [href (unchecked-get loc "href")]
|
|
(some-> href u/uri :query u/query-string->map)))
|
|
|
|
(defn init-ui
|
|
[]
|
|
(when-let [params (parse-params glob/location)]
|
|
(when-let [component (case (:route params)
|
|
"objects" (render-objects params)
|
|
"components" (render-components params)
|
|
nil)]
|
|
(mf/render! app-root component))))
|
|
|
|
(defn ^:export init
|
|
[]
|
|
(st/emit! (features/initialize))
|
|
(init-ui))
|
|
|
|
(defn reinit
|
|
[]
|
|
(init-ui))
|
|
|
|
(defn ^:dev/after-load after-load
|
|
[]
|
|
(reinit))
|
|
|
|
|
|
|