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

This commit is contained in:
Andrey Antukh 2025-02-17 15:20:33 +01:00
commit c41f86f0a4
39 changed files with 682 additions and 205 deletions

View file

@ -80,6 +80,7 @@ is a number of cores)
- Fix problem opening url when page-id didn't exist [Taiga #10157](https://tree.taiga.io/project/penpot/issue/10157) - Fix problem opening url when page-id didn't exist [Taiga #10157](https://tree.taiga.io/project/penpot/issue/10157)
- Fix problem with onboarding to a team [Taiga #10143](https://tree.taiga.io/project/penpot/issue/10143) - Fix problem with onboarding to a team [Taiga #10143](https://tree.taiga.io/project/penpot/issue/10143)
- Fix problem with grid layout crashing [Taiga #10127](https://tree.taiga.io/project/penpot/issue/10127) - Fix problem with grid layout crashing [Taiga #10127](https://tree.taiga.io/project/penpot/issue/10127)
- Fix rename locked boards [Taiga #10174](https://tree.taiga.io/project/penpot/issue/10174)
## 2.4.3 ## 2.4.3

View file

@ -552,7 +552,6 @@
and p.team_id = ? and p.team_id = ?
order by f.modified_at desc") order by f.modified_at desc")
(defn- get-library-summary (defn- get-library-summary
[cfg {:keys [id data] :as file}] [cfg {:keys [id data] :as file}]
(letfn [(assets-sample [assets limit] (letfn [(assets-sample [assets limit]

View file

@ -963,7 +963,6 @@
{:title "string" {:title "string"
:description "not whitespace string" :description "not whitespace string"
:gen/gen (sg/word-string) :gen/gen (sg/word-string)
:error/code "errors.invalid-text"
:error/fn :error/fn
(fn [{:keys [value schema]}] (fn [{:keys [value schema]}]
(let [{:keys [max min] :as props} (properties schema)] (let [{:keys [max min] :as props} (properties schema)]
@ -971,16 +970,23 @@
(and (string? value) (and (string? value)
(number? max) (number? max)
(> (count value) max)) (> (count value) max))
["errors.field-max-length" max] {:code ["errors.field-max-length" max]}
(and (string? value) (and (string? value)
(number? min) (number? min)
(< (count value) min)) (< (count value) min))
["errors.field-min-length" min] {:code ["errors.field-min-length" min]}
(and (string? value)
(str/empty? value))
{:code "errors.field-missing"}
(and (string? value) (and (string? value)
(str/blank? value)) (str/blank? value))
"errors.field-not-all-whitespace")))}}) {:code "errors.field-not-all-whitespace"}
:else
{:code "errors.invalid-text"})))}})
(register! (register!
{:type ::password {:type ::password

View file

@ -588,51 +588,51 @@
;; - Blur ;; - Blur
;; - Border radius ;; - Border radius
(def ^:private basic-extract-props (def ^:private basic-extract-props
[:fills #{:fills
:strokes :strokes
:opacity :opacity
;; Layout Item ;; Layout Item
:layout-item-margin :layout-item-margin
:layout-item-margin-type :layout-item-margin-type
:layout-item-h-sizing :layout-item-h-sizing
:layout-item-v-sizing :layout-item-v-sizing
:layout-item-max-h :layout-item-max-h
:layout-item-min-h :layout-item-min-h
:layout-item-max-w :layout-item-max-w
:layout-item-min-w :layout-item-min-w
:layout-item-absolute :layout-item-absolute
:layout-item-z-index :layout-item-z-index
;; Constraints ;; Constraints
:constraints-h :constraints-h
:constraints-v :constraints-v
:shadow :shadow
:blur :blur
;; Radius ;; Radius
:r1 :r1
:r2 :r2
:r3 :r3
:r4]) :r4})
(def ^:private layout-extract-props (def ^:private layout-extract-props
[:layout #{:layout
:layout-flex-dir :layout-flex-dir
:layout-gap-type :layout-gap-type
:layout-gap :layout-gap
:layout-wrap-type :layout-wrap-type
:layout-align-items :layout-align-items
:layout-align-content :layout-align-content
:layout-justify-items :layout-justify-items
:layout-justify-content :layout-justify-content
:layout-padding-type :layout-padding-type
:layout-padding :layout-padding
:layout-grid-dir :layout-grid-dir
:layout-grid-rows :layout-grid-rows
:layout-grid-columns :layout-grid-columns
:layout-grid-cells]) :layout-grid-cells})
(defn extract-props (defn extract-props
"Retrieves an object with the 'pasteable' properties for a shape." "Retrieves an object with the 'pasteable' properties for a shape."
@ -668,10 +668,13 @@
[props shape] [props shape]
(d/patch-object props (select-keys shape layout-extract-props)))] (d/patch-object props (select-keys shape layout-extract-props)))]
(-> shape (let [;; For texts we don't extract the fill
(select-keys basic-extract-props) extract-props
(cond-> (cfh/text-shape? shape) (extract-text-props shape)) (cond-> basic-extract-props (cfh/text-shape? shape) (disj :fills))]
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape))))) (-> shape
(select-keys extract-props)
(cond-> (cfh/text-shape? shape) (extract-text-props shape))
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape))))))
(defn patch-props (defn patch-props
"Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary" "Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary"

View file

@ -0,0 +1,43 @@
{
"~#set": [
{
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:name": "Lorem Ipsum",
"~:revn": 2,
"~:modified-at": "~m1739356261950",
"~:vern": 0,
"~:id": "~u69b52fcf-7de0-81cd-8005-b9b180a0bfb5",
"~:thumbnail-id": "~u55bb9e08-6eed-4a64-a94d-2bcce7006e79",
"~:is-shared": true,
"~:project-id": "~u1ad2931c-eb80-8098-8005-b86c1d9d26c2",
"~:created-at": "~m1739356217030",
"~:library-summary": {
"~:components": {
"~:count": 0,
"~:sample": []
},
"~:media": {
"~:count": 0,
"~:sample": []
},
"~:colors": {
"~:count": 1,
"~:sample": [
{
"~:path": "",
"~:color": "#0087ff",
"~:name": "#0087ff",
"~:modified-at": "~m1739356244863",
"~:opacity": 1,
"~:id": "~u0449ccff-62fe-805c-8005-b9b194b094dd"
}
]
},
"~:typographies": {
"~:count": 0,
"~:sample": []
}
}
}
]
}

View file

@ -72,7 +72,7 @@ export class DashboardPage extends BaseWebSocketPage {
this.draftsLink = this.sidebar.getByText("Drafts"); this.draftsLink = this.sidebar.getByText("Drafts");
this.fontsLink = this.sidebar.getByText("Fonts"); this.fontsLink = this.sidebar.getByText("Fonts");
this.libsLink = this.sidebar.getByText("Libraries"); this.librariesLink = this.sidebar.getByText("Libraries");
this.searchButton = page.getByRole("button", { name: "dashboard-search" }); this.searchButton = page.getByRole("button", { name: "dashboard-search" });
this.searchInput = page.getByPlaceholder("Search…"); this.searchInput = page.getByPlaceholder("Search…");
@ -281,6 +281,13 @@ export class DashboardPage extends BaseWebSocketPage {
await this.userProfileOption.click(); await this.userProfileOption.click();
} }
async goToLibraries() {
await this.page.goto(
`#/dashboard/libraries?team-id=${DashboardPage.anyTeamId}`,
);
await expect(this.mainHeading).toHaveText("Libraries");
}
} }
export default DashboardPage; export default DashboardPage;

View file

@ -0,0 +1,33 @@
import { test, expect } from "@playwright/test";
import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test("BUG 10421 - Fix libraries context menu", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.mockRPC(
"get-team-shared-files?team-id=*",
"dashboard/get-team-shared-files-10142.json",
);
await dashboardPage.mockRPC(
"get-all-projects",
"dashboard/get-all-projects.json",
);
await dashboardPage.goToLibraries();
const libraryItem = page.getByTitle(/Lorem Ipsum/);
await expect(libraryItem).toBeVisible();
await libraryItem.getByRole("button", { name: "Options" }).click();
await expect(page.getByText("Rename")).toBeVisible();
});

View file

@ -47,7 +47,7 @@ test("User goes to an empty libraries page", async ({ page }) => {
await dashboardPage.setupLibrariesEmpty(); await dashboardPage.setupLibrariesEmpty();
await dashboardPage.goToDashboard(); await dashboardPage.goToDashboard();
await dashboardPage.libsLink.click(); await dashboardPage.librariesLink.click();
await expect(dashboardPage.mainHeading).toHaveText("Libraries"); await expect(dashboardPage.mainHeading).toHaveText("Libraries");
await expect(dashboardPage.page).toHaveScreenshot(); await expect(dashboardPage.page).toHaveScreenshot();
@ -100,7 +100,7 @@ test("User goes to a full library page", async ({ page }) => {
await dashboardPage.setupDashboardFull(); await dashboardPage.setupDashboardFull();
await dashboardPage.goToDashboard(); await dashboardPage.goToDashboard();
await dashboardPage.libsLink.click(); await dashboardPage.librariesLink.click();
await expect(dashboardPage.mainHeading).toHaveText("Libraries"); await expect(dashboardPage.mainHeading).toHaveText("Libraries");
await expect(dashboardPage.page).toHaveScreenshot(); await expect(dashboardPage.page).toHaveScreenshot();

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

View file

@ -188,8 +188,8 @@
(ptk/reify ::show-file-menu-with-position (ptk/reify ::show-file-menu-with-position
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :dashboard-local (update state :dashboard-local assoc
assoc :menu-open true :menu-open true
:menu-pos pos :menu-pos pos
:file-id file-id)))) :file-id file-id))))

View file

@ -55,7 +55,6 @@
(dissoc state :current-project-id) (dissoc state :current-project-id)
state))))) state)))))
(defn- files-fetched (defn- files-fetched
[project-id files] [project-id files]
(ptk/reify ::files-fetched (ptk/reify ::files-fetched
@ -67,14 +66,14 @@
(assoc project :count (count files)))))))) (assoc project :count (count files))))))))
(defn fetch-files (defn fetch-files
[project-id] ([] (fetch-files nil))
(assert (uuid? project-id) "expected valid uuid for `project-id`") ([project-id]
(ptk/reify ::fetch-files (ptk/reify ::fetch-files
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ state _]
(->> (rp/cmd! :get-project-files {:project-id project-id}) (when-let [project-id (or project-id (:current-project-id state))]
(rx/map (partial files-fetched project-id)))))) (->> (rp/cmd! :get-project-files {:project-id project-id})
(rx/map (partial files-fetched project-id))))))))

View file

@ -227,26 +227,6 @@
(->> (rp/cmd! :get-webhooks {:team-id team-id}) (->> (rp/cmd! :get-webhooks {:team-id team-id})
(rx/map (partial webhooks-fetched team-id))))))) (rx/map (partial webhooks-fetched team-id)))))))
(defn- shared-files-fetched
[files]
(ptk/reify ::shared-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [files (d/index-by :id files)]
(assoc state :shared-files files)))))
(defn fetch-shared-files
"Event mainly used for fetch a list of shared libraries for a team,
this list does not includes the content of the library per se. It
is used mainly for show available libraries and a summary of it."
[]
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
(rx/map shared-files-fetched))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Modification ;; Data Modification
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -567,6 +547,25 @@
(rx/of (fetch-webhooks))))) (rx/of (fetch-webhooks)))))
(rx/catch on-error)))))) (rx/catch on-error))))))
(defn- shared-files-fetched
[files]
(ptk/reify ::shared-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [files (d/index-by :id files)]
(update state :shared-files merge files)))))
(defn fetch-shared-files
"Event mainly used for fetch a list of shared libraries for a team,
this list does not includes the content of the library per se. It
is used mainly for show available libraries and a summary of it."
([] (fetch-shared-files nil))
([team-id]
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ state _]
(when-let [team-id (or team-id (:current-team-id state))]
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
(rx/map shared-files-fetched)))))))

View file

@ -197,8 +197,8 @@
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [route (:route state)
qparams (:query-params route) qparams (:query-params route)
index (:index qparams) index (some-> (:index qparams) parse-long)
frame-id (:frame-id qparams)] frame-id (some-> (:frame-id qparams) uuid/parse)]
(rx/merge (rx/merge
(rx/of (case (:zoom qparams) (rx/of (case (:zoom qparams)
"fit" zoom-to-fit "fit" zoom-to-fit
@ -520,8 +520,8 @@
(update [_ state] (update [_ state]
(let [route (:route state) (let [route (:route state)
qparams (:query-params route) qparams (:query-params route)
page-id (:page-id qparams) page-id (some-> (:page-id qparams) uuid/parse)
index (:index qparams) index (some-> (:index qparams) parse-long)
frames (get-in state [:viewer :pages page-id :frames]) frames (get-in state [:viewer :pages page-id :frames])
frame (get frames index)] frame (get frames index)]
(cond-> state (cond-> state
@ -538,7 +538,7 @@
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [route (:route state)
qparams (:query-params route) qparams (:query-params route)
page-id (:page-id qparams) page-id (some-> (:page-id qparams) uuid/parse)
frames (get-in state [:viewer :pages page-id :frames]) frames (get-in state [:viewer :pages page-id :frames])
index (d/index-of-pred frames #(= (:id %) frame-id))] index (d/index-of-pred frames #(= (:id %) frame-id))]
(rx/of (go-to-frame-by-index (or index 0)))))))) (rx/of (go-to-frame-by-index (or index 0))))))))
@ -550,7 +550,7 @@
(watch [_ state _] (watch [_ state _]
(let [route (:route state) (let [route (:route state)
qparams (:query-params route) qparams (:query-params route)
page-id (:page-id qparams) page-id (some-> (:page-id qparams) uuid/parse)
flows (get-in state [:viewer :pages page-id :options :flows])] flows (get-in state [:viewer :pages page-id :options :flows])]
(if (seq flows) (if (seq flows)
(let [frame-id (:starting-frame (first flows))] (let [frame-id (:starting-frame (first flows))]
@ -622,7 +622,7 @@
(update [_ state] (update [_ state]
(let [route (:route state) (let [route (:route state)
qparams (:query-params route) qparams (:query-params route)
page-id (:page-id qparams) page-id (some-> (:page-id qparams) uuid/parse)
frames (dm/get-in state [:viewer :pages page-id :all-frames]) frames (dm/get-in state [:viewer :pages page-id :all-frames])
frame (d/seek #(= (:id %) frame-id) frames) frame (d/seek #(= (:id %) frame-id) frames)
overlays (:viewer-overlays state)] overlays (:viewer-overlays state)]
@ -654,7 +654,7 @@
(update [_ state] (update [_ state]
(let [route (:route state) (let [route (:route state)
qparams (:query-params route) qparams (:query-params route)
page-id (:page-id qparams) page-id (some-> (:page-id qparams) uuid/parse)
frames (get-in state [:viewer :pages page-id :all-frames]) frames (get-in state [:viewer :pages page-id :all-frames])
frame (d/seek #(= (:id %) frame-id) frames) frame (d/seek #(= (:id %) frame-id) frames)
overlays (:viewer-overlays state)] overlays (:viewer-overlays state)]
@ -718,7 +718,7 @@
(update [_ state] (update [_ state]
(let [route (:route state) (let [route (:route state)
qparams (:query-params route) qparams (:query-params route)
page-id (:page-id qparams) page-id (some-> (:page-id qparams) uuid/parse)
objects (get-in state [:viewer :pages page-id :objects]) objects (get-in state [:viewer :pages page-id :objects])
selection (-> state selection (-> state
(get-in [:viewer-local :selected] #{}) (get-in [:viewer-local :selected] #{})
@ -734,8 +734,8 @@
(update [_ state] (update [_ state]
(let [route (:route state) (let [route (:route state)
qparams (:query-params route) qparams (:query-params route)
page-id (:page-id qparams) page-id (some-> (:page-id qparams) uuid/parse)
index (:index qparams) index (some-> (:index qparams) parse-long)
objects (get-in state [:viewer :pages page-id :objects]) objects (get-in state [:viewer :pages page-id :objects])
frame-id (get-in state [:viewer :pages page-id :frames index :id]) frame-id (get-in state [:viewer :pages page-id :frames index :id])

View file

@ -22,7 +22,6 @@
[app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.edition :as dwe] [app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.viewport :as dwv]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.streams :as ms] [app.main.streams :as ms]
@ -118,7 +117,7 @@
:page-id (:page-id thread))) :page-id (:page-id thread)))
(->> stream (->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport)) (rx/filter (ptk/type? ::dcmt/comment-threads-fetched))
(rx/take 1) (rx/take 1)
(rx/mapcat #(rx/of (center-to-comment-thread thread) (rx/mapcat #(rx/of (center-to-comment-thread thread)
(dwd/select-for-drawing :comments) (dwd/select-for-drawing :comments)

View file

@ -219,7 +219,7 @@
plugin-url (some-> params :plugin) plugin-url (some-> params :plugin)
template-url (some-> params :template)] template-url (some-> params :template)]
[:? [:?
#_[:& app.main.ui.releases/release-notes-modal {:version "2.4"}] #_[:& app.main.ui.releases/release-notes-modal {:version "2.5"}]
#_[:& app.main.ui.onboarding/onboarding-templates-modal] #_[:& app.main.ui.onboarding/onboarding-templates-modal]
#_[:& app.main.ui.onboarding/onboarding-modal] #_[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal] #_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]

View file

@ -61,7 +61,7 @@
[:validation :email-as-password] [:validation :email-as-password]
(swap! form assoc-in [:errors :password] (swap! form assoc-in [:errors :password]
{:code "errors.email-as-password"}) {:message (tr "errors.email-as-password")})
(st/emit! (ntf/error (tr "errors.generic"))))))) (st/emit! (ntf/error (tr "errors.generic")))))))

View file

@ -43,6 +43,7 @@
(def mentions-context (mf/create-context nil)) (def mentions-context (mf/create-context nil))
(def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)") (def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)")
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)") (def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
(def zero-width-space \u200B)
(defn- parse-comment (defn- parse-comment
"Parse a comment into its elements (texts and mentions)" "Parse a comment into its elements (texts and mentions)"
@ -78,7 +79,7 @@
([text] ([text]
(-> (dom/create-element "span") (-> (dom/create-element "span")
(dom/set-data! "type" "text") (dom/set-data! "type" "text")
(dom/set-html! (if (empty? text) "&#8203;" text))))) (dom/set-html! (if (empty? text) zero-width-space text)))))
(defn- create-mention-node (defn- create-mention-node
"Creates a mention node" "Creates a mention node"
@ -127,7 +128,7 @@
(or (str/blank? content) (or (str/blank? content)
(str/empty? content) (str/empty? content)
;; If only one char and it's the zero-width whitespace ;; If only one char and it's the zero-width whitespace
(and (= 1 (count content)) (= (first content) \u200B)))) (and (= 1 (count content)) (= (first content) zero-width-space))))
;; Component that renders the component content ;; Component that renders the component content
(mf/defc comment-content* (mf/defc comment-content*
@ -183,7 +184,7 @@
;; If a node is empty we set the content to "empty" ;; If a node is empty we set the content to "empty"
(when (and (= (dom/get-data child-node "type") "text") (when (and (= (dom/get-data child-node "type") "text")
(empty? (dom/get-text child-node))) (empty? (dom/get-text child-node)))
(dom/set-html! child-node "&#8203;")) (dom/set-html! child-node zero-width-space))
;; Remove mentions that have been modified ;; Remove mentions that have been modified
(when (and (= (dom/get-data child-node "type") "mention") (when (and (= (dom/get-data child-node "type") "mention")
@ -301,7 +302,7 @@
after-span (create-text-node (dm/str " " suffix)) after-span (create-text-node (dm/str " " suffix))
sel (wapi/get-selection)] sel (wapi/get-selection)]
(dom/set-html! span-node (if (empty? prefix) "&#8203;" prefix)) (dom/set-html! span-node (if (empty? prefix) zero-width-space prefix))
(dom/insert-after! node span-node mention-span) (dom/insert-after! node span-node mention-span)
(dom/insert-after! node mention-span after-span) (dom/insert-after! node mention-span after-span)
(wapi/set-cursor-after! after-span) (wapi/set-cursor-after! after-span)
@ -368,7 +369,7 @@
(when span-node (when span-node
(let [txt (.-textContent span-node)] (let [txt (.-textContent span-node)]
(dom/set-html! span-node (dm/str (subs txt 0 offset) "\n&#8203;" (subs txt offset))) (dom/set-html! span-node (dm/str (subs txt 0 offset) "\n" zero-width-space (subs txt offset)))
(wapi/set-cursor! span-node (inc offset)) (wapi/set-cursor! span-node (inc offset))
(handle-input))))) (handle-input)))))

View file

@ -33,6 +33,8 @@
more-classes (get props :class) more-classes (get props :class)
auto-focus? (get props :auto-focus? false) auto-focus? (get props :auto-focus? false)
data-testid (d/nilv data-testid input-name)
form (or form (mf/use-ctx form-ctx)) form (or form (mf/use-ctx form-ctx))
type' (mf/use-state input-type) type' (mf/use-state input-type)
@ -45,7 +47,9 @@
(= @type' "email")) (= @type' "email"))
placeholder (when is-text? (or placeholder label)) placeholder (when is-text? (or placeholder label))
touched? (get-in @form [:touched input-name]) touched? (and (contains? (:data @form) input-name)
(get-in @form [:touched input-name]))
error (get-in @form [:errors input-name]) error (get-in @form [:errors input-name])
value (get-in @form [:data input-name] "") value (get-in @form [:data input-name] "")
@ -153,6 +157,14 @@
children]) children])
(cond (cond
(and touched? (:message error) show-error)
(let [message (:message error)]
[:div {:id (dm/str "error-" input-name)
:class (stl/css :error)
:data-testid (dm/str data-testid "-error")}
message])
;; FIXME: DEPRECATED
(and touched? (:code error) show-error) (and touched? (:code error) show-error)
(let [code (:code error)] (let [code (:code error)]
[:div {:id (dm/str "error-" input-name) [:div {:id (dm/str "error-" input-name)
@ -173,7 +185,9 @@
focus? (mf/use-state false) focus? (mf/use-state false)
touched? (get-in @form [:touched input-name]) touched? (and (contains? (:data @form) input-name)
(get-in @form [:touched input-name]))
error (get-in @form [:errors input-name]) error (get-in @form [:errors input-name])
value (get-in @form [:data input-name] "") value (get-in @form [:data input-name] "")
@ -211,6 +225,9 @@
[:label {:class (stl/css :textarea-label)} label] [:label {:class (stl/css :textarea-label)} label]
[:> :textarea props] [:> :textarea props]
(cond (cond
(and touched? (:message error))
[:span {:class (stl/css :error)} (:message error)]
(and touched? (:code error)) (and touched? (:code error))
[:span {:class (stl/css :error)} (tr (:code error))] [:span {:class (stl/css :error)} (tr (:code error))]

View file

@ -316,19 +316,25 @@
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(dom/prevent-default event) (dom/prevent-default event)
(when-not selected? (when-not selected?
(when-not (kbd/shift? event) (when-not (kbd/shift? event)
(st/emit! (dd/clear-selected-files))) (st/emit! (dd/clear-selected-files)))
(st/emit! (dd/toggle-file-select file))) (do
(st/emit! (dd/toggle-file-select file))))
(let [client-position
(dom/get-client-position event)
position
(if (and (nil? (:y client-position)) (nil? (:x client-position)))
(let [target-element (dom/get-target event)
points (dom/get-bounding-rect target-element)
y (:top points)
x (:left points)]
(gpt/point x y))
client-position)]
(let [client-position (dom/get-client-position event)
position (if (and (nil? (:y client-position)) (nil? (:x client-position)))
(let [target-element (dom/get-target event)
points (dom/get-bounding-rect target-element)
y (:top points)
x (:left points)]
(gpt/point x y))
client-position)]
(st/emit! (dd/show-file-menu-with-position file-id position))))) (st/emit! (dd/show-file-menu-with-position file-id position)))))
on-context-menu on-context-menu
@ -401,50 +407,53 @@
[:h3 (:name file)]) [:h3 (:name file)])
[:& grid-item-metadata {:modified-at (:modified-at file)}]] [:& grid-item-metadata {:modified-at (:modified-at file)}]]
(when-not is-library-view [:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))}
[:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))} [:div
[:div {:class (stl/css :project-th-icon :menu)
{:class (stl/css :project-th-icon :menu) :tab-index "0"
:tab-index "0" :role "button"
:ref menu-ref :aria-label (tr "dashboard.options")
:id (str file-id "-action-menu") :ref menu-ref
:on-click on-menu-click :id (str file-id "-action-menu")
:on-key-down (fn [event] :on-click on-menu-click
(when (kbd/enter? event) :on-key-down (fn [event]
(dom/stop-propagation event) (when (kbd/enter? event)
(on-menu-click event)))} (dom/stop-propagation event)
menu-icon (on-menu-click event)))}
(when (and selected? file-menu-open?) menu-icon
(when (and selected? file-menu-open?)
;; When the menu is open we disable events in the dashboard. We need to force pointer events ;; When the menu is open we disable events in the dashboard. We need to force pointer events
;; so the menu can be handled ;; so the menu can be handled
[:div {:style {:pointer-events "all"}} [:div {:style {:pointer-events "all"}}
[:> file-menu* {:files (vals selected-files) [:> file-menu* {:files (vals selected-files)
:left (+ 24 (:x (:menu-pos dashboard-local))) :left (+ 24 (:x (:menu-pos dashboard-local)))
:top (:y (:menu-pos dashboard-local)) :top (:y (:menu-pos dashboard-local))
:can-edit can-edit :can-edit can-edit
:navigate true :navigate true
:on-edit on-edit :on-edit on-edit
:on-menu-close on-menu-close :on-menu-close on-menu-close
:origin origin :origin origin
:parent-id (dm/str file-id "-action-menu")}]])]])]]])) :parent-id (dm/str file-id "-action-menu")}]])]]]]]))
(mf/defc grid (mf/defc grid
{::mf/props :obj} {::mf/props :obj}
[{:keys [files project origin limit create-fn can-edit selected-files]}] [{:keys [files project origin limit create-fn can-edit selected-files]}]
(let [dragging? (mf/use-state false) (let [dragging? (mf/use-state false)
project-id (:id project) project-id (get project :id)
team-id (get project :team-id)
node-ref (mf/use-var nil) node-ref (mf/use-var nil)
on-finish-import on-finish-import
(mf/use-fn (mf/use-fn
(mf/deps project-id team-id)
(fn [] (fn []
(st/emit! (dpj/fetch-files project-id) (st/emit! (dpj/fetch-files project-id)
(dtm/fetch-shared-files) (dtm/fetch-shared-files team-id)
(dd/clear-selected-files)))) (dd/clear-selected-files))))
import-files
(use-import-file project-id on-finish-import)
import-files (use-import-file project-id on-finish-import)
on-drag-enter on-drag-enter
(mf/use-fn (mf/use-fn

View file

@ -15,23 +15,38 @@
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private ref:selected-files
(l/derived (fn [state]
(let [selected (get state :selected-files)
files (get state :shared-files)]
(refs/extract-selected-files files selected)))
st/state))
(mf/defc libraries-page* (mf/defc libraries-page*
{::mf/props :obj} {::mf/props :obj}
[{:keys [team default-project]}] [{:keys [team default-project]}]
(let [files (let [files
(mf/deref refs/shared-files) (mf/deref refs/shared-files)
files team-id
(mf/with-memo [files] (get team :id)
(->> (vals files)
(sort-by :modified-at)
(reverse)))
can-edit can-edit
(-> team :permissions :can-edit) (-> team :permissions :can-edit)
files
(mf/with-memo [files team-id]
(->> (vals files)
(filter #(= team-id (:team-id %)))
(sort-by :modified-at)
(reverse)))
selected-files
(mf/deref ref:selected-files)
[rowref limit] [rowref limit]
(hooks/use-dynamic-grid-item-width 350)] (hooks/use-dynamic-grid-item-width 350)]
@ -41,16 +56,19 @@
(:name team))] (:name team))]
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname)))) (dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))
(mf/with-effect [team] (mf/with-effect [team-id]
(st/emit! (dtm/fetch-shared-files) (st/emit! (dtm/fetch-shared-files team-id)
(dd/clear-selected-files))) (dd/clear-selected-files)))
[:* [:*
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
[:div#dashboard-libraries-title {:class (stl/css :dashboard-title)} [:div#dashboard-libraries-title {:class (stl/css :dashboard-title)}
[:h1 (tr "dashboard.libraries-title")]]] [:h1 (tr "dashboard.libraries-title")]]]
[:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared) :ref rowref}
[:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared)
:ref rowref}
[:& grid {:files files [:& grid {:files files
:selected-files selected-files
:project default-project :project default-project
:origin :libraries :origin :libraries
:limit limit :limit limit

View file

@ -34,3 +34,8 @@
text-decoration: none; text-decoration: none;
} }
} }
.notification-message {
display: flex;
align-items: center;
}

View file

@ -18,3 +18,7 @@
max-width: $s-960; max-width: $s-960;
z-index: $z-index-modal; z-index: $z-index-modal;
} }
.link {
margin: 0;
}

View file

@ -31,6 +31,7 @@
[app.main.ui.releases.v2-2] [app.main.ui.releases.v2-2]
[app.main.ui.releases.v2-3] [app.main.ui.releases.v2-3]
[app.main.ui.releases.v2-4] [app.main.ui.releases.v2-4]
[app.main.ui.releases.v2-5]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.timers :as tm] [app.util.timers :as tm]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -95,4 +96,4 @@
(defmethod rc/render-release-notes "0.0" (defmethod rc/render-release-notes "0.0"
[params] [params]
(rc/render-release-notes (assoc params :version "2.4"))) (rc/render-release-notes (assoc params :version "2.5")))

View file

@ -0,0 +1,175 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.releases.v2-5
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
;; TODO: Review all copies and alt text
(defmethod c/render-release-notes "2.5"
[{:keys [slide klass next finish navigate version]}]
(mf/html
(case slide
:start
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.5-slide-0.png"
:class (stl/css :start-image)
:border "0"
:alt "A graphic illustration with Penpot style"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Whats new in Penpot?"]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :features-block)}
[:span {:class (stl/css :feature-title)}
"Were thrilled to introduce Penpot 2.5"]
[:p {:class (stl/css :feature-content)}
"This release brings multi-step gradients, along with comment notifications, making it easier than ever to communicate with your team members. Now you also can easily copy/paste groups of styles between layers and share direct links to specific boards, among other new capabilities considered true gems for designers and team collaboration."]
[:p {:class (stl/css :feature-content)}
"But thats not all—weve also tackled numerous bug fixes and optimizations that will improve performance when working with long texts."]
[:p {:class (stl/css :feature-content)}
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.5-gradients.gif"
:class (stl/css :start-image)
:border "0"
:alt "Multi-step gradients and more"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Multi-step gradients and more"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"Were so happy to bring you one of our most requested features—multi-step gradients! Now, you can create smooth, complex color transitions with multiple stops, giving you more creative options to customize your designs."]
[:p {:class (stl/css :feature-content)}
"And thats not all. Weve also added quick actions to flip and rotate gradients, plus now you can adjust the radius for radial gradients. More control, more flexibility, more fun."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.5-mention.gif"
:class (stl/css :start-image)
:border "0"
:alt "Comment mentions and manage notifications"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Comment mentions and manage notifications"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"No more lost comments! You can now tag teammates in comments, and theyll get a notification so they never miss direct feedback. Plus, now you can filter mentions—just select 'Only your mentions' to quickly find discussions that matter to you."]
[:p {:class (stl/css :feature-content)}
"Weve also added a new section in your profile where you can customize your notifications, choosing what to receive on your dashboard and via email. On top of that, comments got a UI refresh, making everything clearer and better organized. And this is just the first batch of improvements—expect even more comment-related upgrades in the next Penpot release."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.5-copy.gif"
:class (stl/css :start-image)
:border "0"
:alt "Copy/paste styles, CSS, and text"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Copy/paste styles, CSS, and text"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"Easily copy and apply styles across your designs with just a few clicks. With the new Copy/Paste options, you can quickly transfer fills, strokes, shadows, and other properties from one layer to another—or multiple layers at once. Reusing styles is no longer a repetitive task."]
[:p {:class (stl/css :feature-content)}
"And weve also added more copy options:"]
[:p {:class (stl/css :feature-content)}
"- 'Copy as CSS' to grab the code instantly."]
[:p {:class (stl/css :feature-content)}
"- 'Copy as text' if you just need the content."]
[:p {:class (stl/css :feature-content)}
"Less manual work for a faster workflow. We hope you find it as useful as we do."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.5-link.gif"
:class (stl/css :start-image)
:border "0"
:alt "Links to specific boards"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Links to specific boards"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"In a single Penpot file, it's common to have multiple individual screens or designs spread across different boards. Now, you can generate direct links to each board, making it easy to share them with team members or include direct links in documentation."]
[:p {:class (stl/css :feature-content)}
"No more navigating through the design workspace of a file to find a specific screen—just send a link and take your team straight to the intended board."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))

View file

@ -0,0 +1,102 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@import "refactor/common-refactor.scss";
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: $s-324 1fr;
height: $s-500;
width: $s-888;
border-radius: $br-8;
background-color: var(--modal-background-color);
border: $s-2 solid var(--modal-border-color);
}
.start-image {
width: $s-324;
border-radius: $br-8 0 0 $br-8;
}
.modal-content {
padding: $s-40;
display: grid;
grid-template-rows: auto 1fr $s-32;
gap: $s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: $s-8;
}
.version-tag {
@include flexCenter;
@include headlineSmallTypography;
height: $s-32;
width: $s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: $br-8;
}
.modal-title {
@include headlineLargeTypography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: $s-16;
width: $s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: $s-8;
}
.feature-title {
@include bodyLargeTypography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include bodyMediumTypography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include bodyMediumTypography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: $s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend .button-primary;
width: $s-100;
justify-self: flex-end;
grid-area: button;
}

View file

@ -59,7 +59,7 @@
[:map {:title "EmailChangeForm"} [:map {:title "EmailChangeForm"}
[:email-1 ::sm/email] [:email-1 ::sm/email]
[:email-2 ::sm/email]] [:email-2 ::sm/email]]
[:fn {:error/code "errors.invalid-email-confirmation" [:fn {:error/fn #(tr "errors.invalid-email-confirmation")
:error/field :email-2} :error/field :email-2}
(fn [data] (fn [data]
(let [email-1 (:email-1 data) (let [email-1 (:email-1 data)

View file

@ -21,10 +21,10 @@
(case (:code (ex-data error)) (case (:code (ex-data error))
:old-password-not-match :old-password-not-match
(swap! form assoc-in [:errors :password-old] (swap! form assoc-in [:errors :password-old]
{:code "errors.wrong-old-password"}) {:message (tr "errors.wrong-old-password")})
:email-as-password :email-as-password
(swap! form assoc-in [:errors :password-1] (swap! form assoc-in [:errors :password-1]
{:code "errors.email-as-password"}) {:message (tr "errors.email-as-password")})
(let [msg (tr "generic.error")] (let [msg (tr "generic.error")]
(st/emit! (ntf/error msg))))) (st/emit! (ntf/error msg)))))

View file

@ -142,6 +142,8 @@
(let [id (:id library) (let [id (:id library)
importing? (deref importing) importing? (deref importing)
team-id (mf/use-ctx ctx/current-team-id)
on-error on-error
(mf/use-fn (mf/use-fn
(fn [_] (fn [_]
@ -150,11 +152,13 @@
on-success on-success
(mf/use-fn (mf/use-fn
(mf/deps team-id)
(fn [_] (fn [_]
(st/emit! (dtm/fetch-shared-files)))) (st/emit! (dtm/fetch-shared-files team-id))))
import-library import-library
(mf/use-fn (mf/use-fn
(mf/deps on-success on-error)
(fn [_] (fn [_]
(reset! importing id) (reset! importing id)
(st/emit! (dd/clone-template (st/emit! (dd/clone-template
@ -565,6 +569,7 @@
file (deref refs/file) file (deref refs/file)
file-id (:id file) file-id (:id file)
team-id (:team-id file)
shared? (:is-shared file) shared? (:is-shared file)
linked-libraries linked-libraries
@ -611,8 +616,8 @@
:id "updates" :id "updates"
:content updates-tab}]] :content updates-tab}]]
(mf/with-effect [] (mf/with-effect [team-id]
(st/emit! (dtm/fetch-shared-files))) (st/emit! (dtm/fetch-shared-files team-id)))
[:div {:class (stl/css :modal-overlay) [:div {:class (stl/css :modal-overlay)
:on-click close-dialog-outside :on-click close-dialog-outside

View file

@ -120,16 +120,17 @@
[:& layer-name {:ref name-ref [:& layer-name {:ref name-ref
:shape-id id :shape-id id
:shape-name name :shape-name name
:shape-touched? touched? :is-shape-touched touched?
:disabled-double-click read-only? :disabled-double-click read-only?
:on-start-edit on-disable-drag :on-start-edit on-disable-drag
:on-stop-edit on-enable-drag :on-stop-edit on-enable-drag
:depth depth :depth depth
:is-blocked blocked?
:parent-size parent-size :parent-size parent-size
:selected? selected? :is-selected selected?
:type-comp component-tree? :type-comp component-tree?
:type-frame (cfh/frame-shape? item) :type-frame (cfh/frame-shape? item)
:hidden? hidden?}] :is-hidden hidden?}]
(when (not read-only?) (when (not read-only?)
[:div {:class (stl/css-case [:div {:class (stl/css-case

View file

@ -27,9 +27,9 @@
(mf/defc layer-name (mf/defc layer-name
{::mf/wrap-props false {::mf/wrap-props false
::mf/forward-ref true} ::mf/forward-ref true}
[{:keys [shape-id shape-name shape-touched? disabled-double-click [{:keys [shape-id shape-name is-shape-touched disabled-double-click
on-start-edit on-stop-edit depth parent-size selected? on-start-edit on-stop-edit depth parent-size is-selected
type-comp type-frame hidden?]} external-ref] type-comp type-frame is-hidden is-blocked]} external-ref]
(let [edition* (mf/use-state false) (let [edition* (mf/use-state false)
edition? (deref edition*) edition? (deref edition*)
@ -42,9 +42,10 @@
start-edit start-edit
(mf/use-fn (mf/use-fn
(mf/deps disabled-double-click on-start-edit shape-id) (mf/deps disabled-double-click on-start-edit shape-id is-blocked)
(fn [] (fn []
(when (not disabled-double-click) (when (and (not is-blocked)
(not disabled-double-click))
(on-start-edit) (on-start-edit)
(reset! edition* true) (reset! edition* true)
(st/emit! (dw/start-rename-shape shape-id))))) (st/emit! (dw/start-rename-shape shape-id)))))
@ -102,8 +103,8 @@
{:class (stl/css-case {:class (stl/css-case
:element-name true :element-name true
:left-ellipsis has-path? :left-ellipsis has-path?
:selected selected? :selected is-selected
:hidden hidden? :hidden is-hidden
:type-comp type-comp :type-comp type-comp
:type-frame type-frame) :type-frame type-frame)
:style {"--depth" depth "--parent-size" parent-size} :style {"--depth" depth "--parent-size" parent-size}
@ -112,5 +113,5 @@
(if (dbg/enabled? :show-ids) (if (dbg/enabled? :show-ids)
(str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24)) (str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
(d/nilv shape-name ""))] (d/nilv shape-name ""))]
(when (and (dbg/enabled? :show-touched) ^boolean shape-touched?) (when (and (dbg/enabled? :show-touched) ^boolean is-shape-touched)
[:span {:class (stl/css :element-name-touched)} "*"])]))) [:span {:class (stl/css :element-name-touched)} "*"])])))

View file

@ -96,6 +96,8 @@
"var(--color-accent-tertiary)") "var(--color-accent-tertiary)")
"#8f9da3") ;; TODO: Set this color on the DS "#8f9da3") ;; TODO: Set this color on the DS
blocked? (:blocked frame)
on-pointer-down on-pointer-down
(mf/use-fn (mf/use-fn
(mf/deps (:id frame) on-frame-select workspace-read-only?) (mf/deps (:id frame) on-frame-select workspace-read-only?)
@ -145,9 +147,10 @@
start-edit start-edit
(mf/use-fn (mf/use-fn
(mf/deps frame-id edition?) (mf/deps frame-id edition? blocked? workspace-read-only?)
(fn [] (fn []
(when-not (-> @st/state :workspace-global :read-only?) (when (and (not blocked?)
(not workspace-read-only?))
(if (not edition?) (if (not edition?)
(reset! edition* true) (reset! edition* true)
(st/emit! (dw/start-rename-shape frame-id)))))) (st/emit! (dw/start-rename-shape frame-id))))))

View file

@ -87,4 +87,6 @@
padding-left: $s-6; padding-left: $s-6;
border: $s-1 solid var(--input-border-color-focus); border: $s-1 solid var(--input-border-color-focus);
color: var(--layer-row-foreground-color); color: var(--layer-row-foreground-color);
width: 100%;
max-width: initial;
} }

View file

@ -10,43 +10,78 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.util.i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str] [cuerdas.core :as str]
[malli.core :as m] [malli.core :as m]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
;; --- Handlers Helpers ;; --- Handlers Helpers
(defn- translate-code
[code]
(if (vector? code)
(tr (nth code 0) (i18n/c (nth code 1)))
(tr code)))
(defn- handle-error-fn
[props problem]
(let [v-fn (:error/fn props)
result (v-fn problem)]
(if (string? result)
{:message result}
{:message (or (some-> (get result :code)
(translate-code))
(get result :message)
(tr "errors.invalid-data"))})))
(defn- handle-error-message
[props]
{:message (get props :error/message)})
(defn- handle-error-code
[props]
(let [code (get props :error/code)]
{:message (translate-code code)}))
(defn- interpret-schema-problem (defn- interpret-schema-problem
[acc {:keys [schema in value] :as problem}] [acc {:keys [schema in value type] :as problem}]
(let [props (merge (m/type-properties schema) (let [props (m/properties schema)
(m/properties schema)) tprops (m/type-properties schema)
field (or (first in) (:error/field props))] field (or (first in)
(:error/field props))]
(if (contains? acc field) (if (contains? acc field)
acc acc
(cond (cond
(nil? value) (nil? field)
(assoc acc field {:code "errors.field-missing"}) acc
(contains? props :error/code) (or (= type :malli.core/missing-key)
(assoc acc field {:code (:error/code props)}) (nil? value))
(assoc acc field {:message (tr "errors.field-missing")})
;; --- CHECK on schema props
(contains? props :error/fn)
(assoc acc field (handle-error-fn props problem))
(contains? props :error/message) (contains? props :error/message)
(assoc acc field {:code (:error/message props)}) (assoc acc field (handle-error-message props))
(contains? props :error/fn) (contains? props :error/code)
(let [v-fn (:error/fn props) (assoc acc field (handle-error-code props))
code (v-fn problem)]
(assoc acc field {:code code}))
(contains? props :error/validators) ;; --- CHECK on type props
(let [validators (:error/validators props) (contains? tprops :error/fn)
props (reduce #(%2 %1 value) props validators)] (assoc acc field (handle-error-fn tprops problem))
(assoc acc field {:code (d/nilv (:error/code props) "errors.invalid-data")}))
(contains? tprops :error/message)
(assoc acc field (handle-error-message tprops))
(contains? tprops :error/code)
(assoc acc field (handle-error-code tprops))
:else :else
(assoc acc field {:code "errors.invalid-data"}))))) (assoc acc field {:message (tr "errors.invalid-data")})))))
(defn- use-rerender-fn (defn- use-rerender-fn
[] []
@ -177,21 +212,6 @@
;; --- Helper Components ;; --- Helper Components
(mf/defc field-error
[{:keys [form field type]
:as props}]
(let [{:keys [message] :as error} (dm/get-in form [:errors field])
touched? (dm/get-in form [:touched field])
show? (and touched? error message
(cond
(nil? type) true
(keyword? type) (= (:type error) type)
(ifn? type) (type (:type error))
:else false))]
(when show?
[:ul
[:li {:key (:code error)} (tr (:message error))]])))
(defn error-class (defn error-class
[form field] [form field]
(when (and (dm/get-in form [:errors field]) (when (and (dm/get-in form [:errors field])

View file

@ -1234,6 +1234,18 @@ msgstr "Something wrong has happened."
msgid "errors.invalid-color" msgid "errors.invalid-color"
msgstr "Invalid color" msgstr "Invalid color"
#: src/app/util/forms.cljs
msgid "errors.invalid-data"
msgstr "Invalid data"
#: src/app/util/forms.cljs
msgid "errors.field-missing"
msgstr "Empty field"
#: src/app/util/forms.cljs
msgid "errors.invalid-text"
msgstr "Invalid text"
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
#, unused #, unused
msgid "errors.invalid-email" msgid "errors.invalid-email"

View file

@ -1231,6 +1231,18 @@ msgstr "Ha ocurrido algún error."
msgid "errors.invalid-color" msgid "errors.invalid-color"
msgstr "Color no válido" msgstr "Color no válido"
#: src/app/util/forms.cljs
msgid "errors.invalid-data"
msgstr "Datos no válidos"
#: src/app/util/forms.cljs
msgid "errors.field-missing"
msgstr "Campo vacio"
#: src/app/util/forms.cljs
msgid "errors.invalid-text"
msgstr "Texto no válido"
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
#, unused #, unused
msgid "errors.invalid-email" msgid "errors.invalid-email"