Compare commits

...

14 Commits

Author SHA1 Message Date
Andrey Antukh
10ca2b20e4 📎 Update changelog 2025-01-30 11:41:50 +01:00
Marina López
a2ce5efe69 Add has-files prop to create-file event 2025-01-30 11:31:45 +01:00
Andrey Antukh
15a896e050 🐛 Add migration for fix files with invalid token-data (#5712)
Because of previous bug that is already fixed
2025-01-30 09:17:02 +01:00
Aitor Moreno
8145eb89d7 🐛 Fix styles not being inherited (#5717) 2025-01-29 23:06:05 +01:00
Andrey Antukh
a6485b93b7 Merge pull request #5707 from penpot/niwinz-update-selfhost-defaults
 Add the ability to set http body size on docker images
2025-01-29 12:43:17 +01:00
Andrey Antukh
47f1ca9627 Change backend defaults for http body 2025-01-29 12:26:30 +01:00
Andrey Antukh
f252ffb201 Add the ability to overwrite default http body size on docker images
And provide a compose file with good defaults
2025-01-29 12:22:05 +01:00
Marina López
0768ef1b8f Add A/B test switching '+' to 'Add file' on hover (#5705) 2025-01-29 11:42:08 +01:00
Andrey Antukh
d9ba107da2 🔧 Update default body size for docker images
Set it to 350MiB, the same as we have on our saas
2025-01-29 11:38:16 +01:00
Alonso Torres
407b664910 🐛 Fix problem with plugin data null values (#5696) 2025-01-28 14:47:37 +01:00
Belén Albeza
31145f2805 Merge pull request #5675 from penpot/azazeln28-fix-issue-9900
🐛 Fix Colorpicker shows RGBA inputs when HSVA mode is active
2025-01-28 11:35:04 +01:00
AzazelN28
33192cfdb8 🐛 Fix colorpicker HSVA inputs 2025-01-27 15:38:21 +01:00
Andrey Antukh
471699960f 🐛 Update media references after instantiation of a component (#5652)
🐛 Update media references after instantiation of a component
2025-01-27 11:58:13 +01:00
Alonso Torres
7458a35f31 🐛 Fix problem when pasting text (#5671) 2025-01-24 11:26:06 +01:00
24 changed files with 481 additions and 111 deletions

View File

@@ -1,12 +1,14 @@
# CHANGELOG
## 2.4.3 (Unreleased)
## 2.4.3
### :bug: Bugs fixed
- Fix errors from editable select on measures menu [Taiga #9888](https://tree.taiga.io/project/penpot/issue/9888)
- Fix exception on importing some templates from templates slider
- Consolidate adding share button to workspace
- Fix problem when pasting text [Taiga #9929](https://tree.taiga.io/project/penpot/issue/9929)
- Fix incorrect media reference handling on component instantiation
## 2.4.2

View File

@@ -12,6 +12,7 @@
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.files.helpers :as cfh]
[app.common.files.migrations :as fmg]
[app.common.files.validate :as fval]
[app.common.logging :as l]
@@ -29,7 +30,6 @@
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.set :as set]
[clojure.walk :as walk]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[datoteka.io :as io]))
@@ -241,40 +241,65 @@
:data nil}
{::sql/columns [:media-id :file-id :revn]}))
(def ^:private sql:get-missing-media-references
"SELECT fmo.*
FROM file_media_object AS fmo
WHERE fmo.id = ANY(?::uuid[])
AND file_id != ?")
(def ^:private
xform:collect-media-id
(comp
(map :objects)
(mapcat vals)
(mapcat (fn [obj]
;; NOTE: because of some bug, we ended with
;; many shape types having the ability to
;; have fill-image attribute (which initially
;; designed for :path shapes).
(sequence
(keep :id)
(concat [(:fill-image obj)
(:metadata obj)]
(map :fill-image (:fills obj))
(map :stroke-image (:strokes obj))
(->> (:content obj)
(tree-seq map? :children)
(mapcat :fills)
(map :fill-image))))))))
(defn update-media-references!
"Given a file and a coll of media-refs, check if all provided
references are correct or fix them in-place"
[{:keys [::db/conn] :as cfg} {file-id :id :as file} media-refs]
(let [missing-index
(reduce (fn [result {:keys [id] :as fmo}]
(assoc result id
(-> fmo
(assoc :id (uuid/next))
(assoc :file-id file-id)
(dissoc :created-at)
(dissoc :deleted-at))))
{}
(db/exec! conn [sql:get-missing-media-references
(->> (into #{} xf-map-id media-refs)
(db/create-array conn "uuid"))
file-id]))
(defn collect-used-media
"Given a fdata (file data), returns all media references."
[data]
(-> #{}
(into xform:collect-media-id (vals (:pages-index data)))
(into xform:collect-media-id (vals (:components data)))
(into (keys (:media data)))))
lookup-index
(fn [id]
(if-let [mobj (get missing-index id)]
(do
(l/trc :hint "lookup index"
:file-id (str file-id)
:snap-id (str (:snapshot-id file))
:id (str id)
:result (str (get mobj :id)))
(get mobj :id))
id))
update-shapes
(fn [data {:keys [page-id shape-id]}]
(d/update-in-when data [:pages-index page-id :objects shape-id] cfh/relink-media-refs lookup-index))
file
(update file :data #(reduce update-shapes % media-refs))]
(doseq [[old-id item] missing-index]
(l/dbg :hint "create missing references"
:file-id (str file-id)
:snap-id (str (:snapshot-id file))
:old-id (str old-id)
:id (str (:id item)))
(db/insert! conn :file-media-object item
{::db/return-keys false}))
file))
(defn get-file-media
[cfg {:keys [data id] :as file}]
(db/run! cfg (fn [{:keys [::db/conn]}]
(let [ids (collect-used-media data)
(let [ids (cfh/collect-used-media data)
ids (db/create-array conn "uuid" ids)
sql (str "SELECT * FROM file_media_object WHERE id = ANY(?)")]
@@ -327,48 +352,7 @@
replace the old :component-file reference with the new
ones, using the provided file-index."
[data]
(letfn [(process-map-form [form]
(cond-> form
;; Relink image shapes
(and (map? (:metadata form))
(= :image (:type form)))
(update-in [:metadata :id] lookup-index)
;; Relink paths with fill image
(map? (:fill-image form))
(update-in [:fill-image :id] lookup-index)
;; This covers old shapes and the new :fills.
(uuid? (:fill-color-ref-file form))
(update :fill-color-ref-file lookup-index)
;; This covers the old shapes and the new :strokes
(uuid? (:stroke-color-ref-file form))
(update :stroke-color-ref-file lookup-index)
;; This covers all text shapes that have typography referenced
(uuid? (:typography-ref-file form))
(update :typography-ref-file lookup-index)
;; This covers the component instance links
(uuid? (:component-file form))
(update :component-file lookup-index)
;; This covers the shadows and grids (they have directly
;; the :file-id prop)
(uuid? (:file-id form))
(update :file-id lookup-index)))
(process-form [form]
(if (map? form)
(try
(process-map-form form)
(catch Throwable cause
(l/warn :hint "failed form" :form (pr-str form) ::l/sync? true)
(throw cause)))
form))]
(walk/postwalk process-form data)))
(cfh/relink-media-refs data lookup-index))
(defn- relink-media
"A function responsible of process the :media attr of file data and

View File

@@ -42,8 +42,8 @@
(def default-params
{::port 6060
::host "0.0.0.0"
::max-body-size (* 1024 1024 30) ; default 30 MiB
::max-multipart-body-size (* 1024 1024 120)}) ; default 120 MiB
::max-body-size 31457280 ; default 30 MiB
::max-multipart-body-size 367001600}) ; default 350 MiB
(defmethod ig/expand-key ::server
[k v]

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.files-update
(:require
[app.binfile.common :as bfc]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
@@ -415,19 +416,37 @@
(l/error :hint "file validation error"
:cause cause))))
(defn- process-changes-and-validate
[cfg file changes skip-validate]
(let [;; WARNING: this ruins performance; maybe we need to find
;; some other way to do general validation
libs (when (and (or (contains? cf/flags :file-validation)
(contains? cf/flags :soft-file-validation))
(not skip-validate))
(get-file-libraries cfg file))
libs
(when (and (or (contains? cf/flags :file-validation)
(contains? cf/flags :soft-file-validation))
(not skip-validate))
(get-file-libraries cfg file))
file (-> (files/check-version! file)
(update :revn inc)
(update :data cpc/process-changes changes)
(update :data d/without-nils))]
;; The main purpose of this atom is provide a contextual state
;; for the changes subsystem where optionally some hints can
;; be provided for the changes processing. Right now we are
;; using it for notify about the existence of media refs when
;; a new shape is added.
state
(atom {})
file
(binding [cpc/*state* state]
(-> (files/check-version! file)
(update :revn inc)
(update :data cpc/process-changes changes)
(update :data d/without-nils)))
file
(if-let [media-refs (-> @state :media-refs not-empty)]
(bfc/update-media-references! cfg file media-refs)
file)]
(binding [pmap/*tracked* nil]
(when (contains? cf/flags :soft-file-validation)

View File

@@ -53,7 +53,7 @@
fixes all not propertly referenced file-media-object for a file"
[{:keys [id data] :as file} & _]
(let [conn (db/get-connection h/*system*)
used (bfc/collect-used-media data)
used (cfh/collect-used-media data)
ids (db/create-array conn "uuid" used)
sql (str "SELECT * FROM file_media_object WHERE id = ANY(?)")
rows (db/exec! conn [sql ids])

View File

@@ -10,7 +10,7 @@
file is eligible to be garbage collected after some period of
inactivity (the default threshold is 72h)."
(:require
[app.binfile.common :as bfc]
[app.common.files.helpers :as cfh]
[app.common.files.migrations :as fmg]
[app.common.files.validate :as cfv]
[app.common.logging :as l]
@@ -54,7 +54,7 @@
(def ^:private xf:collect-used-media
(comp
(map :data)
(mapcat bfc/collect-used-media)))
(mapcat cfh/collect-used-media)))
(defn- clean-file-media!
"Performs the garbage collection of file media objects."

View File

@@ -1658,3 +1658,174 @@
components (get-in result [:data :components])]
(t/is (not (contains? components c-id)))))))
(defn add-file-media-object
[& {:keys [profile-id file-id]}]
(let [mfile {:filename "sample.jpg"
:path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"
:size 312043}
params {::th/type :upload-file-media-object
::rpc/profile-id profile-id
:file-id file-id
:is-local true
:name "testfile"
:content mfile}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))
(t/deftest file-gc-with-media-assets-and-absorb-library
(let [storage (:app.storage/storage th/*system*)
profile (th/create-profile* 1)
file-1 (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared true})
file-2 (th/create-file* 2 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})
fmedia (add-file-media-object :profile-id (:id profile) :file-id (:id file-1))
rel (th/link-file-to-library*
{:file-id (:id file-2)
:library-id (:id file-1)})
s-id-1 (uuid/random)
s-id-2 (uuid/random)
c-id (uuid/random)
f1-page-id (first (get-in file-1 [:data :pages]))
f2-page-id (first (get-in file-2 [:data :pages]))
fills
[{:fill-image
{:id (:id fmedia)
:name "test"
:width 200
:height 200}}]]
;; Update file library inserting new component
(update-file!
:file-id (:id file-1)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id f1-page-id
:id s-id-1
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id s-id-1
:name "Board"
:frame-id uuid/zero
:parent-id uuid/zero
:type :frame
:fills fills
:main-instance true
:component-root true
:component-file (:id file-1)
:component-id c-id})}
{:type :add-component
:path ""
:name "Board"
:main-instance-id s-id-1
:main-instance-page f1-page-id
:id c-id
:anotation nil}])
;; Instanciate a component in a different file
(update-file!
:file-id (:id file-2)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id f2-page-id
:id s-id-2
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id s-id-2
:name "Board"
:frame-id uuid/zero
:parent-id uuid/zero
:type :frame
:fills fills
:main-instance false
:component-root true
:component-file (:id file-1)
:component-id c-id})}])
;; Check that file media object references are present for both objects
;; the original one and the instance.
(let [rows (th/db-exec! ["SELECT * FROM file_media_object ORDER BY created_at ASC"])]
(t/is (= 2 (count rows)))
(t/is (= (:id file-1) (:file-id (get rows 0))))
(t/is (= (:id file-2) (:file-id (get rows 1))))
(t/is (every? (comp nil? :deleted-at) rows)))
;; Check if the underlying media reference on shape is different
;; from the instantiation
(let [data {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file-2)}
out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
fill (get-in result [:data :pages-index f2-page-id :objects s-id-2 :fills 0 :fill-image])]
(t/is (some? fill))
(t/is (not= (:id fill) (:id fmedia)))))
;; Run the file-gc on file and library
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-1)})))
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-2)})))
;; Now proceed to delete file and absorb it
(let [data {::th/type :delete-file
::rpc/profile-id (:id profile)
:id (:id file-1)}
out (th/command! data)]
(t/is (th/success? out)))
(th/run-task! :delete-object
{:object :file
:deleted-at (dt/now)
:id (:id file-1)})
;; Check that file media object references are marked all for deletion
(let [rows (th/db-exec! ["SELECT * FROM file_media_object ORDER BY created_at ASC"])]
;; (pp/pprint rows)
(t/is (= 2 (count rows)))
(t/is (= (:id file-1) (:file-id (get rows 0))))
(t/is (some? (:deleted-at (get rows 0))))
(t/is (= (:id file-2) (:file-id (get rows 1))))
(t/is (nil? (:deleted-at (get rows 1)))))
(th/run-task! :objects-gc
{:min-age 0})
(let [rows (th/db-exec! ["SELECT * FROM file_media_object ORDER BY created_at ASC"])]
(t/is (= 1 (count rows)))
(t/is (= (:id file-2) (:file-id (get rows 0))))
(t/is (nil? (:deleted-at (get rows 0)))))))

View File

@@ -484,6 +484,11 @@
modification."
nil)
(def ^:dynamic *state*
"A general purpose state to signal some out of order operations
to the processor backend."
nil)
(defmulti process-change (fn [_ change] (:type change)))
(defmulti process-operation (fn [_ op] (:type op)))
@@ -617,12 +622,38 @@
;; --- Shape / Obj
;; The main purpose of this is ensure that all created shapes has
;; valid media references; so for make sure of it, we analyze each
;; shape added via `:add-obj` change for media usage, and if shape has
;; media refs, we put that media refs on the check list (on the
;; *state*) which will subsequently be processed and all incorrect
;; references will be corrected. The media ref is anything that can
;; be pointing to a file-media-object on the shape, per example we
;; have fill-image, stroke-image, etc.
(defn- collect-shape-media-refs
[state obj page-id]
(let [media-refs
(-> (cfh/collect-shape-media-refs obj)
(not-empty))
xform
(map (fn [id]
{:page-id page-id
:shape-id (:id obj)
:id id}))]
(update state :media-refs into xform media-refs)))
(defmethod process-change :add-obj
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}]
(let [update-container
(fn [container]
(ctst/add-shape id obj container frame-id parent-id index ignore-touched))]
(when *state*
(swap! *state* collect-shape-media-refs obj page-id))
(if page-id
(d/update-in-when data [:pages-index page-id] update-container)
(d/update-in-when data [:components component-id] update-container))))
@@ -876,7 +907,7 @@
(letfn [(update-fn [data]
(if (some? value)
(assoc-in data [:plugin-data namespace key] value)
(update-in data [:plugin-data namespace] dissoc key)))]
(d/update-in-when data [:plugin-data namespace] dissoc key)))]
(case object-type
:file

View File

@@ -6,4 +6,4 @@
(ns app.common.files.defaults)
(def version 62)
(def version 65)

View File

@@ -12,6 +12,7 @@
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[clojure.set :as set]
[clojure.walk :as walk]
[cuerdas.core :as str]))
#?(:clj (set! *warn-on-reflection* true))
@@ -533,6 +534,86 @@
(get-position-on-parent objects)
inc))
(defn collect-shape-media-refs
"Collect all media refs on the provided shape. Returns a set of ids"
[shape]
(sequence
(keep :id)
;; NOTE: because of some bug, we ended with
;; many shape types having the ability to
;; have fill-image attribute (which initially
;; designed for :path shapes).
(concat [(:fill-image shape)
(:metadata shape)]
(map :fill-image (:fills shape))
(map :stroke-image (:strokes shape))
(->> (:content shape)
(tree-seq map? :children)
(mapcat :fills)
(map :fill-image)))))
(def ^:private
xform:collect-media-refs
"A transducer for collect media-id usage across a container (page or
component)"
(comp
(map :objects)
(mapcat vals)
(mapcat collect-shape-media-refs)))
(defn collect-used-media
"Given a fdata (file data), returns all media references used in the
file data"
[data]
(-> #{}
(into xform:collect-media-refs (vals (:pages-index data)))
(into xform:collect-media-refs (vals (:components data)))
(into (keys (:media data)))))
(defn relink-media-refs
"A function responsible to analyze all file data and replace the
old :component-file reference with the new ones, using the provided
file-index."
[data lookup-index]
(letfn [(process-map-form [form]
(cond-> form
;; Relink image shapes
(and (map? (:metadata form))
(= :image (:type form)))
(update-in [:metadata :id] lookup-index)
;; Relink paths with fill image
(map? (:fill-image form))
(update-in [:fill-image :id] lookup-index)
;; This covers old shapes and the new :fills.
(uuid? (:fill-color-ref-file form))
(update :fill-color-ref-file lookup-index)
;; This covers the old shapes and the new :strokes
(uuid? (:stroke-color-ref-file form))
(update :stroke-color-ref-file lookup-index)
;; This covers all text shapes that have typography referenced
(uuid? (:typography-ref-file form))
(update :typography-ref-file lookup-index)
;; This covers the component instance links
(uuid? (:component-file form))
(update :component-file lookup-index)
;; This covers the shadows and grids (they have directly
;; the :file-id prop)
(uuid? (:file-id form))
(update :file-id lookup-index)))
(process-form [form]
(if (map? form)
(process-map-form form)
form))]
(walk/postwalk process-form data)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHAPES ORGANIZATION (PATH MANAGEMENT)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -1179,6 +1179,24 @@
(update data :components update-vals update-component)))
(defn migrate-up-65
[data]
(let [update-object
(fn [object]
(d/update-when object :plugin-data d/without-nils))
update-page
(fn [page]
(-> (update-object page)
(update :objects update-vals update-object)))]
(-> data
(update-object)
(d/update-when :pages-index update-vals update-page)
(d/update-when :colors update-vals update-object)
(d/update-when :typographies update-vals update-object)
(d/update-when :components update-vals update-object))))
(def migrations
"A vector of all applicable migrations"
[{:id 2 :migrate-up migrate-up-2}
@@ -1229,4 +1247,5 @@
{:id 56 :migrate-up migrate-up-56}
{:id 57 :migrate-up migrate-up-57}
{:id 59 :migrate-up migrate-up-59}
{:id 62 :migrate-up migrate-up-62}])
{:id 62 :migrate-up migrate-up-62}
{:id 65 :migrate-up migrate-up-65}])

View File

@@ -29,6 +29,15 @@ x-flags: &penpot-flags
x-uri: &penpot-public-uri
PENPOT_PUBLIC_URI: http://localhost:9001
x-body-size: &penpot-http-body-size
# Max body size (30MiB); Used for plain requests, should never be
# greater than multi-part size
PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
# Max multipart body size (350MiB)
PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600
networks:
penpot:
@@ -103,7 +112,7 @@ services:
# - "traefik.http.routers.penpot-https.tls.certresolver=letsencrypt"
environment:
<< : *penpot-flags
<< : [*penpot-flags, *penpot-http-body-size]
penpot-backend:
image: "penpotapp/backend:latest"
@@ -123,7 +132,7 @@ services:
## container.
environment:
<< : [*penpot-flags, *penpot-public-uri]
<< : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size]
## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems
## (eg http sessions, or invitations) are derived.
@@ -261,5 +270,3 @@ services:
# ports:
# - 9000:9000
# - 9001:9001

View File

@@ -22,7 +22,9 @@ update_flags /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060};
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061};
export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-127.0.0.11};
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600}; # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_INTERNAL_RESOLVER" < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_INTERNAL_RESOLVER,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
< /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
exec "$@";

View File

@@ -64,7 +64,7 @@ http {
listen 8080 default_server;
server_name _;
client_max_body_size 100M;
client_max_body_size $PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE;
charset utf-8;
proxy_http_version 1.1;

View File

@@ -22,3 +22,22 @@ test("Bug 7549 - User clicks on color swatch to display the color picker next to
const distance = swatchBox.x - (pickerBox.x + pickerBox.width);
expect(distance).toBeLessThan(60);
});
// Fix for https://tree.taiga.io/project/penpot/issue/9900
test("Bug 9900 - Color picker has no inputs for HSV values", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" });
await swatch.click();
const HSVA = await workspacePage.page.getByLabel("HSVA");
await HSVA.click();
await workspacePage.page.getByLabel("H", { exact: true }).isVisible();
await workspacePage.page.getByLabel("S", { exact: true }).isVisible();
await workspacePage.page.getByLabel("V", { exact: true }).isVisible();
});

View File

@@ -225,3 +225,16 @@ test("Bug 9066 - Problem with grid layout", async ({ page }) => {
page.getByTestId("children-6ad3e6b9-c5a0-80cf-8005-283bbe378bcb"),
).toHaveText(["CBCDEF"]);
});
test("[Taiga #9929] Paste text in workspace", async ({ page, context }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.evaluate(() => navigator.clipboard.writeText("Lorem ipsum dolor"));
await workspacePage.viewport.click({ button: "right" });
await page.getByText("PasteCtrlV").click();
await workspacePage.viewport
.getByRole("textbox")
.getByText("Lorem ipsum dolor");
});

View File

@@ -953,6 +953,7 @@
(defn create-file
[{:keys [project-id name] :as params}]
(dm/assert! (uuid? project-id))
(ptk/reify ::create-file
ev/Event
(-data [_] {:project-id project-id})

View File

@@ -1722,10 +1722,10 @@
(coll? transit-data)
(rx/of (paste-transit (assoc transit-data :in-viewport in-viewport?)))
(string? html-data)
(and (string? html-data) (d/not-empty? html-data))
(rx/of (paste-html-text html-data text-data))
(string? text-data)
(and (string? text-data) (d/not-empty? text-data))
(rx/of (paste-text text-data))
:else

View File

@@ -160,7 +160,7 @@
(let [mdata {:on-success on-file-created}
params {:project-id (:id project)}]
(st/emit! (-> (dd/create-file (with-meta params mdata))
(with-meta {::ev/origin origin}))))))]
(with-meta {::ev/origin origin :has-files (> file-count 0)}))))))]
(mf/with-effect [project]
(when project

View File

@@ -20,7 +20,10 @@
(mf/use-fn
(mf/deps create-fn)
(fn [_]
(create-fn "dashboard:empty-folder-placeholder")))]
(create-fn "dashboard:empty-folder-placeholder")))
show-text (mf/use-state nil)
on-mouse-enter (mf/use-fn #(reset! show-text true))
on-mouse-leave (mf/use-fn #(reset! show-text nil))]
(cond
(true? dragging?)
[:ul
@@ -43,9 +46,15 @@
:else
[:div {:class (stl/css :grid-empty-placeholder)}
[:button {:class (stl/css :create-new)
:on-click on-click}
(if (cf/external-feature-flag "add-file-01" "test") (tr "dashboard.add-file") i/add)]])))
(if (cf/external-feature-flag "add-file-01" "test")
[:button {:class (stl/css :create-new)
:on-click on-click
:on-mouse-enter on-mouse-enter
:on-mouse-leave on-mouse-leave}
(if @show-text (tr "dashboard.add-file") i/add)]
[:button {:class (stl/css :create-new)
:on-click on-click}
i/add])])))
(mf/defc loading-placeholder
[]

View File

@@ -179,7 +179,7 @@
(let [mdata {:on-success on-file-created}
params {:project-id project-id}]
(st/emit! (-> (dd/create-file (with-meta params mdata))
(with-meta {::ev/origin origin}))))))
(with-meta {::ev/origin origin :has-files (> file-count 0)}))))))
on-create-click
(mf/use-fn

View File

@@ -97,6 +97,8 @@
active-color-tab (mf/use-state (dc/get-active-color-tab))
drag? (mf/use-state false)
type (if (= @active-color-tab "hsva") :hsv :rgb)
fill-image-ref (mf/use-ref nil)
selected-mode (get state :type :color)
@@ -358,7 +360,7 @@
:on-change-tab on-change-tab}]]
[:& color-inputs
{:type (if (= @active-color-tab :hsva) :hsv :rgb)
{:type type
:disable-opacity disable-opacity
:color current-color
:on-change handle-change-color}]

View File

@@ -120,7 +120,7 @@
(if (= type :rgb)
[:*
[:div {:class (stl/css :input-wrapper)}
[:span {:class (stl/css :input-label)} "R"]
[:label {:for "red-value" :class (stl/css :input-label)} "R"]
[:input {:id "red-value"
:ref (:r refs)
:type "number"
@@ -129,7 +129,7 @@
:default-value red
:on-change (on-change-property :r 255)}]]
[:div {:class (stl/css :input-wrapper)}
[:span {:class (stl/css :input-label)} "G"]
[:label {:for "green-value" :class (stl/css :input-label)} "G"]
[:input {:id "green-value"
:ref (:g refs)
:type "number"
@@ -138,7 +138,7 @@
:default-value green
:on-change (on-change-property :g 255)}]]
[:div {:class (stl/css :input-wrapper)}
[:span {:class (stl/css :input-label)} "B"]
[:label {:for "blue-value" :class (stl/css :input-label)} "B"]
[:input {:id "blue-value"
:ref (:b refs)
:type "number"
@@ -149,7 +149,7 @@
[:*
[:div {:class (stl/css :input-wrapper)}
[:span {:class (stl/css :input-label)} "H"]
[:label {:for "hue-value" :class (stl/css :input-label)} "H"]
[:input {:id "hue-value"
:ref (:h refs)
:type "number"
@@ -158,7 +158,7 @@
:default-value hue
:on-change (on-change-property :h 360)}]]
[:div {:class (stl/css :input-wrapper)}
[:span {:class (stl/css :input-label)} "S"]
[:label {:for "saturation-value" :class (stl/css :input-label)} "S"]
[:input {:id "saturation-value"
:ref (:s refs)
:type "number"
@@ -168,7 +168,7 @@
:default-value saturation
:on-change (on-change-property :s 100)}]]
[:div {:class (stl/css :input-wrapper)}
[:span {:class (stl/css :input-label)} "V"]
[:label {:for "value-value" :class (stl/css :input-label)} "V"]
[:input {:id "value-value"
:ref (:v refs)
:type "number"
@@ -179,7 +179,7 @@
[:div {:class (stl/css :hex-alpha-wrapper)}
[:div {:class (stl/css-case :input-wrapper true
:hex true)}
[:span {:class (stl/css :input-label)} "HEX"]
[:label {:for "hex-value" :class (stl/css :input-label)} "HEX"]
[:input {:id "hex-value"
:ref (:hex refs)
:default-value hex
@@ -187,7 +187,7 @@
:on-blur on-blur-hex}]]
(when (not disable-opacity)
[:div {:class (stl/css-case :input-wrapper true)}
[:span {:class (stl/css :input-label)} "A"]
[:label {:for "alpha-value" :class (stl/css :input-label)} "A"]
[:input {:id "alpha-value"
:ref (:alpha refs)
:type "number"

View File

@@ -358,6 +358,16 @@ export class SelectionController extends EventTarget {
detail: this.#currentStyle,
})
);
} else {
const firstInline = this.#textEditor.root?.firstElementChild?.firstElementChild;
if (firstInline) {
this.#updateCurrentStyle(firstInline);
this.dispatchEvent(
new CustomEvent("stylechange", {
detail: this.#currentStyle,
}),
);
}
}
}