diff --git a/CHANGES.md b/CHANGES.md index 8ec6d35813..2edb3d22f4 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 3d20987122..bc512c0b7f 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 eaa4fffbdc..e7329e6210 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 2b138a9d48..d32549782a 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 0000000000..4f974f3c9e --- /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 9dfde88439..f297218d3e 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 0000000000..d30f6e82c0 --- /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 44bfb9abb0..50ed19787f 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 0000000000..6413b04c90 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 0000000000..7894043f34 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 0000000000..cf2f5a9fd4 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 0000000000..1abefd20a5 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 0000000000..58c5c54b96 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 ffa0681a83..f6ab552d6d 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 74a927d8d0..81db7856b4 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 5ff7cf097d..50af94a444 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 2aa9299ca0..159fbe37c6 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 d88422177c..c47fedeaa2 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 328f2b3e4e..3efcac7ff3 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 7b6bc2f996..ea75d06059 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 6e0d1da309..9843f1b335 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 3ed28eca06..ce5ea99967 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 5d8ed5a0ef..bc59636340 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 1ba3e856f1..dd9464c60c 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 ed11f7b11a..d69dffcf4e 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 7deaff1c75..61381be5bf 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 da769523d1..a7cd6b7f28 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 0000000000..fcbddc92c4 --- /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 0000000000..dd1b81c82b --- /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 5535c08c32..e11aadc95f 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 5de6d7b657..5de1e2796b 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 5a07a1a256..963385602f 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 280f6a3330..0ebb9849c2 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 43d87b40d3..5765041a67 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 a5ce2a790b..3386d3b9a4 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 453043c0ed..6df8e7a7d4 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 f628b06b21..5e818a7a3c 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 c7cb5848a1..3db863a147 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 091c49e9bf..c30a0976d7 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"