mirror of
https://github.com/penpot/penpot.git
synced 2025-04-14 07:51:35 -05:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
0268964f36
33 changed files with 480 additions and 304 deletions
15
CHANGES.md
15
CHANGES.md
|
@ -56,7 +56,20 @@
|
|||
- Fix validation on team name input [Taiga #5510](https://tree.taiga.io/project/penpot/issue/5510)
|
||||
- Fix incorrect uri generation issues on share-link modal [Taiga #5564](https://tree.taiga.io/project/penpot/issue/5564)
|
||||
- Fix cache issues with share-links [Taiga #5559](https://tree.taiga.io/project/penpot/issue/5559)
|
||||
|
||||
- Makes height priority for the rows/columns grids [#2774](https://github.com/penpot/penpot/issues/2774)
|
||||
- Fix problem with comments mode not staying [#3363](https://github.com/penpot/penpot/issues/3363)
|
||||
- Fix problem with comments when user left the team [Taiga #5562](https://tree.taiga.io/project/penpot/issue/5562)
|
||||
- Fix problem with images patterns repeating [#3372](https://github.com/penpot/penpot/issues/3372)
|
||||
- Fix grid not being clipped in frames [#3365](https://github.com/penpot/penpot/issues/3365)
|
||||
- Fix cut/delete text layer when while creating text [Taiga #5602](https://tree.taiga.io/project/penpot/issue/5602)
|
||||
- Fix picking a gradient color in recent colors for a new color in the assets tab [Taiga #5601](https://tree.taiga.io/project/penpot/issue/5601)
|
||||
- Fix problem with importation process [Taiga #5597](https://tree.taiga.io/project/penpot/issue/5597)
|
||||
- Fix problem with HSV color picker [#3317](https://github.com/penpot/penpot/issues/3317)
|
||||
- Fix problem with slashes in layers names for exporter [#3276](https://github.com/penpot/penpot/issues/3276)
|
||||
- Fix incorrect modified data on moving files on dashboard [Taiga #5530](https://tree.taiga.io/project/penpot/issue/5530)
|
||||
- Fix focus handling on comments edition [Taiga #5560](https://tree.taiga.io/project/penpot/issue/5560)
|
||||
- Fix incorrect fullname use on registring user after OIDC authentication [Taiga #5517](https://tree.taiga.io/project/penpot/issue/5517)
|
||||
- Fix incorrect modified-at on project after import file [Taiga #5268](https://tree.taiga.io/project/penpot/issue/5268)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
|
|
|
@ -118,8 +118,7 @@
|
|||
(t/write! tw data)))
|
||||
(catch java.io.IOException _)
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unexpected error on encoding response"
|
||||
:cause cause))
|
||||
(l/error :hint "unexpected error on encoding response" :cause cause))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))))
|
||||
|
||||
|
@ -132,8 +131,8 @@
|
|||
|
||||
(catch java.io.IOException _)
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unexpected error on encoding response"
|
||||
:cause cause))
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))))
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
[::sm/word-string {:max 500}])
|
||||
|
||||
(def schema:token
|
||||
[::sm/word-string {:max 1000}])
|
||||
[::sm/word-string {:max 6000}])
|
||||
|
||||
;; ---- COMMAND: login with password
|
||||
|
||||
|
@ -323,9 +323,9 @@
|
|||
:extra-data ptoken})))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token] :as params}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}]
|
||||
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
|
||||
params (merge params claims)
|
||||
params (assoc claims :fullname fullname)
|
||||
|
||||
is-active (or (:is-active params)
|
||||
(not (contains? cf/flags :email-verification)))
|
||||
|
|
|
@ -929,5 +929,10 @@
|
|||
::input (:path file)
|
||||
::project-id project-id
|
||||
::ignore-index-errors? true))]
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id})
|
||||
|
||||
(rph/with-meta ids
|
||||
{::audit/props {:file nil :file-ids ids}}))))
|
||||
|
|
|
@ -189,6 +189,8 @@
|
|||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn check-features-compatibility!
|
||||
"Function responsible to check if provided features are supported by
|
||||
the current backend"
|
||||
[features]
|
||||
(let [not-supported (set/difference features supported-features)]
|
||||
(when (seq not-supported)
|
||||
|
@ -248,47 +250,59 @@
|
|||
(into #{} (comp (filter pmap/pointer-map?)
|
||||
(map pmap/get-id)))))
|
||||
|
||||
;; FIXME: file locking
|
||||
(defn- process-components-v2-feature
|
||||
"A special case handling of the components/v2 feature."
|
||||
[conn {:keys [id features data] :as file}]
|
||||
(binding [pmap/*tracked* (atom {})]
|
||||
(let [data (ctf/migrate-to-components-v2 data)
|
||||
features (conj features "components/v2")
|
||||
features' (db/create-array conn "text" features)]
|
||||
(db/update! conn :file
|
||||
{:data (blob/encode data)
|
||||
:features features'}
|
||||
{:id id})
|
||||
(persist-pointers! conn id)
|
||||
(-> file
|
||||
(assoc :features features)
|
||||
(assoc :data data)))))
|
||||
|
||||
(defn handle-file-features!
|
||||
[conn {:keys [features] :as file} client-features]
|
||||
|
||||
;; Check features compatibility between the currently supported features on
|
||||
;; the current backend instance and the file retrieved from the database
|
||||
(check-features-compatibility! features)
|
||||
|
||||
(cond-> file
|
||||
(and (contains? features "components/v2")
|
||||
(not (contains? client-features "components/v2")))
|
||||
(as-> file (ex/raise :type :restriction
|
||||
:code :feature-mismatch
|
||||
:feature "components/v2"
|
||||
:hint "file has 'components/v2' feature enabled but frontend didn't specifies it"
|
||||
:file-id (:id file)))
|
||||
|
||||
;; This operation is needed because the components migration generates a new
|
||||
;; page with random id which is returned to the client; without persisting
|
||||
;; the migration this can cause that two simultaneous clients can have a
|
||||
;; different view of the file data and end persisting two pages with main
|
||||
;; components and breaking the whole file."
|
||||
(and (contains? client-features "components/v2")
|
||||
(not (contains? features "components/v2")))
|
||||
(as-> file (process-components-v2-feature conn file))
|
||||
|
||||
;; This operation is needed for backward comapatibility with frontends that
|
||||
;; does not support pointer-map resolution mechanism; this just resolves the
|
||||
;; pointers on backend and return a complete file.
|
||||
(and (contains? features "storage/pointer-map")
|
||||
(not (contains? client-features "storage/pointer-map")))
|
||||
(process-pointers deref)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUERY COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn handle-file-features!
|
||||
[conn {:keys [id features data] :as file} client-features]
|
||||
|
||||
(when (and (contains? features "components/v2")
|
||||
(not (contains? client-features "components/v2")))
|
||||
(ex/raise :type :restriction
|
||||
:code :feature-mismatch
|
||||
:feature "components/v2"
|
||||
:hint "file has 'components/v2' feature enabled but frontend didn't specifies it"))
|
||||
|
||||
;; NOTE: this operation is needed because the components migration
|
||||
;; generates a new page with random id which is returned to the
|
||||
;; client; without persisting the migration this can cause that two
|
||||
;; simultaneous clients can have a different view of the file data
|
||||
;; and end persisting two pages with main components and breaking
|
||||
;; the whole file
|
||||
(let [file (if (and (contains? client-features "components/v2")
|
||||
(not (contains? features "components/v2")))
|
||||
(binding [pmap/*tracked* (atom {})]
|
||||
(let [data (ctf/migrate-to-components-v2 data)
|
||||
features (conj features "components/v2")
|
||||
features' (db/create-array conn "text" features)]
|
||||
(db/update! conn :file
|
||||
{:data (blob/encode data)
|
||||
:features features'}
|
||||
{:id id})
|
||||
(persist-pointers! conn id)
|
||||
(-> file
|
||||
(assoc :features features)
|
||||
(assoc :data data))))
|
||||
file)]
|
||||
|
||||
(cond-> file
|
||||
(and (contains? features "storage/pointer-map")
|
||||
(not (contains? client-features "storage/pointer-map")))
|
||||
(process-pointers deref))))
|
||||
|
||||
;; --- COMMAND QUERY: get-file (by id)
|
||||
|
||||
(sm/def! ::features
|
||||
|
@ -331,7 +345,7 @@
|
|||
([conn id client-features]
|
||||
(get-file conn id client-features nil))
|
||||
([conn id client-features project-id]
|
||||
;; here we check if client requested features are supported
|
||||
;; here we check if client requested features are supported
|
||||
(check-features-compatibility! client-features)
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(let [params (merge {:id id}
|
||||
|
|
|
@ -86,16 +86,16 @@
|
|||
(ex/raise :type :validation
|
||||
:code :cant-persist-already-persisted-file))
|
||||
|
||||
(loop [revs (seq revs)
|
||||
data (blob/decode (:data file))]
|
||||
(if-let [rev (first revs)]
|
||||
(recur (rest revs)
|
||||
(->> rev :changes blob/decode (cp/process-changes data)))
|
||||
(db/update! conn :file
|
||||
{:deleted-at nil
|
||||
:revn revn
|
||||
:data (blob/encode data)}
|
||||
{:id id})))
|
||||
|
||||
(let [data
|
||||
(->> revs
|
||||
(mapcat #(->> % :changes blob/decode))
|
||||
(cp/process-changes (blob/decode (:data file))))]
|
||||
(db/update! conn :file
|
||||
{:deleted-at nil
|
||||
:revn revn
|
||||
:data (blob/encode data)}
|
||||
{:id id}))
|
||||
nil))
|
||||
|
||||
(s/def ::persist-temp-file
|
||||
|
|
|
@ -323,3 +323,13 @@
|
|||
:rfn (fn [^Reader rdr]
|
||||
(let [^List x (read-object! rdr)]
|
||||
(Matrix. (.get x 0) (.get x 1) (.get x 2) (.get x 3) (.get x 4) (.get x 5))))})
|
||||
|
||||
|
||||
;; Backward compatibility for 1.19 with v1.20;
|
||||
|
||||
(add-handlers!
|
||||
{:name "penpot/geom/rect"
|
||||
:rfn read-map-like}
|
||||
{:name "penpot/shape"
|
||||
:rfn read-map-like})
|
||||
|
||||
|
|
|
@ -218,7 +218,7 @@
|
|||
(make-selrect (min xp1 xp2) (min yp1 yp2) (abs (- xp1 xp2)) (abs (- yp1 yp2)))))
|
||||
|
||||
(defn clip-selrect
|
||||
[{:keys [x1 y1 x2 y2] :as sr} bounds]
|
||||
[{:keys [x1 y1 x2 y2] :as sr} clip-rect]
|
||||
(when (some? sr)
|
||||
(let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2} (rect->selrect bounds)]
|
||||
(let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2 :as sr2} (rect->selrect clip-rect)]
|
||||
(corners->selrect (max bx1 x1) (max by1 y1) (min bx2 x2) (min by2 y2)))))
|
||||
|
|
|
@ -96,22 +96,25 @@
|
|||
"Get the parent shape linked to a component for this shape, if any"
|
||||
([objects shape] (get-component-shape objects shape nil))
|
||||
([objects shape {:keys [allow-main?] :or {allow-main? false} :as options}]
|
||||
(cond
|
||||
(nil? shape)
|
||||
nil
|
||||
(cond
|
||||
(nil? shape)
|
||||
nil
|
||||
|
||||
(and (not (ctk/in-component-copy? shape)) (not allow-main?))
|
||||
nil
|
||||
(= uuid/zero (:id shape))
|
||||
nil
|
||||
|
||||
(ctk/instance-root? shape)
|
||||
shape
|
||||
(and (not (ctk/in-component-copy? shape)) (not allow-main?))
|
||||
nil
|
||||
|
||||
:else
|
||||
(get-component-shape objects (get objects (:parent-id shape)) options))))
|
||||
(ctk/instance-root? shape)
|
||||
shape
|
||||
|
||||
:else
|
||||
(get-component-shape objects (get objects (:parent-id shape)) options))))
|
||||
|
||||
(defn in-component-main?
|
||||
"Check if the shape is inside a component non-main instance.
|
||||
|
||||
|
||||
Note that we must iterate on the parents because non-root shapes in
|
||||
a main component have not any discriminating attribute."
|
||||
[objects shape]
|
||||
|
|
|
@ -92,16 +92,6 @@ http {
|
|||
error_page 301 302 307 = @handle_redirect;
|
||||
}
|
||||
|
||||
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
|
||||
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_set_header User-Agent "curl/7.74.0";
|
||||
proxy_set_header Host "raw.githubusercontent.com";
|
||||
proxy_set_header Accept "*/*";
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /internal/gfonts/css {
|
||||
proxy_pass https://fonts.googleapis.com/css?$args;
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
|
@ -124,31 +114,6 @@ http {
|
|||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~ ^/internal/gfonts/font/(?<font_file>.+) {
|
||||
proxy_pass https://fonts.gstatic.com/s/$font_file;
|
||||
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_hide_header Cross-Origin-Resource-Policy;
|
||||
proxy_hide_header Link;
|
||||
proxy_hide_header Alt-Svc;
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Expires;
|
||||
proxy_hide_header Cross-Origin-Opener-Policy;
|
||||
proxy_hide_header Report-To;
|
||||
|
||||
proxy_ignore_headers Set-Cookie Vary Cache-Control Expires;
|
||||
|
||||
proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
|
||||
proxy_set_header Host "fonts.gstatic.com";
|
||||
proxy_set_header Accept "*/*";
|
||||
|
||||
proxy_cache penpot;
|
||||
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
add_header Cache-Control max-age=86400;
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location /internal/assets {
|
||||
internal;
|
||||
alias /home/penpot/penpot/backend/assets;
|
||||
|
@ -192,6 +157,41 @@ http {
|
|||
}
|
||||
|
||||
location / {
|
||||
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
|
||||
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_set_header User-Agent "curl/7.74.0";
|
||||
proxy_set_header Host "raw.githubusercontent.com";
|
||||
proxy_set_header Accept "*/*";
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location ~ ^/internal/gfonts/font/(?<font_file>.+) {
|
||||
proxy_pass https://fonts.gstatic.com/s/$font_file;
|
||||
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_hide_header Cross-Origin-Resource-Policy;
|
||||
proxy_hide_header Link;
|
||||
proxy_hide_header Alt-Svc;
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Expires;
|
||||
proxy_hide_header Cross-Origin-Opener-Policy;
|
||||
proxy_hide_header Report-To;
|
||||
|
||||
proxy_ignore_headers Set-Cookie Vary Cache-Control Expires;
|
||||
|
||||
proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
|
||||
proxy_set_header Host "fonts.gstatic.com";
|
||||
proxy_set_header Accept "*/*";
|
||||
|
||||
proxy_cache penpot;
|
||||
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
add_header Cache-Control max-age=86400;
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~ ^/(/|css|fonts|images|js|wasm) {
|
||||
}
|
||||
|
||||
|
|
|
@ -115,31 +115,6 @@ http {
|
|||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~ ^/internal/gfonts/font/(?<font_file>.+) {
|
||||
proxy_pass https://fonts.gstatic.com/s/$font_file;
|
||||
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_hide_header Cross-Origin-Resource-Policy;
|
||||
proxy_hide_header Link;
|
||||
proxy_hide_header Alt-Svc;
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Expires;
|
||||
proxy_hide_header Cross-Origin-Opener-Policy;
|
||||
proxy_hide_header Report-To;
|
||||
|
||||
proxy_ignore_headers Set-Cookie Vary Cache-Control Expires;
|
||||
|
||||
proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
|
||||
proxy_set_header Host "fonts.gstatic.com";
|
||||
proxy_set_header Accept "*/*";
|
||||
|
||||
proxy_cache penpot;
|
||||
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
add_header Cache-Control max-age=86400;
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location /internal/assets {
|
||||
internal;
|
||||
alias /opt/data/assets;
|
||||
|
@ -161,6 +136,31 @@ http {
|
|||
}
|
||||
|
||||
location / {
|
||||
location ~ ^/internal/gfonts/font/(?<font_file>.+) {
|
||||
proxy_pass https://fonts.gstatic.com/s/$font_file;
|
||||
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_hide_header Cross-Origin-Resource-Policy;
|
||||
proxy_hide_header Link;
|
||||
proxy_hide_header Alt-Svc;
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_hide_header Expires;
|
||||
proxy_hide_header Cross-Origin-Opener-Policy;
|
||||
proxy_hide_header Report-To;
|
||||
|
||||
proxy_ignore_headers Set-Cookie Vary Cache-Control Expires;
|
||||
|
||||
proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
|
||||
proxy_set_header Host "fonts.gstatic.com";
|
||||
proxy_set_header Accept "*/*";
|
||||
|
||||
proxy_cache penpot;
|
||||
|
||||
add_header Access-Control-Allow-Origin $http_origin;
|
||||
add_header Cache-Control max-age=86400;
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~* \.(js|css).*$ {
|
||||
add_header Cache-Control "max-age=86400" always; # 24 hours
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
(declare ^:private assoc-file-name)
|
||||
(declare prepare-exports)
|
||||
|
||||
;; Regex to clean namefiles
|
||||
(def sanitize-file-regex #"[\\/:*?\"<>|]")
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::filename ::us/string)
|
||||
(s/def ::name ::us/string)
|
||||
|
@ -134,7 +137,7 @@
|
|||
:on-progress on-progress)
|
||||
|
||||
append (fn [{:keys [filename path] :as object}]
|
||||
(rsc/add-to-zip! zip path filename))
|
||||
(rsc/add-to-zip! zip path (str/replace filename sanitize-file-regex "_")))
|
||||
|
||||
proc (-> (p/do
|
||||
(p/loop [exports (seq exports)]
|
||||
|
@ -144,9 +147,7 @@
|
|||
(p/recur (rest exports)))))
|
||||
(.finalize zip))
|
||||
(p/then (constantly resource))
|
||||
(p/catch on-error))
|
||||
]
|
||||
|
||||
(p/catch on-error))]
|
||||
(if wait
|
||||
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
|
||||
(assoc exchange :response/body (dissoc resource :path)))))
|
||||
|
|
|
@ -199,7 +199,7 @@
|
|||
}
|
||||
|
||||
&.value {
|
||||
background: linear-gradient(var(--gradient-direction), #fff 0%, #000 100%);
|
||||
background: linear-gradient(var(--gradient-direction), #000 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.handler {
|
||||
|
|
|
@ -134,14 +134,13 @@
|
|||
(rx/throw {:type :comment-error})))))))))
|
||||
|
||||
(defn update-comment-thread-status
|
||||
[{:keys [id] :as thread}]
|
||||
(dm/assert! (comment-thread? thread))
|
||||
[thread-id]
|
||||
(ptk/reify ::update-comment-thread-status
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [done #(d/update-in-when % [:comment-threads id] assoc :count-unread-comments 0)
|
||||
(let [done #(d/update-in-when % [:comment-threads thread-id] assoc :count-unread-comments 0)
|
||||
share-id (-> state :viewer-local :share-id)]
|
||||
(->> (rp/cmd! :update-comment-thread-status {:id id :share-id share-id})
|
||||
(->> (rp/cmd! :update-comment-thread-status {:id thread-id :share-id share-id})
|
||||
(rx/map (constantly done))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))))))
|
||||
|
||||
|
@ -282,7 +281,7 @@
|
|||
(fetched [[users comments] state]
|
||||
(let [state (-> state
|
||||
(assoc :comment-threads (d/index-by :id comments))
|
||||
(assoc :current-file-comments-users (d/index-by :id users)))]
|
||||
(update :current-file-comments-users merge (d/index-by :id users)))]
|
||||
(reduce set-comment-threds state comments)))]
|
||||
|
||||
(ptk/reify ::retrieve-comment-threads
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[app.common.data.macros :as dm]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as dt]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
|
@ -872,10 +873,14 @@
|
|||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [origin-project (get-in state [:dashboard-files (first ids) :project-id])]
|
||||
(let [origin-project (get-in state [:dashboard-files (first ids) :project-id])
|
||||
update-project (fn [project]
|
||||
(-> project
|
||||
(update :count #(+ % (count ids)))
|
||||
(assoc :modified-at (dt/now))))]
|
||||
(-> state
|
||||
(d/update-in-when [:dashboard-projects origin-project] update :count #(- % (count ids)))
|
||||
(d/update-in-when [:dashboard-projects project-id] update :count #(+ % (count ids))))))
|
||||
(d/update-in-when [:dashboard-projects origin-project] update-project)
|
||||
(d/update-in-when [:dashboard-projects project-id] update-project))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
|
|
|
@ -515,27 +515,32 @@
|
|||
(let [shape-id (-> state wsh/lookup-selected first)]
|
||||
(update state :colorpicker
|
||||
(fn [state]
|
||||
(if (some? gradient)
|
||||
(let [stop (or (:editing-stop state) 0)
|
||||
stops (mapv split-color-components (:stops gradient))
|
||||
type (case (:type gradient)
|
||||
:linear :linear-gradient
|
||||
:radial :radial-gradient)]
|
||||
(-> state
|
||||
(assoc :type type)
|
||||
(assoc :current-color (nth stops stop))
|
||||
(assoc :stops stops)
|
||||
(assoc :gradient (-> gradient
|
||||
(dissoc :stops)
|
||||
(assoc :shape-id shape-id)))
|
||||
(assoc :editing-stop stop)))
|
||||
(let [current-color (:current-color state)]
|
||||
(if (some? gradient)
|
||||
(let [stop (or (:editing-stop state) 0)
|
||||
stops (mapv split-color-components (:stops gradient))
|
||||
type (case (:type gradient)
|
||||
:linear :linear-gradient
|
||||
:radial :radial-gradient
|
||||
(:type state))]
|
||||
(-> state
|
||||
(assoc :type type)
|
||||
(assoc :current-color (nth stops stop))
|
||||
(assoc :stops stops)
|
||||
(assoc :gradient (-> gradient
|
||||
(dissoc :stops)
|
||||
(assoc :shape-id shape-id)))
|
||||
(assoc :editing-stop stop)))
|
||||
|
||||
(-> state
|
||||
(assoc :type :color)
|
||||
(assoc :current-color (split-color-components (dissoc data :gradient)))
|
||||
(dissoc :editing-stop)
|
||||
(dissoc :gradient)
|
||||
(dissoc :stops)))))))))
|
||||
(-> state
|
||||
(assoc :type :color)
|
||||
(cond-> (or (nil? current-color)
|
||||
(not= (:color data) (:color current-color))
|
||||
(not= (:opacity data) (:opacity current-color)))
|
||||
(assoc :current-color (split-color-components (dissoc data :gradient))))
|
||||
(dissoc :editing-stop)
|
||||
(dissoc :gradient)
|
||||
(dissoc :stops))))))))))
|
||||
|
||||
(defn update-colorpicker-color
|
||||
[changes add-recent?]
|
||||
|
|
|
@ -142,14 +142,17 @@
|
|||
(rx/merge
|
||||
(rx/of (update-editor-state shape nil))
|
||||
(when (and (not= content (:content shape))
|
||||
(some? (:current-page-id state)))
|
||||
(some? (:current-page-id state))
|
||||
(some? shape))
|
||||
(rx/of
|
||||
(dch/update-shapes
|
||||
[id]
|
||||
(fn [shape]
|
||||
(let [{:keys [width height]} modifiers]
|
||||
(let [{:keys [width height position-data]} modifiers]
|
||||
(-> shape
|
||||
(assoc :content content)
|
||||
(cond-> position-data
|
||||
(assoc :position-data position-data))
|
||||
(cond-> new-shape?
|
||||
(assoc :name text))
|
||||
(cond-> (or (some? width) (some? height))
|
||||
|
|
|
@ -165,12 +165,22 @@
|
|||
[{:keys [code] :as error}]
|
||||
(cond
|
||||
(= :feature-mismatch code)
|
||||
(let [message (tr "errors.feature-mismatch" (:feature error))]
|
||||
(st/emit! (modal/show {:type :alert :message message})))
|
||||
(let [message (tr "errors.feature-mismatch" (:feature error))
|
||||
team-id (:current-team-id @st/state)
|
||||
project-id (:current-project-id @st/state)
|
||||
on-accept #(if (and project-id team-id)
|
||||
(st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id}))
|
||||
(set! (.-href glob/location) ""))]
|
||||
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
|
||||
|
||||
(= :features-not-supported code)
|
||||
(let [message (tr "errors.feature-not-supported" (:feature error))]
|
||||
(st/emit! (modal/show {:type :alert :message message})))
|
||||
(let [message (tr "errors.feature-not-supported" (:feature error))
|
||||
team-id (:current-team-id @st/state)
|
||||
project-id (:current-project-id @st/state)
|
||||
on-accept #(if (and project-id team-id)
|
||||
(st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id}))
|
||||
(set! (.-href glob/location) ""))]
|
||||
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
|
||||
|
||||
(= :max-quote-reached code)
|
||||
(let [message (tr "errors.max-quote-reached" (:target error))]
|
||||
|
|
|
@ -298,6 +298,15 @@
|
|||
(into [] (keep #(get objects %)) parent-ids)))
|
||||
workspace-page-objects =))
|
||||
|
||||
(defn shape-parents
|
||||
[id]
|
||||
(l/derived
|
||||
(fn [objects]
|
||||
(into []
|
||||
(keep (d/getf objects))
|
||||
(cph/get-parent-ids objects id)))
|
||||
workspace-page-objects =))
|
||||
|
||||
(defn children-objects
|
||||
[id]
|
||||
(l/derived
|
||||
|
|
|
@ -10,11 +10,10 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr t]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[goog.events :as events]
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc alert-dialog
|
||||
{::mf/register modal/components
|
||||
|
@ -26,29 +25,27 @@
|
|||
hint
|
||||
accept-label
|
||||
accept-style] :as props}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
|
||||
on-accept (or on-accept identity)
|
||||
message (or message (t locale "ds.alert-title"))
|
||||
(let [on-accept (or on-accept identity)
|
||||
message (or message (tr "ds.alert-title"))
|
||||
accept-label (or accept-label (tr "ds.alert-ok"))
|
||||
accept-style (or accept-style :danger)
|
||||
title (or title (t locale "ds.alert-title"))
|
||||
title (or title (tr "ds.alert-title"))
|
||||
|
||||
accept-fn
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (modal/hide))
|
||||
(on-accept props)))]
|
||||
|
||||
(mf/with-effect
|
||||
(mf/with-effect []
|
||||
(letfn [(on-keydown [event]
|
||||
(when (k/enter? event)
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/hide))
|
||||
(on-accept props)))]
|
||||
(->> (events/listen js/document EventType.KEYDOWN on-keydown)
|
||||
(->> (events/listen js/document "keydown" on-keydown)
|
||||
(partial events/unlistenByKey))))
|
||||
|
||||
[:div.modal-overlay
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.comments
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.config :as cfg]
|
||||
|
@ -19,38 +20,55 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc resizing-textarea
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [value (obj/get props "value" "")
|
||||
on-focus (obj/get props "on-focus")
|
||||
on-blur (obj/get props "on-blur")
|
||||
placeholder (obj/get props "placeholder")
|
||||
on-change (obj/get props "on-change")
|
||||
on-esc (obj/get props "on-esc")
|
||||
autofocus? (obj/get props "autofocus")
|
||||
{::mf/wrap-props false
|
||||
::mf/forward-ref true}
|
||||
[props ref]
|
||||
(let [value (d/nilv (unchecked-get props "value") "")
|
||||
on-focus (unchecked-get props "on-focus")
|
||||
on-blur (unchecked-get props "on-blur")
|
||||
placeholder (unchecked-get props "placeholder")
|
||||
on-change (unchecked-get props "on-change")
|
||||
on-esc (unchecked-get props "on-esc")
|
||||
autofocus? (unchecked-get props "autofocus")
|
||||
select-on-focus? (unchecked-get props "select-on-focus")
|
||||
|
||||
ref (mf/use-ref)
|
||||
local-ref (mf/use-ref)
|
||||
ref (or ref local-ref)
|
||||
|
||||
on-key-down
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(when (and (kbd/esc? event)
|
||||
(fn? on-esc))
|
||||
(on-esc event))))
|
||||
|
||||
on-change*
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps on-change)
|
||||
(fn [event]
|
||||
(let [content (dom/get-target-val event)]
|
||||
(on-change content))))]
|
||||
(on-change content))))
|
||||
|
||||
on-focus*
|
||||
(mf/use-fn
|
||||
(mf/deps select-on-focus? on-focus)
|
||||
(fn [event]
|
||||
(when (fn? on-focus)
|
||||
(on-focus event))
|
||||
|
||||
(when ^boolean select-on-focus?
|
||||
(let [target (dom/get-target event)]
|
||||
(dom/select-text! target)
|
||||
;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
|
||||
(.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))
|
||||
]
|
||||
|
||||
|
||||
|
||||
(mf/use-layout-effect
|
||||
|
@ -64,7 +82,7 @@
|
|||
{:ref ref
|
||||
:auto-focus autofocus?
|
||||
:on-key-down on-key-down
|
||||
:on-focus on-focus
|
||||
:on-focus on-focus*
|
||||
:on-blur on-blur
|
||||
:value value
|
||||
:placeholder placeholder
|
||||
|
@ -76,24 +94,24 @@
|
|||
content (mf/use-state "")
|
||||
|
||||
on-focus
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
#(reset! show-buttons? true))
|
||||
|
||||
on-blur
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
#(reset! show-buttons? false))
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
#(reset! content %))
|
||||
|
||||
on-cancel
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
#(do (reset! content "")
|
||||
(reset! show-buttons? false)))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps thread @content)
|
||||
(fn []
|
||||
(st/emit! (dcm/add-comment thread @content))
|
||||
|
@ -128,7 +146,7 @@
|
|||
pos-y (* (:y position) zoom)
|
||||
|
||||
on-esc
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps draft)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
|
@ -137,13 +155,13 @@
|
|||
(st/emit! :interrupt))))
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps draft)
|
||||
(fn [content]
|
||||
(st/emit! (dcm/update-draft-thread {:content content}))))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps draft)
|
||||
(partial on-submit draft))]
|
||||
|
||||
|
@ -179,16 +197,20 @@
|
|||
(let [content (mf/use-state content)
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
#(reset! content %))
|
||||
|
||||
on-submit*
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps @content)
|
||||
(fn [] (on-submit @content)))]
|
||||
(fn [] (on-submit @content)))
|
||||
]
|
||||
|
||||
|
||||
[:div.reply-form.edit-form
|
||||
[:& resizing-textarea {:value @content
|
||||
:autofocus true
|
||||
:select-on-focus true
|
||||
:on-change on-change}]
|
||||
[:div.buttons
|
||||
[:input.btn-primary {:type "button" :value "Post" :on-click on-submit*}]
|
||||
|
@ -202,24 +224,24 @@
|
|||
edition? (mf/use-state false)
|
||||
|
||||
on-show-options
|
||||
(mf/use-callback #(reset! options true))
|
||||
(mf/use-fn #(reset! options true))
|
||||
|
||||
on-hide-options
|
||||
(mf/use-callback #(reset! options false))
|
||||
(mf/use-fn #(reset! options false))
|
||||
|
||||
on-edit-clicked
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! options false)
|
||||
(reset! edition? true)))
|
||||
|
||||
on-delete-comment
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps comment)
|
||||
#(st/emit! (dcm/delete-comment comment)))
|
||||
|
||||
delete-thread
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps thread)
|
||||
#(st/emit! (dcm/close-thread)
|
||||
(if (= origin :viewer)
|
||||
|
@ -228,7 +250,7 @@
|
|||
|
||||
|
||||
on-delete-thread
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps thread)
|
||||
#(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
|
@ -238,17 +260,17 @@
|
|||
:on-accept delete-thread})))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps comment thread)
|
||||
(fn [content]
|
||||
(reset! edition? false)
|
||||
(st/emit! (dcm/update-comment (assoc comment :content content)))))
|
||||
|
||||
on-cancel
|
||||
(mf/use-callback #(reset! edition? false))
|
||||
(mf/use-fn #(reset! edition? false))
|
||||
|
||||
toggle-resolved
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps thread)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
|
@ -268,6 +290,7 @@
|
|||
(if (:is-resolved thread)
|
||||
[:span i/checkbox-checked]
|
||||
[:span i/checkbox-unchecked])])
|
||||
|
||||
(when (= (:id profile) (:id owner))
|
||||
[:div.options
|
||||
[:div.options-icon {:on-click on-show-options} i/actions]])]
|
||||
|
@ -287,40 +310,45 @@
|
|||
[:li {:on-click on-delete-thread} (tr "labels.delete-comment-thread")]
|
||||
[:li {:on-click on-delete-comment} (tr "labels.delete-comment")])]]]))
|
||||
|
||||
(defn comments-ref
|
||||
[{:keys [id] :as thread}]
|
||||
(l/derived (l/in [:comments id]) st/state))
|
||||
(defn make-comments-ref
|
||||
[thread-id]
|
||||
(l/derived (l/in [:comments thread-id]) st/state))
|
||||
|
||||
(mf/defc thread-comments
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [thread zoom users origin position-modifier]}]
|
||||
(let [ref (mf/use-ref)
|
||||
pos (cond-> (:position thread)
|
||||
(some? position-modifier)
|
||||
(gpt/transform position-modifier))
|
||||
(let [ref (mf/use-ref)
|
||||
|
||||
pos-x (+ (* (:x pos) zoom) 14)
|
||||
pos-y (- (* (:y pos) zoom) 14)
|
||||
|
||||
comments-ref (mf/use-memo (mf/deps thread) #(comments-ref thread))
|
||||
thread-id (:id thread)
|
||||
thread-pos (:position thread)
|
||||
|
||||
pos (cond-> thread-pos
|
||||
(some? position-modifier)
|
||||
(gpt/transform position-modifier))
|
||||
|
||||
pos-x (+ (* (:x pos) zoom) 14)
|
||||
pos-y (- (* (:y pos) zoom) 14)
|
||||
|
||||
comments-ref (mf/with-memo [thread-id]
|
||||
(make-comments-ref thread-id))
|
||||
comments-map (mf/deref comments-ref)
|
||||
comments (->> (vals comments-map)
|
||||
(sort-by :created-at))
|
||||
|
||||
comments (mf/with-memo [comments-map]
|
||||
(->> (vals comments-map)
|
||||
(sort-by :created-at)))
|
||||
|
||||
comment (first comments)]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps thread)
|
||||
#(st/emit! (dcm/retrieve-comments (:id thread))))
|
||||
(mf/with-effect [thread-id]
|
||||
(st/emit! (dcm/retrieve-comments thread-id)))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps thread)
|
||||
#(st/emit! (dcm/update-comment-thread-status thread)))
|
||||
(mf/with-effect [thread-id]
|
||||
(st/emit! (dcm/update-comment-thread-status thread-id)))
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps thread comments-map)
|
||||
(fn []
|
||||
(when-let [node (mf/ref-val ref)]
|
||||
(dom/scroll-into-view-if-needed! node))))
|
||||
(mf/with-layout-effect [thread-pos comments-map]
|
||||
(when-let [node (mf/ref-val ref)]
|
||||
(dom/scroll-into-view-if-needed! node)))
|
||||
|
||||
(when (some? comment)
|
||||
[:div.thread-content
|
||||
|
@ -345,22 +373,22 @@
|
|||
(defn use-buble
|
||||
[zoom {:keys [position frame-id]}]
|
||||
(let [dragging-ref (mf/use-ref false)
|
||||
start-ref (mf/use-ref nil)
|
||||
start-ref (mf/use-ref nil)
|
||||
|
||||
state (mf/use-state {:hover false
|
||||
:new-position-x nil
|
||||
:new-position-y nil
|
||||
:new-frame-id frame-id})
|
||||
state (mf/use-state {:hover false
|
||||
:new-position-x nil
|
||||
:new-position-y nil
|
||||
:new-frame-id frame-id})
|
||||
|
||||
on-pointer-down
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/capture-pointer event)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(mf/set-ref-val! start-ref (dom/get-client-position event))))
|
||||
|
||||
on-pointer-up
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps (select-keys @state [:new-position-x :new-position-y :new-frame-id]))
|
||||
(fn [_ thread]
|
||||
(when (and
|
||||
|
@ -369,7 +397,7 @@
|
|||
(st/emit! (dwcm/update-comment-thread-position thread [(:new-position-x @state) (:new-position-y @state)])))))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
(mf/set-ref-val! dragging-ref false)
|
||||
|
@ -378,7 +406,7 @@
|
|||
(swap! state assoc :new-position-y nil)))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps position zoom)
|
||||
(fn [event]
|
||||
(when-let [_ (mf/ref-val dragging-ref)]
|
||||
|
@ -416,7 +444,7 @@
|
|||
pos-y (* (or (:new-position-y @state) (:y pos)) zoom)
|
||||
|
||||
on-pointer-down*
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps origin was-open? open? drag? on-pointer-down)
|
||||
(fn [event]
|
||||
(when (not= origin :viewer)
|
||||
|
@ -427,7 +455,7 @@
|
|||
(on-pointer-down event))))
|
||||
|
||||
on-pointer-up*
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps origin thread was-open? drag? on-pointer-up)
|
||||
(fn [event]
|
||||
(when (not= origin :viewer)
|
||||
|
@ -439,7 +467,7 @@
|
|||
(st/emit! (dcm/open-thread thread))))))
|
||||
|
||||
on-pointer-move*
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps origin drag? on-pointer-move)
|
||||
(fn [event]
|
||||
(when (not= origin :viewer)
|
||||
|
@ -448,7 +476,7 @@
|
|||
(on-pointer-move event))))
|
||||
|
||||
on-click*
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps origin thread on-click)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
|
@ -472,7 +500,7 @@
|
|||
[{:keys [item users on-click]}]
|
||||
(let [owner (get users (:owner-id item))
|
||||
on-click*
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps item)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
|
|
|
@ -200,11 +200,6 @@
|
|||
(when (and (some? current) (not (.contains current target)))
|
||||
(dom/blur! current)))))))
|
||||
|
||||
on-mouse-up
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(dom/prevent-default event)))
|
||||
|
||||
handle-focus
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
|
@ -213,9 +208,9 @@
|
|||
(on-focus event))
|
||||
|
||||
(when select-on-focus?
|
||||
(-> event (dom/get-target) (.select))
|
||||
(dom/select-text! event)
|
||||
;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
|
||||
(.addEventListener target "mouseup" on-mouse-up #js {"once" true})))))
|
||||
(.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))
|
||||
|
||||
props (-> props
|
||||
(obj/without ["value" "onChange" "nillable" "onFocus"])
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
(fn []
|
||||
(st/emit! (dd/fetch-files {:project-id project-id})
|
||||
(dd/fetch-recent-files (:id team))
|
||||
(dd/fetch-projects)
|
||||
(dd/clear-selected-files))))]
|
||||
|
||||
(mf/with-effect
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
[app.util.object :as obj]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def no-repeat-padding 1.05)
|
||||
|
||||
(mf/defc fills
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
@ -71,7 +73,10 @@
|
|||
|
||||
(let [fill-id (dm/str "fill-" shape-index "-" render-id)]
|
||||
[:> :pattern (-> (obj/clone pattern-attrs)
|
||||
(obj/set! "id" fill-id))
|
||||
(obj/set! "id" fill-id)
|
||||
(cond-> has-image?
|
||||
(-> (obj/set! "width" (* width no-repeat-padding))
|
||||
(obj/set! "height" (* height no-repeat-padding)))))
|
||||
[:g
|
||||
(for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)]
|
||||
[:> :rect (-> (attrs/extract-fill-attrs value render-id fill-index type)
|
||||
|
@ -80,7 +85,17 @@
|
|||
(obj/set! "height" height))])
|
||||
|
||||
(when has-image?
|
||||
[:image {:href (or (:data-uri shape) (get embed uri uri))
|
||||
:preserveAspectRatio "none"
|
||||
:width width
|
||||
:height height}])]])])))))
|
||||
[:g
|
||||
;; We add this shape to add a padding so the patter won't repeat
|
||||
;; Issue: https://tree.taiga.io/project/penpot/issue/5583
|
||||
[:rect {:x 0
|
||||
:y 0
|
||||
:width (* width no-repeat-padding)
|
||||
:height (* height no-repeat-padding)
|
||||
:fill "none"}]
|
||||
[:image {:href (or (:data-uri shape) (get embed uri uri))
|
||||
:preserveAspectRatio "none"
|
||||
:x 0
|
||||
:y 0
|
||||
:width width
|
||||
:height height}]])]])])))))
|
||||
|
|
|
@ -68,7 +68,9 @@
|
|||
(mf/deps current-color @drag?)
|
||||
(fn [color]
|
||||
(when (or (not= (str/lower (:hex color)) (str/lower (:hex current-color)))
|
||||
(not= (:h color) (:h current-color)))
|
||||
(not= (:h color) (:h current-color))
|
||||
(not= (:s color) (:s current-color))
|
||||
(not= (:v color) (:v current-color)))
|
||||
(let [recent-color (merge current-color color)
|
||||
recent-color (dc/materialize-color-components recent-color)]
|
||||
(st/emit! (dc/update-colorpicker-color recent-color (not @drag?)))))))
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.workspace.colorpicker.color-inputs
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.util.color :as uc]
|
||||
[app.util.dom :as dom]
|
||||
|
@ -17,6 +18,14 @@
|
|||
val
|
||||
(str \# val)))
|
||||
|
||||
(defn value->hsv-value
|
||||
[val]
|
||||
(* 255 (/ val 100)))
|
||||
|
||||
(defn hsv-value->value
|
||||
[val]
|
||||
(* (/ val 255) 100))
|
||||
|
||||
(mf/defc color-inputs [{:keys [type color disable-opacity on-change]}]
|
||||
(let [{red :r green :g blue :b
|
||||
hue :h saturation :s value :v
|
||||
|
@ -56,8 +65,11 @@
|
|||
on-change-property
|
||||
(fn [property max-value]
|
||||
(fn [e]
|
||||
(let [val (-> e dom/get-target-val (mth/clamp 0 max-value))
|
||||
val (if (#{:s} property) (/ val 100) val)]
|
||||
(let [val (-> e dom/get-target-val d/parse-double (mth/clamp 0 max-value))
|
||||
val (case property
|
||||
:s (/ val 100)
|
||||
:v (value->hsv-value val)
|
||||
val)]
|
||||
(when (not (nil? val))
|
||||
(if (#{:r :g :b} property)
|
||||
(let [{:keys [r g b]} (merge color (hash-map property val))
|
||||
|
@ -89,10 +101,12 @@
|
|||
property-ref (get refs ref-key)]
|
||||
(when (and property-val property-ref)
|
||||
(when-let [node (mf/ref-val property-ref)]
|
||||
(case ref-key
|
||||
(:s :alpha) (dom/set-value! node (* property-val 100))
|
||||
:hex (dom/set-value! node property-val)
|
||||
(dom/set-value! node property-val))))))))
|
||||
(let [new-val
|
||||
(case ref-key
|
||||
(:s :alpha) (mth/precision (* property-val 100) 2)
|
||||
:v (mth/precision (hsv-value->value property-val) 2)
|
||||
property-val)]
|
||||
(dom/set-value! node new-val))))))))
|
||||
|
||||
[:div.color-values
|
||||
{:class (when disable-opacity "disable-opacity")}
|
||||
|
@ -149,9 +163,9 @@
|
|||
:ref (:v refs)
|
||||
:type "number"
|
||||
:min 0
|
||||
:max 255
|
||||
:max 100
|
||||
:default-value value
|
||||
:on-change (on-change-property :v 255)}]])
|
||||
:on-change (on-change-property :v 100)}]])
|
||||
|
||||
(when (not disable-opacity)
|
||||
[:input.alpha-value {:id "alpha-value"
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
[:span.hsva-selector-label "V"]
|
||||
[:& slider-selector
|
||||
{:class "value"
|
||||
:reverse? true
|
||||
:reverse? false
|
||||
:max-value 255
|
||||
:value value
|
||||
:on-change (handle-change-slider :v)
|
||||
|
|
|
@ -29,12 +29,12 @@
|
|||
[]
|
||||
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
|
||||
update-mode
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:mode mode}))))
|
||||
|
||||
update-show
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:show mode}))))]
|
||||
|
||||
|
@ -76,7 +76,7 @@
|
|||
page-id (or page-id (mf/use-ctx ctx/current-page-id))
|
||||
|
||||
on-thread-click
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps page-id)
|
||||
(fn [thread]
|
||||
(when (not= page-id (:page-id thread))
|
||||
|
|
|
@ -81,7 +81,11 @@
|
|||
(let [tool (-> (dom/get-current-target event)
|
||||
(dom/get-data "tool")
|
||||
(keyword))]
|
||||
(st/emit! :interrupt (dw/select-for-drawing tool)))))
|
||||
(st/emit! :interrupt
|
||||
dw/clear-edition-mode)
|
||||
|
||||
;; Delay so anything that launched :interrupt can finish
|
||||
(st/emit! 100 (dw/select-for-drawing tool)))))
|
||||
|
||||
toggle-text-palette
|
||||
(mf/use-fn
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
(ns app.main.ui.workspace.viewport.frame-grid
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.refs :as refs]
|
||||
|
@ -109,32 +112,70 @@
|
|||
:strokeOpacity color-opacity
|
||||
:fill "none"}}]]))]))
|
||||
|
||||
(defn frame-clip-area
|
||||
[{:keys [selrect]} parents]
|
||||
(reduce
|
||||
(fn [sr parent]
|
||||
(cond-> sr
|
||||
(and (not (cph/root? parent))
|
||||
(cph/frame-shape? parent)
|
||||
(not (:show-content parent)))
|
||||
(gsh/clip-selrect (:selrect parent))))
|
||||
selrect
|
||||
parents))
|
||||
|
||||
(mf/defc grid-display-frame
|
||||
[{:keys [frame zoom]}]
|
||||
(for [[index grid] (->> (:grids frame)
|
||||
(filter :display)
|
||||
(map-indexed vector))]
|
||||
(let [props #js {:key (str (:id frame) "-grid-" index)
|
||||
:frame frame
|
||||
:zoom zoom
|
||||
:grid grid}]
|
||||
(case (:type grid)
|
||||
:square [:> square-grid props]
|
||||
:column [:> layout-grid props]
|
||||
:row [:> layout-grid props]))))
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [frame zoom transforming]}]
|
||||
(let [frame-id (:id frame)
|
||||
parents-ref (mf/with-memo [frame-id] (refs/shape-parents frame-id))
|
||||
parents (mf/deref parents-ref)
|
||||
clip-area (frame-clip-area frame parents)
|
||||
clip-id (dm/str (:id frame) "-grid-clip")
|
||||
|
||||
transform?
|
||||
(or (contains? transforming frame-id)
|
||||
(some #(contains? transforming %) (map :id parents)))]
|
||||
|
||||
(when-not transform?
|
||||
[:g {:clip-path (dm/fmt "url(#%)" clip-id)}
|
||||
[:defs
|
||||
[:clipPath {:id clip-id}
|
||||
[:rect {:x (:x clip-area)
|
||||
:y (:y clip-area)
|
||||
:width (:width clip-area)
|
||||
:height (:height clip-area)}]]]
|
||||
|
||||
(for [[index grid] (->> (:grids frame)
|
||||
(filter :display)
|
||||
(map-indexed vector))]
|
||||
(let [props #js {:key (str (:id frame) "-grid-" index)
|
||||
:frame frame
|
||||
:zoom zoom
|
||||
:grid grid}]
|
||||
(case (:type grid)
|
||||
:square [:> square-grid props]
|
||||
:column [:> layout-grid props]
|
||||
:row [:> layout-grid props])))])))
|
||||
|
||||
(defn has-grid?
|
||||
[{:keys [grids]}]
|
||||
(and (some? grids)
|
||||
(d/not-empty? (->> grids (filter :display)))))
|
||||
|
||||
(mf/defc frame-grid
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [zoom transform selected focus]}]
|
||||
(let [frames (mf/deref refs/workspace-frames)
|
||||
transforming (when (some? transform) selected)
|
||||
is-transform? #(contains? transforming (:id %))]
|
||||
(let [frames (->> (mf/deref refs/workspace-frames)
|
||||
(filter has-grid?))
|
||||
transforming (when (some? transform) selected)]
|
||||
|
||||
[:g.grid-display {:style {:pointer-events "none"}}
|
||||
(for [frame frames]
|
||||
(when (and (not (is-transform? frame))
|
||||
(when (and #_(not (is-transform? frame))
|
||||
(not (ctst/rotated-frame? frame))
|
||||
(or (empty? focus) (contains? focus (:id frame))))
|
||||
[:& grid-display-frame {:key (str "grid-" (:id frame))
|
||||
:zoom zoom
|
||||
:frame frame}]))]))
|
||||
:frame frame
|
||||
:transforming transforming}]))]))
|
||||
|
|
|
@ -193,4 +193,4 @@
|
|||
(or (:color-library-name color)
|
||||
(:name color)
|
||||
(:color color)
|
||||
(gradient-type->string (:type (:gradient color)))))
|
||||
(gradient-type->string (:type (:gradient color)))))
|
||||
|
|
|
@ -234,7 +234,7 @@
|
|||
|
||||
(defn select-text!
|
||||
[^js node]
|
||||
(when (and (some? node) (or (= "INPUT" (.-tagName node)) (= "TEXTAREA" (.-tagName node))))
|
||||
(when (some? node)
|
||||
(.select ^js node)))
|
||||
|
||||
(defn ^boolean equals?
|
||||
|
|
|
@ -25,28 +25,31 @@
|
|||
(mth/floor (/ frame-length-no-margins (+ item-length gutter)))))
|
||||
|
||||
(defn- calculate-generic-grid
|
||||
[v width {:keys [size gutter margin item-length type]}]
|
||||
[v total-length {:keys [size gutter margin item-length type]}]
|
||||
(let [size (if (number? size)
|
||||
size
|
||||
(calculate-size width item-length margin gutter))
|
||||
parts (/ width size)
|
||||
(calculate-size total-length item-length margin gutter))
|
||||
|
||||
width' (min (or item-length ##Inf) (+ parts (- gutter) (/ gutter size) (- (/ (* margin 2) size))))
|
||||
parts (/ total-length size)
|
||||
|
||||
item-length (if (number? item-length)
|
||||
item-length
|
||||
(+ parts (- gutter) (/ gutter size) (- (/ (* margin 2) size))))
|
||||
|
||||
offset (case type
|
||||
:right (- width (* width' size) (* gutter (dec size)) margin)
|
||||
:center (/ (- width (* width' size) (* gutter (dec size))) 2)
|
||||
:right (- total-length (* item-length size) (* gutter (dec size)) margin)
|
||||
:center (/ (- total-length (* item-length size) (* gutter (dec size))) 2)
|
||||
margin)
|
||||
|
||||
gutter (if (= :stretch type)
|
||||
(let [gutter (/ (- width (* width' size) (* margin 2)) (dec size))]
|
||||
(let [gutter (max 0 gutter (/ (- total-length (* item-length size) (* margin 2)) (dec size)))]
|
||||
(if (d/num? gutter) gutter 0))
|
||||
gutter)
|
||||
|
||||
next-v (fn [cur-val]
|
||||
(+ offset v (* (+ width' gutter) cur-val)))]
|
||||
(+ offset v (* (+ item-length gutter) cur-val)))]
|
||||
|
||||
[size width' next-v gutter]))
|
||||
[size item-length next-v gutter]))
|
||||
|
||||
(defn- calculate-column-grid
|
||||
[{:keys [width height x y] :as frame} params]
|
||||
|
|
Loading…
Add table
Reference in a new issue