mirror of
https://github.com/penpot/penpot.git
synced 2025-05-20 03:06:11 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
c41f86f0a4
39 changed files with 682 additions and 205 deletions
|
@ -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 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 rename locked boards [Taiga #10174](https://tree.taiga.io/project/penpot/issue/10174)
|
||||
|
||||
## 2.4.3
|
||||
|
||||
|
|
|
@ -552,7 +552,6 @@
|
|||
and p.team_id = ?
|
||||
order by f.modified_at desc")
|
||||
|
||||
|
||||
(defn- get-library-summary
|
||||
[cfg {:keys [id data] :as file}]
|
||||
(letfn [(assets-sample [assets limit]
|
||||
|
|
|
@ -963,7 +963,6 @@
|
|||
{:title "string"
|
||||
:description "not whitespace string"
|
||||
:gen/gen (sg/word-string)
|
||||
:error/code "errors.invalid-text"
|
||||
:error/fn
|
||||
(fn [{:keys [value schema]}]
|
||||
(let [{:keys [max min] :as props} (properties schema)]
|
||||
|
@ -971,16 +970,23 @@
|
|||
(and (string? value)
|
||||
(number? max)
|
||||
(> (count value) max))
|
||||
["errors.field-max-length" max]
|
||||
{:code ["errors.field-max-length" max]}
|
||||
|
||||
(and (string? value)
|
||||
(number? 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)
|
||||
(str/blank? value))
|
||||
"errors.field-not-all-whitespace")))}})
|
||||
{:code "errors.field-not-all-whitespace"}
|
||||
|
||||
:else
|
||||
{:code "errors.invalid-text"})))}})
|
||||
|
||||
(register!
|
||||
{:type ::password
|
||||
|
|
|
@ -588,7 +588,7 @@
|
|||
;; - Blur
|
||||
;; - Border radius
|
||||
(def ^:private basic-extract-props
|
||||
[:fills
|
||||
#{:fills
|
||||
:strokes
|
||||
:opacity
|
||||
|
||||
|
@ -615,10 +615,10 @@
|
|||
:r1
|
||||
:r2
|
||||
:r3
|
||||
:r4])
|
||||
:r4})
|
||||
|
||||
(def ^:private layout-extract-props
|
||||
[:layout
|
||||
#{:layout
|
||||
:layout-flex-dir
|
||||
:layout-gap-type
|
||||
:layout-gap
|
||||
|
@ -632,7 +632,7 @@
|
|||
:layout-grid-dir
|
||||
:layout-grid-rows
|
||||
:layout-grid-columns
|
||||
:layout-grid-cells])
|
||||
:layout-grid-cells})
|
||||
|
||||
(defn extract-props
|
||||
"Retrieves an object with the 'pasteable' properties for a shape."
|
||||
|
@ -668,10 +668,13 @@
|
|||
[props shape]
|
||||
(d/patch-object props (select-keys shape layout-extract-props)))]
|
||||
|
||||
(let [;; For texts we don't extract the fill
|
||||
extract-props
|
||||
(cond-> basic-extract-props (cfh/text-shape? shape) (disj :fills))]
|
||||
(-> shape
|
||||
(select-keys basic-extract-props)
|
||||
(select-keys extract-props)
|
||||
(cond-> (cfh/text-shape? shape) (extract-text-props shape))
|
||||
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape)))))
|
||||
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape))))))
|
||||
|
||||
(defn patch-props
|
||||
"Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary"
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -72,7 +72,7 @@ export class DashboardPage extends BaseWebSocketPage {
|
|||
|
||||
this.draftsLink = this.sidebar.getByText("Drafts");
|
||||
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.searchInput = page.getByPlaceholder("Search…");
|
||||
|
@ -281,6 +281,13 @@ export class DashboardPage extends BaseWebSocketPage {
|
|||
|
||||
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;
|
||||
|
|
33
frontend/playwright/ui/specs/dashboard-libraries.spec.js
Normal file
33
frontend/playwright/ui/specs/dashboard-libraries.spec.js
Normal 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();
|
||||
});
|
|
@ -47,7 +47,7 @@ test("User goes to an empty libraries page", async ({ page }) => {
|
|||
await dashboardPage.setupLibrariesEmpty();
|
||||
|
||||
await dashboardPage.goToDashboard();
|
||||
await dashboardPage.libsLink.click();
|
||||
await dashboardPage.librariesLink.click();
|
||||
|
||||
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
||||
await expect(dashboardPage.page).toHaveScreenshot();
|
||||
|
@ -100,7 +100,7 @@ test("User goes to a full library page", async ({ page }) => {
|
|||
await dashboardPage.setupDashboardFull();
|
||||
|
||||
await dashboardPage.goToDashboard();
|
||||
await dashboardPage.libsLink.click();
|
||||
await dashboardPage.librariesLink.click();
|
||||
|
||||
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
||||
await expect(dashboardPage.page).toHaveScreenshot();
|
||||
|
|
BIN
frontend/resources/images/features/2.5-copy.gif
Normal file
BIN
frontend/resources/images/features/2.5-copy.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 366 KiB |
BIN
frontend/resources/images/features/2.5-gradients.gif
Normal file
BIN
frontend/resources/images/features/2.5-gradients.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 697 KiB |
BIN
frontend/resources/images/features/2.5-link.gif
Normal file
BIN
frontend/resources/images/features/2.5-link.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
BIN
frontend/resources/images/features/2.5-mention.gif
Normal file
BIN
frontend/resources/images/features/2.5-mention.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 158 KiB |
BIN
frontend/resources/images/features/2.5-slide-0.png
Normal file
BIN
frontend/resources/images/features/2.5-slide-0.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 270 KiB |
|
@ -188,8 +188,8 @@
|
|||
(ptk/reify ::show-file-menu-with-position
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :dashboard-local
|
||||
assoc :menu-open true
|
||||
(update state :dashboard-local assoc
|
||||
:menu-open true
|
||||
:menu-pos pos
|
||||
:file-id file-id))))
|
||||
|
||||
|
|
|
@ -55,7 +55,6 @@
|
|||
(dissoc state :current-project-id)
|
||||
state)))))
|
||||
|
||||
|
||||
(defn- files-fetched
|
||||
[project-id files]
|
||||
(ptk/reify ::files-fetched
|
||||
|
@ -67,14 +66,14 @@
|
|||
(assoc project :count (count files))))))))
|
||||
|
||||
(defn fetch-files
|
||||
[project-id]
|
||||
(assert (uuid? project-id) "expected valid uuid for `project-id`")
|
||||
([] (fetch-files nil))
|
||||
([project-id]
|
||||
(ptk/reify ::fetch-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(watch [_ state _]
|
||||
(when-let [project-id (or project-id (:current-project-id state))]
|
||||
(->> (rp/cmd! :get-project-files {:project-id project-id})
|
||||
(rx/map (partial files-fetched project-id))))))
|
||||
|
||||
(rx/map (partial files-fetched project-id))))))))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -227,26 +227,6 @@
|
|||
(->> (rp/cmd! :get-webhooks {:team-id 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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -567,6 +547,25 @@
|
|||
(rx/of (fetch-webhooks)))))
|
||||
(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)))))))
|
||||
|
||||
|
||||
|
|
|
@ -197,8 +197,8 @@
|
|||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
index (:index qparams)
|
||||
frame-id (:frame-id qparams)]
|
||||
index (some-> (:index qparams) parse-long)
|
||||
frame-id (some-> (:frame-id qparams) uuid/parse)]
|
||||
(rx/merge
|
||||
(rx/of (case (:zoom qparams)
|
||||
"fit" zoom-to-fit
|
||||
|
@ -520,8 +520,8 @@
|
|||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
index (:index qparams)
|
||||
page-id (some-> (:page-id qparams) uuid/parse)
|
||||
index (some-> (:index qparams) parse-long)
|
||||
frames (get-in state [:viewer :pages page-id :frames])
|
||||
frame (get frames index)]
|
||||
(cond-> state
|
||||
|
@ -538,7 +538,7 @@
|
|||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
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])
|
||||
index (d/index-of-pred frames #(= (:id %) frame-id))]
|
||||
(rx/of (go-to-frame-by-index (or index 0))))))))
|
||||
|
@ -550,7 +550,7 @@
|
|||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
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])]
|
||||
(if (seq flows)
|
||||
(let [frame-id (:starting-frame (first flows))]
|
||||
|
@ -622,7 +622,7 @@
|
|||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
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])
|
||||
frame (d/seek #(= (:id %) frame-id) frames)
|
||||
overlays (:viewer-overlays state)]
|
||||
|
@ -654,7 +654,7 @@
|
|||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
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])
|
||||
frame (d/seek #(= (:id %) frame-id) frames)
|
||||
overlays (:viewer-overlays state)]
|
||||
|
@ -718,7 +718,7 @@
|
|||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
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])
|
||||
selection (-> state
|
||||
(get-in [:viewer-local :selected] #{})
|
||||
|
@ -734,8 +734,8 @@
|
|||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
index (:index qparams)
|
||||
page-id (some-> (:page-id qparams) uuid/parse)
|
||||
index (some-> (:index qparams) parse-long)
|
||||
objects (get-in state [:viewer :pages page-id :objects])
|
||||
frame-id (get-in state [:viewer :pages page-id :frames index :id])
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.edition :as dwe]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.viewport :as dwv]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.streams :as ms]
|
||||
|
@ -118,7 +117,7 @@
|
|||
:page-id (:page-id thread)))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
||||
(rx/filter (ptk/type? ::dcmt/comment-threads-fetched))
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rx/of (center-to-comment-thread thread)
|
||||
(dwd/select-for-drawing :comments)
|
||||
|
|
|
@ -219,7 +219,7 @@
|
|||
plugin-url (some-> params :plugin)
|
||||
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-modal]
|
||||
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
|
||||
[:validation :email-as-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")))))))
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
(def mentions-context (mf/create-context nil))
|
||||
(def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)")
|
||||
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
|
||||
(def zero-width-space \u200B)
|
||||
|
||||
(defn- parse-comment
|
||||
"Parse a comment into its elements (texts and mentions)"
|
||||
|
@ -78,7 +79,7 @@
|
|||
([text]
|
||||
(-> (dom/create-element "span")
|
||||
(dom/set-data! "type" "text")
|
||||
(dom/set-html! (if (empty? text) "​" text)))))
|
||||
(dom/set-html! (if (empty? text) zero-width-space text)))))
|
||||
|
||||
(defn- create-mention-node
|
||||
"Creates a mention node"
|
||||
|
@ -127,7 +128,7 @@
|
|||
(or (str/blank? content)
|
||||
(str/empty? content)
|
||||
;; 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
|
||||
(mf/defc comment-content*
|
||||
|
@ -183,7 +184,7 @@
|
|||
;; If a node is empty we set the content to "empty"
|
||||
(when (and (= (dom/get-data child-node "type") "text")
|
||||
(empty? (dom/get-text child-node)))
|
||||
(dom/set-html! child-node "​"))
|
||||
(dom/set-html! child-node zero-width-space))
|
||||
|
||||
;; Remove mentions that have been modified
|
||||
(when (and (= (dom/get-data child-node "type") "mention")
|
||||
|
@ -301,7 +302,7 @@
|
|||
after-span (create-text-node (dm/str " " suffix))
|
||||
sel (wapi/get-selection)]
|
||||
|
||||
(dom/set-html! span-node (if (empty? prefix) "​" 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 mention-span after-span)
|
||||
(wapi/set-cursor-after! after-span)
|
||||
|
@ -368,7 +369,7 @@
|
|||
|
||||
(when span-node
|
||||
(let [txt (.-textContent span-node)]
|
||||
(dom/set-html! span-node (dm/str (subs txt 0 offset) "\n​" (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))
|
||||
(handle-input)))))
|
||||
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
more-classes (get props :class)
|
||||
auto-focus? (get props :auto-focus? false)
|
||||
|
||||
data-testid (d/nilv data-testid input-name)
|
||||
|
||||
form (or form (mf/use-ctx form-ctx))
|
||||
|
||||
type' (mf/use-state input-type)
|
||||
|
@ -45,7 +47,9 @@
|
|||
(= @type' "email"))
|
||||
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])
|
||||
|
||||
value (get-in @form [:data input-name] "")
|
||||
|
@ -153,6 +157,14 @@
|
|||
children])
|
||||
|
||||
(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)
|
||||
(let [code (:code error)]
|
||||
[:div {:id (dm/str "error-" input-name)
|
||||
|
@ -173,7 +185,9 @@
|
|||
|
||||
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])
|
||||
|
||||
value (get-in @form [:data input-name] "")
|
||||
|
@ -211,6 +225,9 @@
|
|||
[:label {:class (stl/css :textarea-label)} label]
|
||||
[:> :textarea props]
|
||||
(cond
|
||||
(and touched? (:message error))
|
||||
[:span {:class (stl/css :error)} (:message error)]
|
||||
|
||||
(and touched? (:code error))
|
||||
[:span {:class (stl/css :error)} (tr (:code error))]
|
||||
|
||||
|
|
|
@ -316,19 +316,25 @@
|
|||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
|
||||
(when-not selected?
|
||||
(when-not (kbd/shift? event)
|
||||
(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 [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)))))
|
||||
|
||||
on-context-menu
|
||||
|
@ -401,11 +407,12 @@
|
|||
[:h3 (:name 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 :project-th-icon :menu)
|
||||
:tab-index "0"
|
||||
:role "button"
|
||||
:aria-label (tr "dashboard.options")
|
||||
:ref menu-ref
|
||||
:id (str file-id "-action-menu")
|
||||
:on-click on-menu-click
|
||||
|
@ -426,25 +433,27 @@
|
|||
:on-edit on-edit
|
||||
:on-menu-close on-menu-close
|
||||
:origin origin
|
||||
:parent-id (dm/str file-id "-action-menu")}]])]])]]]))
|
||||
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
|
||||
|
||||
(mf/defc grid
|
||||
{::mf/props :obj}
|
||||
[{:keys [files project origin limit create-fn can-edit selected-files]}]
|
||||
(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)
|
||||
|
||||
on-finish-import
|
||||
(mf/use-fn
|
||||
(mf/deps project-id team-id)
|
||||
(fn []
|
||||
(st/emit! (dpj/fetch-files project-id)
|
||||
(dtm/fetch-shared-files)
|
||||
(dtm/fetch-shared-files team-id)
|
||||
(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
|
||||
(mf/use-fn
|
||||
|
|
|
@ -15,23 +15,38 @@
|
|||
[app.main.ui.hooks :as hooks]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[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/props :obj}
|
||||
[{:keys [team default-project]}]
|
||||
(let [files
|
||||
(mf/deref refs/shared-files)
|
||||
|
||||
files
|
||||
(mf/with-memo [files]
|
||||
(->> (vals files)
|
||||
(sort-by :modified-at)
|
||||
(reverse)))
|
||||
team-id
|
||||
(get team :id)
|
||||
|
||||
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]
|
||||
(hooks/use-dynamic-grid-item-width 350)]
|
||||
|
||||
|
@ -41,16 +56,19 @@
|
|||
(:name team))]
|
||||
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))
|
||||
|
||||
(mf/with-effect [team]
|
||||
(st/emit! (dtm/fetch-shared-files)
|
||||
(mf/with-effect [team-id]
|
||||
(st/emit! (dtm/fetch-shared-files team-id)
|
||||
(dd/clear-selected-files)))
|
||||
|
||||
[:*
|
||||
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
||||
[:div#dashboard-libraries-title {:class (stl/css :dashboard-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
|
||||
:selected-files selected-files
|
||||
:project default-project
|
||||
:origin :libraries
|
||||
:limit limit
|
||||
|
|
|
@ -34,3 +34,8 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -18,3 +18,7 @@
|
|||
max-width: $s-960;
|
||||
z-index: $z-index-modal;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
[app.main.ui.releases.v2-2]
|
||||
[app.main.ui.releases.v2-3]
|
||||
[app.main.ui.releases.v2-4]
|
||||
[app.main.ui.releases.v2-5]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as tm]
|
||||
[rumext.v2 :as mf]))
|
||||
|
@ -95,4 +96,4 @@
|
|||
|
||||
(defmethod rc/render-release-notes "0.0"
|
||||
[params]
|
||||
(rc/render-release-notes (assoc params :version "2.4")))
|
||||
(rc/render-release-notes (assoc params :version "2.5")))
|
||||
|
|
175
frontend/src/app/main/ui/releases/v2_5.cljs
Normal file
175
frontend/src/app/main/ui/releases/v2_5.cljs
Normal 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)}
|
||||
"What’s 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)}
|
||||
"We’re 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 that’s not all—we’ve also tackled numerous bug fixes and optimizations that will improve performance when working with long texts."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Let’s 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)}
|
||||
"We’re 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 that’s not all. We’ve 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 they’ll 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)}
|
||||
"We’ve 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 we’ve 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"]]]]]])))
|
||||
|
102
frontend/src/app/main/ui/releases/v2_5.scss
Normal file
102
frontend/src/app/main/ui/releases/v2_5.scss
Normal 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;
|
||||
}
|
|
@ -59,7 +59,7 @@
|
|||
[:map {:title "EmailChangeForm"}
|
||||
[:email-1 ::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}
|
||||
(fn [data]
|
||||
(let [email-1 (:email-1 data)
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
(case (:code (ex-data error))
|
||||
:old-password-not-match
|
||||
(swap! form assoc-in [:errors :password-old]
|
||||
{:code "errors.wrong-old-password"})
|
||||
{:message (tr "errors.wrong-old-password")})
|
||||
:email-as-password
|
||||
(swap! form assoc-in [:errors :password-1]
|
||||
{:code "errors.email-as-password"})
|
||||
{:message (tr "errors.email-as-password")})
|
||||
|
||||
(let [msg (tr "generic.error")]
|
||||
(st/emit! (ntf/error msg)))))
|
||||
|
|
|
@ -142,6 +142,8 @@
|
|||
(let [id (:id library)
|
||||
importing? (deref importing)
|
||||
|
||||
team-id (mf/use-ctx ctx/current-team-id)
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
|
@ -150,11 +152,13 @@
|
|||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
(mf/deps team-id)
|
||||
(fn [_]
|
||||
(st/emit! (dtm/fetch-shared-files))))
|
||||
(st/emit! (dtm/fetch-shared-files team-id))))
|
||||
|
||||
import-library
|
||||
(mf/use-fn
|
||||
(mf/deps on-success on-error)
|
||||
(fn [_]
|
||||
(reset! importing id)
|
||||
(st/emit! (dd/clone-template
|
||||
|
@ -565,6 +569,7 @@
|
|||
file (deref refs/file)
|
||||
|
||||
file-id (:id file)
|
||||
team-id (:team-id file)
|
||||
shared? (:is-shared file)
|
||||
|
||||
linked-libraries
|
||||
|
@ -611,8 +616,8 @@
|
|||
:id "updates"
|
||||
:content updates-tab}]]
|
||||
|
||||
(mf/with-effect []
|
||||
(st/emit! (dtm/fetch-shared-files)))
|
||||
(mf/with-effect [team-id]
|
||||
(st/emit! (dtm/fetch-shared-files team-id)))
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)
|
||||
:on-click close-dialog-outside
|
||||
|
|
|
@ -120,16 +120,17 @@
|
|||
[:& layer-name {:ref name-ref
|
||||
:shape-id id
|
||||
:shape-name name
|
||||
:shape-touched? touched?
|
||||
:is-shape-touched touched?
|
||||
:disabled-double-click read-only?
|
||||
:on-start-edit on-disable-drag
|
||||
:on-stop-edit on-enable-drag
|
||||
:depth depth
|
||||
:is-blocked blocked?
|
||||
:parent-size parent-size
|
||||
:selected? selected?
|
||||
:is-selected selected?
|
||||
:type-comp component-tree?
|
||||
:type-frame (cfh/frame-shape? item)
|
||||
:hidden? hidden?}]
|
||||
:is-hidden hidden?}]
|
||||
|
||||
(when (not read-only?)
|
||||
[:div {:class (stl/css-case
|
||||
|
|
|
@ -27,9 +27,9 @@
|
|||
(mf/defc layer-name
|
||||
{::mf/wrap-props false
|
||||
::mf/forward-ref true}
|
||||
[{:keys [shape-id shape-name shape-touched? disabled-double-click
|
||||
on-start-edit on-stop-edit depth parent-size selected?
|
||||
type-comp type-frame hidden?]} external-ref]
|
||||
[{:keys [shape-id shape-name is-shape-touched disabled-double-click
|
||||
on-start-edit on-stop-edit depth parent-size is-selected
|
||||
type-comp type-frame is-hidden is-blocked]} external-ref]
|
||||
(let [edition* (mf/use-state false)
|
||||
edition? (deref edition*)
|
||||
|
||||
|
@ -42,9 +42,10 @@
|
|||
|
||||
start-edit
|
||||
(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 []
|
||||
(when (not disabled-double-click)
|
||||
(when (and (not is-blocked)
|
||||
(not disabled-double-click))
|
||||
(on-start-edit)
|
||||
(reset! edition* true)
|
||||
(st/emit! (dw/start-rename-shape shape-id)))))
|
||||
|
@ -102,8 +103,8 @@
|
|||
{:class (stl/css-case
|
||||
:element-name true
|
||||
:left-ellipsis has-path?
|
||||
:selected selected?
|
||||
:hidden hidden?
|
||||
:selected is-selected
|
||||
:hidden is-hidden
|
||||
:type-comp type-comp
|
||||
:type-frame type-frame)
|
||||
:style {"--depth" depth "--parent-size" parent-size}
|
||||
|
@ -112,5 +113,5 @@
|
|||
(if (dbg/enabled? :show-ids)
|
||||
(str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
|
||||
(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)} "*"])])))
|
||||
|
|
|
@ -96,6 +96,8 @@
|
|||
"var(--color-accent-tertiary)")
|
||||
"#8f9da3") ;; TODO: Set this color on the DS
|
||||
|
||||
blocked? (:blocked frame)
|
||||
|
||||
on-pointer-down
|
||||
(mf/use-fn
|
||||
(mf/deps (:id frame) on-frame-select workspace-read-only?)
|
||||
|
@ -145,9 +147,10 @@
|
|||
|
||||
start-edit
|
||||
(mf/use-fn
|
||||
(mf/deps frame-id edition?)
|
||||
(mf/deps frame-id edition? blocked? workspace-read-only?)
|
||||
(fn []
|
||||
(when-not (-> @st/state :workspace-global :read-only?)
|
||||
(when (and (not blocked?)
|
||||
(not workspace-read-only?))
|
||||
(if (not edition?)
|
||||
(reset! edition* true)
|
||||
(st/emit! (dw/start-rename-shape frame-id))))))
|
||||
|
|
|
@ -87,4 +87,6 @@
|
|||
padding-left: $s-6;
|
||||
border: $s-1 solid var(--input-border-color-focus);
|
||||
color: var(--layer-row-foreground-color);
|
||||
width: 100%;
|
||||
max-width: initial;
|
||||
}
|
||||
|
|
|
@ -10,43 +10,78 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- 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
|
||||
[acc {:keys [schema in value] :as problem}]
|
||||
(let [props (merge (m/type-properties schema)
|
||||
(m/properties schema))
|
||||
field (or (first in) (:error/field props))]
|
||||
[acc {:keys [schema in value type] :as problem}]
|
||||
(let [props (m/properties schema)
|
||||
tprops (m/type-properties schema)
|
||||
field (or (first in)
|
||||
(:error/field props))]
|
||||
|
||||
(if (contains? acc field)
|
||||
acc
|
||||
(cond
|
||||
(nil? value)
|
||||
(assoc acc field {:code "errors.field-missing"})
|
||||
(nil? field)
|
||||
acc
|
||||
|
||||
(contains? props :error/code)
|
||||
(assoc acc field {:code (:error/code props)})
|
||||
(or (= type :malli.core/missing-key)
|
||||
(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)
|
||||
(assoc acc field {:code (:error/message props)})
|
||||
(assoc acc field (handle-error-message props))
|
||||
|
||||
(contains? props :error/fn)
|
||||
(let [v-fn (:error/fn props)
|
||||
code (v-fn problem)]
|
||||
(assoc acc field {:code code}))
|
||||
(contains? props :error/code)
|
||||
(assoc acc field (handle-error-code props))
|
||||
|
||||
(contains? props :error/validators)
|
||||
(let [validators (:error/validators props)
|
||||
props (reduce #(%2 %1 value) props validators)]
|
||||
(assoc acc field {:code (d/nilv (:error/code props) "errors.invalid-data")}))
|
||||
;; --- CHECK on type props
|
||||
(contains? tprops :error/fn)
|
||||
(assoc acc field (handle-error-fn tprops problem))
|
||||
|
||||
(contains? tprops :error/message)
|
||||
(assoc acc field (handle-error-message tprops))
|
||||
|
||||
(contains? tprops :error/code)
|
||||
(assoc acc field (handle-error-code tprops))
|
||||
|
||||
:else
|
||||
(assoc acc field {:code "errors.invalid-data"})))))
|
||||
(assoc acc field {:message (tr "errors.invalid-data")})))))
|
||||
|
||||
(defn- use-rerender-fn
|
||||
[]
|
||||
|
@ -177,21 +212,6 @@
|
|||
|
||||
;; --- 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
|
||||
[form field]
|
||||
(when (and (dm/get-in form [:errors field])
|
||||
|
|
|
@ -1234,6 +1234,18 @@ msgstr "Something wrong has happened."
|
|||
msgid "errors.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
|
||||
#, unused
|
||||
msgid "errors.invalid-email"
|
||||
|
|
|
@ -1231,6 +1231,18 @@ msgstr "Ha ocurrido algún error."
|
|||
msgid "errors.invalid-color"
|
||||
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
|
||||
#, unused
|
||||
msgid "errors.invalid-email"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue