Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2025-02-18 17:28:38 +01:00
commit f5c913d26e
14 changed files with 349 additions and 210 deletions

View file

@ -228,19 +228,9 @@
[:objects-storage-s3-endpoint {:optional true} ::sm/uri] [:objects-storage-s3-endpoint {:optional true} ::sm/uri]
[:objects-storage-s3-io-threads {:optional true} ::sm/int]])) [:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
(def default-flags
[:enable-backend-api-doc
:enable-backend-openapi-doc
:enable-backend-worker
:enable-secure-session-cookies
:enable-email-verification
:enable-v2-migration])
(defn- parse-flags (defn- parse-flags
[config] [config]
(flags/parse flags/default (flags/parse flags/default (:flags config)))
default-flags
(:flags config)))
(defn read-env (defn read-env
[prefix] [prefix]

View file

@ -143,7 +143,7 @@
(keep flag->feature)) (keep flag->feature))
(defn get-enabled-features (defn get-enabled-features
"Get the globally enabled fratures set." "Get the globally enabled features set."
[flags] [flags]
(into default-features xf-flag-to-feature flags)) (into default-features xf-flag-to-feature flags))

View file

@ -7,13 +7,145 @@
(ns app.common.flags (ns app.common.flags
"Flags parsing algorithm." "Flags parsing algorithm."
(:require (:require
[clojure.set :as set]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(def login
"Flags related to login features"
#{;; Allows registration with login / password
;; if disabled, it's still possible to register/login with providers
:registration
;; Redundant flag. TODO: remove it
:login
;; enables the section of Access Tokens on profile.
:access-tokens
;; Uses email and password as credentials.
:login-with-password
;; Uses Github authentication as credentials.
:login-with-github
;; Uses GitLab authentication as credentials.
:login-with-gitlab
;; Uses Google/Gmail authentication as credentials.
:login-with-google
;; Uses LDAP authentication as credentials.
:login-with-ldap
;; Uses any generic authentication provider that implements OIDC protocol as credentials.
:login-with-oidc
;; Allows registration with Open ID
:oidc-registration
;; This logs to console the invitation tokens. It's useful in case the SMTP is not configured.
:log-invitation-tokens})
(def email
"Flags related to email features"
#{;; Uses the domains in whitelist as the only allowed domains to register in the application.
;; Used with PENPOT_REGISTRATION_DOMAIN_WHITELIST
:email-whitelist
;; Prevents the domains in blacklist to register in the application.
;; Used with PENPOT_REGISTRATION_DOMAIN_BLACKLIST
:email-blacklist
;; Skips the email verification process. Not recommended for production environments.
:email-verification
;; Only used if SMTP is disabled. Logs the emails into the console.
:log-emails
;; Enable it to configure email settings.
:smtp
;; Enables the debug mode of the SMTP library.
:smtp-debug})
(def varia
"Rest of the flags"
#{:audit-log
:audit-log-archive
:audit-log-gc
:auto-file-snapshot
;; enables the `/api/doc` endpoint that lists all the rpc methods available.
:backend-api-doc
;; TODO: remove it and use only `backend-api-doc` flag
:backend-openapi-doc
;; Disable it to start the RPC without the worker.
:backend-worker
;; Only for development
:component-thumbnails
;; enables the default cors configuration that allows all domains (currently this configuration is only used for development).
:cors
;; Enables the templates dialog on Penpot dashboard.
:dashboard-templates-section
;; disabled by default. When enabled, Penpot create demo users with a 7 days expiration.
:demo-users
;; disabled by default. When enabled, it displays a warning that this is a test instance and data will be deleted periodically.
:demo-warning
;; Activates the schema validation during update file.
:file-schema-validation
;; Reports the schema validation errors internally.
:soft-file-schema-validation
;; Activates the referential integrity validation during update file; related to components-v2.
:file-validation
;; Reports the referential integrity validation errors internally.
:soft-file-validation
;; TODO: deprecate this flag and consolidate the code
:frontend-svgo
;; TODO: deprecate this flag and consolidate the code
:exporter-svgo
;; TODO: deprecate this flag and consolidate the code
:backend-svgo
;; If enabled, it makes the Google Fonts available.
:google-fonts-provider
;; Only for development.
:nrepl-server
;; Interactive repl. Only for development.
:urepl-server
;; Programatic access to the runtime, used in administrative tasks.
;; It's mandatory to enable it to use the `manage.py` script.
:prepl-server
;; Shows the onboarding modals right after registration.
:onboarding
:quotes
:soft-quotes
;; Concurrency limit.
:rpc-climit
;; Rate limit.
:rpc-rlimit
;; Soft rate limit.
:soft-rpc-rlimit
;; Disable it if you want to serve Penpot under a different domain than `http://localhost` without HTTPS.
:secure-session-cookies
;; If `cors` enabled, this is ignored.
:strict-session-cookies
:telemetry
:terms-and-privacy-checkbox
;; Only for developtment.
:tiered-file-data-storage
:transit-readable-response
:user-feedback
;; TODO: remove this flag.
:v2-migration
:webhooks
;; TODO: deprecate this flag and consolidate the code
:export-file-v3
:render-wasm-dpr
:hide-release-modal})
(def all-flags
(set/union email login varia))
(def default (def default
"A common flags that affects both: backend and frontend." "Flags with default configuration"
[:enable-registration [:enable-registration
:enable-login-with-password
:enable-export-file-v3 :enable-export-file-v3
:enable-login-with-password]) :enable-frontend-svgo
:enable-exporter-svgo
:enable-backend-svgo
:enable-backend-api-doc
:enable-backend-openapi-doc
:enable-backend-worker
:enable-secure-session-cookies
:enable-email-verification
:enable-onboarding
:enable-dashboard-templates-section
:enable-google-fonts-provider
:enable-component-thumbnails])
(defn parse (defn parse
[& flags] [& flags]

View file

@ -5,6 +5,7 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.common.types.page (ns app.common.types.page
(:refer-clojure :exclude [empty?])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.point :as-alias gpt] [app.common.geom.point :as-alias gpt]
@ -98,3 +99,8 @@
(defn get-frame-flow (defn get-frame-flow
[flows frame-id] [flows frame-id]
(d/seek #(= (:starting-frame %) frame-id) (vals flows))) (d/seek #(= (:starting-frame %) frame-id) (vals flows)))
(defn is-empty?
"Check if page is empty or contains shapes"
[page]
(= 1 (count (:objects page))))

View file

@ -63,17 +63,11 @@
:browser :browser
:webworker)) :webworker))
(def default-flags
[:enable-onboarding
:enable-dashboard-templates-section
:enable-google-fonts-provider
:enable-component-thumbnails])
(defn- parse-flags (defn- parse-flags
[global] [global]
(let [flags (obj/get global "penpotFlags" "") (let [flags (obj/get global "penpotFlags" "")
flags (sequence (map keyword) (str/words flags))] flags (sequence (map keyword) (str/words flags))]
(flags/parse flags/default default-flags flags))) (flags/parse flags/default flags)))
(defn- parse-version (defn- parse-version
[global] [global]

View file

@ -469,10 +469,11 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (let [file (dissoc file :data)]
(assoc-in [:files id] file) (-> state
(assoc-in [:recent-files id] file) (assoc-in [:files id] file)
(update-in [:projects project-id :count] inc))))) (assoc-in [:recent-files id] file)
(update-in [:projects project-id :count] inc))))))
(defn create-file (defn create-file
[{:keys [project-id name] :as params}] [{:keys [project-id name] :as params}]

View file

@ -29,6 +29,7 @@
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
[app.common.types.page :as ctp]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst] [app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
@ -108,9 +109,6 @@
(declare ^:private workspace-initialized) (declare ^:private workspace-initialized)
(declare ^:private fetch-libraries) (declare ^:private fetch-libraries)
(declare ^:private libraries-fetched) (declare ^:private libraries-fetched)
(declare ^:private preload-data-uris)
;; (declare go-to-layout)
;; --- Initialize Workspace ;; --- Initialize Workspace
@ -273,6 +271,15 @@
(rx/of (dws/select-shapes frames-id) (rx/of (dws/select-shapes frames-id)
dwz/zoom-to-selected-shape))))) dwz/zoom-to-selected-shape)))))
(defn- select-frame-tool
[file-id page-id]
(ptk/reify ::select-frame-tool
ptk/WatchEvent
(watch [_ state _]
(let [page (dsh/lookup-page state file-id page-id)]
(when (ctp/is-empty? page)
(rx/of (dwd/select-for-drawing :frame)))))))
(defn- fetch-bundle (defn- fetch-bundle
"Multi-stage file bundle fetch coordinator" "Multi-stage file bundle fetch coordinator"
[file-id] [file-id]
@ -314,13 +321,10 @@
(defn initialize-workspace (defn initialize-workspace
[file-id] [file-id]
(assert (uuid? file-id) "expected valud uuid for `file-id`") (assert (uuid? file-id) "expected valud uuid for `file-id`")
(ptk/reify ::initialize-workspace (ptk/reify ::initialize-workspace
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(dissoc :files)
(dissoc :workspace-ready)
(assoc :recent-colors (:recent-colors storage/user)) (assoc :recent-colors (:recent-colors storage/user))
(assoc :recent-fonts (:recent-fonts storage/user)) (assoc :recent-fonts (:recent-fonts storage/user))
(assoc :current-file-id file-id) (assoc :current-file-id file-id)
@ -395,11 +399,9 @@
(dissoc (dissoc
:current-file-id :current-file-id
:workspace-editor-state :workspace-editor-state
:files
:workspace-media-objects :workspace-media-objects
:workspace-persistence :workspace-persistence
:workspace-presence :workspace-presence
:workspace-ready
:workspace-undo) :workspace-undo)
(update :workspace-global dissoc :read-only?) (update :workspace-global dissoc :read-only?)
(assoc-in [:workspace-global :options-mode] :design))) (assoc-in [:workspace-global :options-mode] :design)))
@ -426,48 +428,69 @@
;; Make this event callable through dynamic resolution ;; Make this event callable through dynamic resolution
(defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file)) (defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file))
(defn initialize-page
[page-id]
(assert (uuid? page-id) "expected valid uuid for `page-id`")
(ptk/reify ::initialize-page
(def ^:private xf:collect-file-media
"Resolve and collect all file media on page objects"
(comp (map second)
(keep (fn [{:keys [metadata fill-image]}]
(cond
(some? metadata) (cf/resolve-file-media metadata)
(some? fill-image) (cf/resolve-file-media fill-image))))))
(defn- initialize-page*
"Second phase of page initialization, once we know the page is
available on the sate"
[file-id page-id page]
(ptk/reify ::initialize-page*
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(if-let [{:keys [id] :as page} (dsh/lookup-page state page-id)] ;; selection; when user abandon the current page, the selection is lost
;; we maintain a cache of page state for user convenience with the exception of the (let [local (dm/get-in state [:workspace-cache [file-id page-id]] default-workspace-local)]
;; selection; when user abandon the current page, the selection is lost (-> state
(let [local (dm/get-in state [:workspace-cache id] default-workspace-local)] (assoc :current-page-id page-id)
(-> state (assoc :workspace-local (assoc local :selected (d/ordered-set)))
(assoc :current-page-id id) (assoc :workspace-trimmed-page (dm/select-keys page [:id :name]))
(assoc :workspace-local (assoc local :selected (d/ordered-set)))
(assoc :workspace-trimmed-page (dm/select-keys page [:id :name]))
;; FIXME: this should be done on `initialize-layout` (?) ;; FIXME: this should be done on `initialize-layout` (?)
(update :workspace-layout layout/load-layout-flags) (update :workspace-layout layout/load-layout-flags)
(update :workspace-global layout/load-layout-state))) (update :workspace-global layout/load-layout-state))))
state)) ptk/EffectEvent
(effect [_ _ _]
(let [uris (into #{} xf:collect-file-media (:objects page))]
(->> (rx/from uris)
(rx/subs! #(http/fetch-data-uri % false)))))))
(defn initialize-page
[file-id page-id]
(assert (uuid? file-id) "expected valid uuid for `file-id`")
(ptk/reify ::initialize-page
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(if (dsh/lookup-page state page-id) (if-let [page (dsh/lookup-page state file-id page-id)]
(let [file-id (:current-file-id state)] (rx/of (initialize-page* file-id page-id page)
(rx/of (preload-data-uris page-id) (dwth/watch-state-changes file-id page-id)
(dwth/watch-state-changes file-id page-id) (dwl/watch-component-changes)
(dwl/watch-component-changes))) (when (cf/external-feature-flag "boards-02" "test")
(rx/of (dcm/go-to-workspace)))))) (select-frame-tool file-id page-id)))
(rx/of (dcm/go-to-workspace :file-id file-id ::rt/replace true))))))
(defn finalize-page (defn finalize-page
[page-id] [file-id page-id]
(assert (uuid? file-id) "expected valid uuid for `file-id`")
(assert (uuid? page-id) "expected valid uuid for `page-id`") (assert (uuid? page-id) "expected valid uuid for `page-id`")
(ptk/reify ::finalize-page (ptk/reify ::finalize-page
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [local (-> (:workspace-local state) (let [local (-> (:workspace-local state)
(dissoc :edition :edit-path :selected)) (dissoc :edition :edit-path :selected))
exit? (not= :workspace (dm/get-in state [:route :data :name])) exit? (not= :workspace (rt/lookup-name state))
state (-> state state (-> state
(update :workspace-cache assoc page-id local) (update :workspace-cache assoc [file-id page-id] local)
(dissoc :current-page-id (dissoc :current-page-id
:workspace-local :workspace-local
:workspace-trimmed-page :workspace-trimmed-page
@ -476,22 +499,6 @@
(cond-> state (cond-> state
exit? (dissoc :workspace-drawing)))))) exit? (dissoc :workspace-drawing))))))
(defn- preload-data-uris
"Preloads the image data so it's ready when necessary"
[page-id]
(ptk/reify ::preload-data-uris
ptk/EffectEvent
(effect [_ state _]
(let [xform (comp (map second)
(keep (fn [{:keys [metadata fill-image]}]
(cond
(some? metadata) (cf/resolve-file-media metadata)
(some? fill-image) (cf/resolve-file-media fill-image)))))
uris (into #{} xform (dsh/lookup-page-objects state page-id))]
(->> (rx/from uris)
(rx/subs! #(http/fetch-data-uri % false)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace Page CRUD ;; Workspace Page CRUD
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -120,6 +120,11 @@
([id params & {:as options}] ([id params & {:as options}]
(navigate id params options))) (navigate id params options)))
(defn lookup-name
[state]
(dm/get-in state [:route :data :name]))
;; FIXME: rename to lookup-params
(defn get-params (defn get-params
[state] [state]
(dm/get-in state [:route :params :query])) (dm/get-in state [:route :params :query]))

View file

@ -127,7 +127,6 @@
{::mf/props :obj {::mf/props :obj
::mf/private true} ::mf/private true}
[{:keys [team-id children]}] [{:keys [team-id children]}]
(mf/with-effect [team-id] (mf/with-effect [team-id]
(st/emit! (dtm/initialize-team team-id)) (st/emit! (dtm/initialize-team team-id))
(fn [] (fn []

View file

@ -621,9 +621,10 @@
[:> comment-content* {:content (:content item)}]] [:> comment-content* {:content (:content item)}]]
[:div {:class (stl/css :replies)} [:div {:class (stl/css :replies)}
(let [total-comments (:count-comments item 1) (let [total-comments (:count-comments item)
total-replies (dec total-comments) unread-comments (:count-unread-comments item)
unread-replies (:count-unread-comments item 0)] total-replies (dec total-comments)
unread-replies (if (= unread-comments total-comments) (dec unread-comments) unread-comments)]
[:* [:*
(when (> total-replies 0) (when (> total-replies 0)
(if (= total-replies 1) (if (= total-replies 1)

View file

@ -9,7 +9,6 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.main.data.common :as dcm] [app.main.data.common :as dcm]
[app.main.data.helpers :as dsh]
[app.main.data.persistence :as dps] [app.main.data.persistence :as dps]
[app.main.data.plugins :as dpl] [app.main.data.plugins :as dpl]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
@ -43,13 +42,6 @@
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn- make-workspace-ready-ref
[file-id]
(l/derived (fn [state]
(and (= file-id (:workspace-ready state))
(some? (dsh/lookup-file-data state file-id))))
st/state))
(mf/defc workspace-content* (mf/defc workspace-content*
{::mf/private true} {::mf/private true}
[{:keys [file layout page wglobal]}] [{:keys [file layout page wglobal]}]
@ -129,35 +121,32 @@
(mf/defc workspace-page* (mf/defc workspace-page*
{::mf/private true} {::mf/private true}
[{:keys [page-id file layout wglobal]}] [{:keys [page-id file-id file layout wglobal]}]
(let [page-id (hooks/use-equal-memo page-id) (let [page (mf/deref refs/workspace-page)]
page (mf/deref refs/workspace-page)]
(mf/with-effect [] (mf/with-effect []
(let [focus-out #(st/emit! (dw/workspace-focus-lost)) (let [focus-out #(st/emit! (dw/workspace-focus-lost))
key (events/listen globals/window "blur" focus-out)] key (events/listen globals/window "blur" focus-out)]
(partial events/unlistenByKey key))) (partial events/unlistenByKey key)))
(mf/with-effect [page-id] (mf/with-effect [file-id page-id]
(if (some? page-id) (st/emit! (dw/initialize-page file-id page-id))
(st/emit! (dw/initialize-page page-id))
(st/emit! (dcm/go-to-workspace ::rt/replace true)))
(fn [] (fn []
(when (some? page-id) (when page-id
(st/emit! (dw/finalize-page page-id))))) (st/emit! (dw/finalize-page file-id page-id)))))
(if (some? page) (if (some? page)
[:> workspace-content* {:file file [:> workspace-content* {:file file
:page page :page page
:wglobal wglobal :wglobal wglobal
:layout layout}] :layout layout}]
[:& workspace-loader*]))) [:> workspace-loader*])))
(def ^:private ref:file-without-data (def ^:private ref:file-without-data
(l/derived (fn [file] (l/derived (fn [file]
(dissoc file :data)) (-> file
(dissoc :data)
(assoc ::has-data (contains? file :data))))
refs/file refs/file
=)) =))
@ -181,10 +170,6 @@
read-only? (mf/deref refs/workspace-read-only?) read-only? (mf/deref refs/workspace-read-only?)
read-only? (or read-only? (not (:can-edit permissions))) read-only? (or read-only? (not (:can-edit permissions)))
ready* (mf/with-memo [file-id]
(make-workspace-ready-ref file-id))
ready? (mf/deref ready*)
design-tokens? (features/use-feature "design-tokens/v1") design-tokens? (features/use-feature "design-tokens/v1")
background-color (:background-color wglobal)] background-color (:background-color wglobal)]
@ -207,6 +192,10 @@
(st/emit! ::dps/force-persist (st/emit! ::dps/force-persist
(dw/finalize-workspace file-id)))) (dw/finalize-workspace file-id))))
(mf/with-effect [file page-id]
(when-not page-id
(st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true))))
[:> (mf/provider ctx/current-project-id) {:value project-id} [:> (mf/provider ctx/current-project-id) {:value project-id}
[:> (mf/provider ctx/current-file-id) {:value file-id} [:> (mf/provider ctx/current-file-id) {:value file-id}
[:> (mf/provider ctx/current-page-id) {:value page-id} [:> (mf/provider ctx/current-page-id) {:value page-id}
@ -219,9 +208,11 @@
:touch-action "none"}} :touch-action "none"}}
[:> context-menu*] [:> context-menu*]
(if ^boolean ready? (if (::has-data file)
[:> workspace-page* {:page-id page-id [:> workspace-page*
:file file {:page-id page-id
:wglobal wglobal :file-id file-id
:layout layout}] :file file
:wglobal wglobal
:layout layout}]
[:> workspace-loader*])]]]]]]])) [:> workspace-loader*])]]]]]]]))

View file

@ -403,104 +403,104 @@
:h h :s s :v v :h h :s s :v v
:alpha (/ alpha 255)})))) :alpha (/ alpha 255)}))))
[:div {:class (stl/css :colorpicker) [:*
:ref node-ref [:div {:class (stl/css :colorpicker)
:style {:touch-action "none"}} :ref node-ref
[:div {:class (stl/css :top-actions)} :style {:touch-action "none"}}
[:div {:class (stl/css :top-actions-right)} [:div {:class (stl/css :top-actions)}
(when (= :gradient selected-mode) [:div {:class (stl/css :top-actions-right)}
[:div {:class (stl/css :opacity-input-wrapper)} (when (= :gradient selected-mode)
[:span {:class (stl/css :icon-text)} "%"] [:div {:class (stl/css :opacity-input-wrapper)}
[:> numeric-input* [:span {:class (stl/css :icon-text)} "%"]
{:value (-> data :opacity opacity->string) [:> numeric-input*
:on-change handle-change-gradient-opacity {:value (-> data :opacity opacity->string)
:default 100 :on-change handle-change-gradient-opacity
:min 0 :default 100
:max 100}]]) :min 0
:max 100}]])
(when (or (not disable-gradient) (not disable-image)) (when (or (not disable-gradient) (not disable-image))
[:div {:class (stl/css :select)} [:div {:class (stl/css :select)}
[:& select [:& select
{:default-value selected-mode {:default-value selected-mode
:options options :options options
:on-change handle-change-mode}]])] :on-change handle-change-mode}]])]
(when (not= selected-mode :image) (when (not= selected-mode :image)
[:button {:class (stl/css-case :picker-btn true [:button {:class (stl/css-case :picker-btn true
:selected picking-color?) :selected picking-color?)
:on-click handle-click-picker} :on-click handle-click-picker}
i/picker])] i/picker])]
(when (= selected-mode :gradient) (when (= selected-mode :gradient)
[:> gradients* [:> gradients*
{:type (:type state) {:type (:type state)
:stops (:stops state) :stops (:stops state)
:editing-stop (:editing-stop state) :editing-stop (:editing-stop state)
:on-stop-edit-start handle-stop-edit-start :on-stop-edit-start handle-stop-edit-start
:on-stop-edit-finish handle-stop-edit-finish :on-stop-edit-finish handle-stop-edit-finish
:on-select-stop handle-change-gradient-selected-stop :on-select-stop handle-change-gradient-selected-stop
:on-change-type handle-change-gradient-type :on-change-type handle-change-gradient-type
:on-change-stop handle-gradient-change-stop :on-change-stop handle-gradient-change-stop
:on-add-stop-auto handle-gradient-add-stop-auto :on-add-stop-auto handle-gradient-add-stop-auto
:on-add-stop-preview handle-gradient-add-stop-preview :on-add-stop-preview handle-gradient-add-stop-preview
:on-remove-stop handle-gradient-remove-stop :on-remove-stop handle-gradient-remove-stop
:on-rotate-stops handle-rotate-stops :on-rotate-stops handle-rotate-stops
:on-reverse-stops handle-reverse-stops :on-reverse-stops handle-reverse-stops
:on-reorder-stops handle-reorder-stops}]) :on-reorder-stops handle-reorder-stops}])
(if (= selected-mode :image) (if (= selected-mode :image)
(let [uri (cfg/resolve-file-media (:image current-color)) (let [uri (cfg/resolve-file-media (:image current-color))
keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio)] keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio)]
[:div {:class (stl/css :select-image)} [:div {:class (stl/css :select-image)}
[:div {:class (stl/css :content)} [:div {:class (stl/css :content)}
(when (:image current-color) (when (:image current-color)
[:img {:src uri}])] [:img {:src uri}])]
(when (some? (:image current-color)) (when (some? (:image current-color))
[:div {:class (stl/css :checkbox-option)} [:div {:class (stl/css :checkbox-option)}
[:label {:for "keep-aspect-ratio" [:label {:for "keep-aspect-ratio"
:class (stl/css-case :global/checked keep-aspect-ratio?)} :class (stl/css-case :global/checked keep-aspect-ratio?)}
[:span {:class (stl/css-case :global/checked keep-aspect-ratio?)} [:span {:class (stl/css-case :global/checked keep-aspect-ratio?)}
(when keep-aspect-ratio? (when keep-aspect-ratio?
i/status-tick)] i/status-tick)]
(tr "media.keep-aspect-ratio") (tr "media.keep-aspect-ratio")
[:input {:type "checkbox" [:input {:type "checkbox"
:id "keep-aspect-ratio" :id "keep-aspect-ratio"
:checked keep-aspect-ratio? :checked keep-aspect-ratio?
:on-change handle-change-keep-aspect-ratio}]]]) :on-change handle-change-keep-aspect-ratio}]]])
[:button [:button
{:class (stl/css :choose-image) {:class (stl/css :choose-image)
:title (tr "media.choose-image") :title (tr "media.choose-image")
:aria-label (tr "media.choose-image") :aria-label (tr "media.choose-image")
:on-click on-fill-image-click} :on-click on-fill-image-click}
(tr "media.choose-image") (tr "media.choose-image")
[:& file-uploader [:& file-uploader
{:input-id "fill-image-upload" {:input-id "fill-image-upload"
:accept "image/jpeg,image/png" :accept "image/jpeg,image/png"
:multi false :multi false
:ref fill-image-ref :ref fill-image-ref
:on-selected on-fill-image-selected}]]]) :on-selected on-fill-image-selected}]]])
[:* [:*
[:div {:class (stl/css :colorpicker-tabs)} [:div {:class (stl/css :colorpicker-tabs)}
[:> tab-switcher* {:tabs tabs [:> tab-switcher* {:tabs tabs
:default-selected "ramp" :default-selected "ramp"
:on-change-tab on-change-tab}]] :on-change-tab on-change-tab}]]
[:& color-inputs [:& color-inputs
{:type type {:type type
:disable-opacity disable-opacity :disable-opacity disable-opacity
:color current-color :color current-color
:on-change handle-change-color}] :on-change handle-change-color}]
[:& libraries
{:state state
:current-color current-color
:disable-gradient disable-gradient
:disable-opacity disable-opacity
:disable-image disable-image
:on-select-color on-select-library-color
:on-add-library-color on-add-library-color}]])
[:& libraries
{:state state
:current-color current-color
:disable-gradient disable-gradient
:disable-opacity disable-opacity
:disable-image disable-image
:on-select-color on-select-library-color
:on-add-library-color on-add-library-color}]])]
(when (fn? on-accept) (when (fn? on-accept)
[:div {:class (stl/css :actions)} [:div {:class (stl/css :actions)}
[:button {:class (stl/css-case [:button {:class (stl/css-case
@ -520,32 +520,41 @@
max-y (- vh h) max-y (- vh h)
rulers? (mf/deref refs/rulers?) rulers? (mf/deref refs/rulers?)
left-offset (if rulers? 40 18) left-offset (if rulers? 40 18)
right-offset (+ w 40)] right-offset (+ w 40)
top-offset (dm/str (- y 70) "px")
bottom-offset "1rem"
max-height-top (str "calc(100vh - " top-offset)
max-height-bottom (str "calc(100vh -" bottom-offset)]
(cond (cond
(or (nil? x) (nil? y)) (or (nil? x) (nil? y))
#js {:left "auto" :right "16rem" :top "4rem"} #js {:left "auto" :right "16rem" :top "4rem" :maxHeight "calc(100vh - 4rem)"}
(= position :left) (= position :left)
(if (> y max-y) (if (> y max-y)
#js {:left (dm/str (- x right-offset) "px") #js {:left (dm/str (- x right-offset) "px")
:bottom "1rem"} :bottom bottom-offset
:maxHeight max-height-bottom}
#js {:left (dm/str (- x right-offset) "px") #js {:left (dm/str (- x right-offset) "px")
:top (dm/str (- y 70) "px")}) :top top-offset
:maxHeight max-height-top})
(= position :right) (= position :right)
(if (> y max-y) (if (> y max-y)
#js {:left (dm/str (+ x 80) "px") #js {:left (dm/str (+ x 80) "px")
:bottom "1rem"} :bottom bottom-offset
:maxHeight max-height-bottom}
#js {:left (dm/str (+ x 80) "px") #js {:left (dm/str (+ x 80) "px")
:top (dm/str (- y 70) "px")}) :top top-offset
:maxHeight max-height-top})
:else :else
(if (> y max-y) (if (> y max-y)
#js {:left (dm/str (+ x left-offset) "px") #js {:left (dm/str (+ x left-offset) "px")
:bottom "1rem"} :bottom bottom-offset
:maxHeight max-height-bottom}
#js {:left (dm/str (+ x left-offset) "px") #js {:left (dm/str (+ x left-offset) "px")
:top (dm/str (- y 70) "px")})))) :top top-offset
:maxHeight max-height-top}))))
(mf/defc colorpicker-modal (mf/defc colorpicker-modal
{::mf/register modal/components {::mf/register modal/components

View file

@ -13,10 +13,14 @@
width: auto; width: auto;
padding: var(--sp-m); padding: var(--sp-m);
width: $sz-284; width: $sz-284;
overflow: auto;
display: flex;
flex-direction: column;
} }
.colorpicker { .colorpicker {
border-radius: $br-8; border-radius: $br-8;
overflow: auto;
} }
.colorpicker-tabs { .colorpicker-tabs {

View file

@ -63,10 +63,10 @@
target-container-id (or target-container-id (:parent-id shape))] target-container-id (or target-container-id (:parent-id shape))]
(filter some? (filter some?
[(when target-page-id (dw/initialize-page target-page-id)) [(when target-page-id (dw/initialize-page (:id file) target-page-id))
(dws/select-shape target-container-id) (dws/select-shape target-container-id)
(dw/paste-shapes pdata) (dw/paste-shapes pdata)
(when target-page-id (dw/initialize-page (:id page)))]))) (when target-page-id (dw/initialize-page (:id file) (:id page)))])))
(defn- sync-file [file] (defn- sync-file [file]
(map (fn [component-tag] (map (fn [component-tag]