diff --git a/CHANGES.md b/CHANGES.md index 8ec6d3581..2edb3d22f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 3d2098712..bc512c0b7 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -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] diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc index eaa4fffbd..e7329e621 100644 --- a/common/src/app/common/schema.cljc +++ b/common/src/app/common/schema.cljc @@ -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 diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 2b138a9d4..d32549782 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -588,51 +588,51 @@ ;; - Blur ;; - Border radius (def ^:private basic-extract-props - [:fills - :strokes - :opacity + #{:fills + :strokes + :opacity - ;; Layout Item - :layout-item-margin - :layout-item-margin-type - :layout-item-h-sizing - :layout-item-v-sizing - :layout-item-max-h - :layout-item-min-h - :layout-item-max-w - :layout-item-min-w - :layout-item-absolute - :layout-item-z-index + ;; Layout Item + :layout-item-margin + :layout-item-margin-type + :layout-item-h-sizing + :layout-item-v-sizing + :layout-item-max-h + :layout-item-min-h + :layout-item-max-w + :layout-item-min-w + :layout-item-absolute + :layout-item-z-index - ;; Constraints - :constraints-h - :constraints-v + ;; Constraints + :constraints-h + :constraints-v - :shadow - :blur + :shadow + :blur - ;; Radius - :r1 - :r2 - :r3 - :r4]) + ;; Radius + :r1 + :r2 + :r3 + :r4}) (def ^:private layout-extract-props - [:layout - :layout-flex-dir - :layout-gap-type - :layout-gap - :layout-wrap-type - :layout-align-items - :layout-align-content - :layout-justify-items - :layout-justify-content - :layout-padding-type - :layout-padding - :layout-grid-dir - :layout-grid-rows - :layout-grid-columns - :layout-grid-cells]) + #{:layout + :layout-flex-dir + :layout-gap-type + :layout-gap + :layout-wrap-type + :layout-align-items + :layout-align-content + :layout-justify-items + :layout-justify-content + :layout-padding-type + :layout-padding + :layout-grid-dir + :layout-grid-rows + :layout-grid-columns + :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)))] - (-> shape - (select-keys basic-extract-props) - (cond-> (cfh/text-shape? shape) (extract-text-props shape)) - (cond-> (ctsl/any-layout? shape) (extract-layout-props shape))))) + (let [;; For texts we don't extract the fill + extract-props + (cond-> basic-extract-props (cfh/text-shape? shape) (disj :fills))] + (-> 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 "Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary" diff --git a/frontend/playwright/data/dashboard/get-team-shared-files-10142.json b/frontend/playwright/data/dashboard/get-team-shared-files-10142.json new file mode 100644 index 000000000..4f974f3c9 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-shared-files-10142.json @@ -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": [] + } + } + } + ] +} diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js index 9dfde8843..f297218d3 100644 --- a/frontend/playwright/ui/pages/DashboardPage.js +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -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; diff --git a/frontend/playwright/ui/specs/dashboard-libraries.spec.js b/frontend/playwright/ui/specs/dashboard-libraries.spec.js new file mode 100644 index 000000000..d30f6e82c --- /dev/null +++ b/frontend/playwright/ui/specs/dashboard-libraries.spec.js @@ -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(); +}); diff --git a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js index 44bfb9abb..50ed19787 100644 --- a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js @@ -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(); diff --git a/frontend/resources/images/features/2.5-copy.gif b/frontend/resources/images/features/2.5-copy.gif new file mode 100644 index 000000000..6413b04c9 Binary files /dev/null and b/frontend/resources/images/features/2.5-copy.gif differ diff --git a/frontend/resources/images/features/2.5-gradients.gif b/frontend/resources/images/features/2.5-gradients.gif new file mode 100644 index 000000000..7894043f3 Binary files /dev/null and b/frontend/resources/images/features/2.5-gradients.gif differ diff --git a/frontend/resources/images/features/2.5-link.gif b/frontend/resources/images/features/2.5-link.gif new file mode 100644 index 000000000..cf2f5a9fd Binary files /dev/null and b/frontend/resources/images/features/2.5-link.gif differ diff --git a/frontend/resources/images/features/2.5-mention.gif b/frontend/resources/images/features/2.5-mention.gif new file mode 100644 index 000000000..1abefd20a Binary files /dev/null and b/frontend/resources/images/features/2.5-mention.gif differ diff --git a/frontend/resources/images/features/2.5-slide-0.png b/frontend/resources/images/features/2.5-slide-0.png new file mode 100644 index 000000000..58c5c54b9 Binary files /dev/null and b/frontend/resources/images/features/2.5-slide-0.png differ diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index ffa0681a8..f6ab552d6 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -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)))) diff --git a/frontend/src/app/main/data/project.cljs b/frontend/src/app/main/data/project.cljs index 74a927d8d..81db7856b 100644 --- a/frontend/src/app/main/data/project.cljs +++ b/frontend/src/app/main/data/project.cljs @@ -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`") - (ptk/reify ::fetch-files - ptk/WatchEvent - (watch [_ _ _] - (->> (rp/cmd! :get-project-files {:project-id project-id}) - (rx/map (partial files-fetched project-id)))))) - + ([] (fetch-files nil)) + ([project-id] + (ptk/reify ::fetch-files + ptk/WatchEvent + (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)))))))) diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index 5ff7cf097..50af94a44 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -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))))))) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 2aa9299ca..159fbe37c 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -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]) diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs index d88422177..c47fedeaa 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -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) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 328f2b3e4..3efcac7ff 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -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] diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 7b6bc2f99..ea75d0605 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -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"))))))) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 6e0d1da30..9843f1b33 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -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))))) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 3ed28eca0..ce5ea9996 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -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))] diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 5d8ed5a0e..bc5963634 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -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 [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))))) on-context-menu @@ -401,50 +407,53 @@ [: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" - :ref menu-ref - :id (str file-id "-action-menu") - :on-click on-menu-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-menu-click event)))} - menu-icon - (when (and selected? file-menu-open?) + [: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 + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (on-menu-click event)))} + 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 ;; so the menu can be handled - [:div {:style {:pointer-events "all"}} - [:> file-menu* {:files (vals selected-files) - :left (+ 24 (:x (:menu-pos dashboard-local))) - :top (:y (:menu-pos dashboard-local)) - :can-edit can-edit - :navigate true - :on-edit on-edit - :on-menu-close on-menu-close - :origin origin - :parent-id (dm/str file-id "-action-menu")}]])]])]]])) + [:div {:style {:pointer-events "all"}} + [:> file-menu* {:files (vals selected-files) + :left (+ 24 (:x (:menu-pos dashboard-local))) + :top (:y (:menu-pos dashboard-local)) + :can-edit can-edit + :navigate true + :on-edit on-edit + :on-menu-close on-menu-close + :origin origin + :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 diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index 1ba3e856f..dd9464c60 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -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 diff --git a/frontend/src/app/main/ui/ds/notifications/actionable.scss b/frontend/src/app/main/ui/ds/notifications/actionable.scss index ed11f7b11..d69dffcf4 100644 --- a/frontend/src/app/main/ui/ds/notifications/actionable.scss +++ b/frontend/src/app/main/ui/ds/notifications/actionable.scss @@ -34,3 +34,8 @@ text-decoration: none; } } + +.notification-message { + display: flex; + align-items: center; +} diff --git a/frontend/src/app/main/ui/notifications/inline_notification.scss b/frontend/src/app/main/ui/notifications/inline_notification.scss index 7deaff1c7..61381be5b 100644 --- a/frontend/src/app/main/ui/notifications/inline_notification.scss +++ b/frontend/src/app/main/ui/notifications/inline_notification.scss @@ -18,3 +18,7 @@ max-width: $s-960; z-index: $z-index-modal; } + +.link { + margin: 0; +} diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs index da769523d..a7cd6b7f2 100644 --- a/frontend/src/app/main/ui/releases.cljs +++ b/frontend/src/app/main/ui/releases.cljs @@ -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"))) diff --git a/frontend/src/app/main/ui/releases/v2_5.cljs b/frontend/src/app/main/ui/releases/v2_5.cljs new file mode 100644 index 000000000..fcbddc92c --- /dev/null +++ b/frontend/src/app/main/ui/releases/v2_5.cljs @@ -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"]]]]]]))) + diff --git a/frontend/src/app/main/ui/releases/v2_5.scss b/frontend/src/app/main/ui/releases/v2_5.scss new file mode 100644 index 000000000..dd1b81c82 --- /dev/null +++ b/frontend/src/app/main/ui/releases/v2_5.scss @@ -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; +} diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs index 5535c08c3..e11aadc95 100644 --- a/frontend/src/app/main/ui/settings/change_email.cljs +++ b/frontend/src/app/main/ui/settings/change_email.cljs @@ -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) diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs index 5de6d7b65..5de1e2796 100644 --- a/frontend/src/app/main/ui/settings/password.cljs +++ b/frontend/src/app/main/ui/settings/password.cljs @@ -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))))) diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 5a07a1a25..963385602 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs index 280f6a333..0ebb9849c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs index 43d87b40d..5765041a6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs @@ -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)} "*"])]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index a5ce2a790..3386d3b9a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -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)))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.scss b/frontend/src/app/main/ui/workspace/viewport/widgets.scss index 453043c0e..6df8e7a7d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.scss +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.scss @@ -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; } diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs index f628b06b2..5e818a7a3 100644 --- a/frontend/src/app/util/forms.cljs +++ b/frontend/src/app/util/forms.cljs @@ -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]) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index c7cb5848a..3db863a14 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 091c49e9b..c30a0976d 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -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"