mirror of
https://github.com/penpot/penpot.git
synced 2025-07-05 12:27:17 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
c41f86f0a4
39 changed files with 682 additions and 205 deletions
|
@ -80,6 +80,7 @@ is a number of cores)
|
||||||
- Fix problem opening url when page-id didn't exist [Taiga #10157](https://tree.taiga.io/project/penpot/issue/10157)
|
- Fix problem opening url when page-id didn't exist [Taiga #10157](https://tree.taiga.io/project/penpot/issue/10157)
|
||||||
- Fix problem with onboarding to a team [Taiga #10143](https://tree.taiga.io/project/penpot/issue/10143)
|
- Fix problem with onboarding to a team [Taiga #10143](https://tree.taiga.io/project/penpot/issue/10143)
|
||||||
- Fix problem with grid layout crashing [Taiga #10127](https://tree.taiga.io/project/penpot/issue/10127)
|
- Fix problem with grid layout crashing [Taiga #10127](https://tree.taiga.io/project/penpot/issue/10127)
|
||||||
|
- Fix rename locked boards [Taiga #10174](https://tree.taiga.io/project/penpot/issue/10174)
|
||||||
|
|
||||||
## 2.4.3
|
## 2.4.3
|
||||||
|
|
||||||
|
|
|
@ -552,7 +552,6 @@
|
||||||
and p.team_id = ?
|
and p.team_id = ?
|
||||||
order by f.modified_at desc")
|
order by f.modified_at desc")
|
||||||
|
|
||||||
|
|
||||||
(defn- get-library-summary
|
(defn- get-library-summary
|
||||||
[cfg {:keys [id data] :as file}]
|
[cfg {:keys [id data] :as file}]
|
||||||
(letfn [(assets-sample [assets limit]
|
(letfn [(assets-sample [assets limit]
|
||||||
|
|
|
@ -963,7 +963,6 @@
|
||||||
{:title "string"
|
{:title "string"
|
||||||
:description "not whitespace string"
|
:description "not whitespace string"
|
||||||
:gen/gen (sg/word-string)
|
:gen/gen (sg/word-string)
|
||||||
:error/code "errors.invalid-text"
|
|
||||||
:error/fn
|
:error/fn
|
||||||
(fn [{:keys [value schema]}]
|
(fn [{:keys [value schema]}]
|
||||||
(let [{:keys [max min] :as props} (properties schema)]
|
(let [{:keys [max min] :as props} (properties schema)]
|
||||||
|
@ -971,16 +970,23 @@
|
||||||
(and (string? value)
|
(and (string? value)
|
||||||
(number? max)
|
(number? max)
|
||||||
(> (count value) max))
|
(> (count value) max))
|
||||||
["errors.field-max-length" max]
|
{:code ["errors.field-max-length" max]}
|
||||||
|
|
||||||
(and (string? value)
|
(and (string? value)
|
||||||
(number? min)
|
(number? min)
|
||||||
(< (count value) min))
|
(< (count value) min))
|
||||||
["errors.field-min-length" min]
|
{:code ["errors.field-min-length" min]}
|
||||||
|
|
||||||
|
(and (string? value)
|
||||||
|
(str/empty? value))
|
||||||
|
{:code "errors.field-missing"}
|
||||||
|
|
||||||
(and (string? value)
|
(and (string? value)
|
||||||
(str/blank? value))
|
(str/blank? value))
|
||||||
"errors.field-not-all-whitespace")))}})
|
{:code "errors.field-not-all-whitespace"}
|
||||||
|
|
||||||
|
:else
|
||||||
|
{:code "errors.invalid-text"})))}})
|
||||||
|
|
||||||
(register!
|
(register!
|
||||||
{:type ::password
|
{:type ::password
|
||||||
|
|
|
@ -588,51 +588,51 @@
|
||||||
;; - Blur
|
;; - Blur
|
||||||
;; - Border radius
|
;; - Border radius
|
||||||
(def ^:private basic-extract-props
|
(def ^:private basic-extract-props
|
||||||
[:fills
|
#{:fills
|
||||||
:strokes
|
:strokes
|
||||||
:opacity
|
:opacity
|
||||||
|
|
||||||
;; Layout Item
|
;; Layout Item
|
||||||
:layout-item-margin
|
:layout-item-margin
|
||||||
:layout-item-margin-type
|
:layout-item-margin-type
|
||||||
:layout-item-h-sizing
|
:layout-item-h-sizing
|
||||||
:layout-item-v-sizing
|
:layout-item-v-sizing
|
||||||
:layout-item-max-h
|
:layout-item-max-h
|
||||||
:layout-item-min-h
|
:layout-item-min-h
|
||||||
:layout-item-max-w
|
:layout-item-max-w
|
||||||
:layout-item-min-w
|
:layout-item-min-w
|
||||||
:layout-item-absolute
|
:layout-item-absolute
|
||||||
:layout-item-z-index
|
:layout-item-z-index
|
||||||
|
|
||||||
;; Constraints
|
;; Constraints
|
||||||
:constraints-h
|
:constraints-h
|
||||||
:constraints-v
|
:constraints-v
|
||||||
|
|
||||||
:shadow
|
:shadow
|
||||||
:blur
|
:blur
|
||||||
|
|
||||||
;; Radius
|
;; Radius
|
||||||
:r1
|
:r1
|
||||||
:r2
|
:r2
|
||||||
:r3
|
:r3
|
||||||
:r4])
|
:r4})
|
||||||
|
|
||||||
(def ^:private layout-extract-props
|
(def ^:private layout-extract-props
|
||||||
[:layout
|
#{:layout
|
||||||
:layout-flex-dir
|
:layout-flex-dir
|
||||||
:layout-gap-type
|
:layout-gap-type
|
||||||
:layout-gap
|
:layout-gap
|
||||||
:layout-wrap-type
|
:layout-wrap-type
|
||||||
:layout-align-items
|
:layout-align-items
|
||||||
:layout-align-content
|
:layout-align-content
|
||||||
:layout-justify-items
|
:layout-justify-items
|
||||||
:layout-justify-content
|
:layout-justify-content
|
||||||
:layout-padding-type
|
:layout-padding-type
|
||||||
:layout-padding
|
:layout-padding
|
||||||
:layout-grid-dir
|
:layout-grid-dir
|
||||||
:layout-grid-rows
|
:layout-grid-rows
|
||||||
:layout-grid-columns
|
:layout-grid-columns
|
||||||
:layout-grid-cells])
|
:layout-grid-cells})
|
||||||
|
|
||||||
(defn extract-props
|
(defn extract-props
|
||||||
"Retrieves an object with the 'pasteable' properties for a shape."
|
"Retrieves an object with the 'pasteable' properties for a shape."
|
||||||
|
@ -668,10 +668,13 @@
|
||||||
[props shape]
|
[props shape]
|
||||||
(d/patch-object props (select-keys shape layout-extract-props)))]
|
(d/patch-object props (select-keys shape layout-extract-props)))]
|
||||||
|
|
||||||
(-> shape
|
(let [;; For texts we don't extract the fill
|
||||||
(select-keys basic-extract-props)
|
extract-props
|
||||||
(cond-> (cfh/text-shape? shape) (extract-text-props shape))
|
(cond-> basic-extract-props (cfh/text-shape? shape) (disj :fills))]
|
||||||
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape)))))
|
(-> shape
|
||||||
|
(select-keys extract-props)
|
||||||
|
(cond-> (cfh/text-shape? shape) (extract-text-props shape))
|
||||||
|
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape))))))
|
||||||
|
|
||||||
(defn patch-props
|
(defn patch-props
|
||||||
"Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary"
|
"Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary"
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"~#set": [
|
||||||
|
{
|
||||||
|
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
|
||||||
|
"~:name": "Lorem Ipsum",
|
||||||
|
"~:revn": 2,
|
||||||
|
"~:modified-at": "~m1739356261950",
|
||||||
|
"~:vern": 0,
|
||||||
|
"~:id": "~u69b52fcf-7de0-81cd-8005-b9b180a0bfb5",
|
||||||
|
"~:thumbnail-id": "~u55bb9e08-6eed-4a64-a94d-2bcce7006e79",
|
||||||
|
"~:is-shared": true,
|
||||||
|
"~:project-id": "~u1ad2931c-eb80-8098-8005-b86c1d9d26c2",
|
||||||
|
"~:created-at": "~m1739356217030",
|
||||||
|
"~:library-summary": {
|
||||||
|
"~:components": {
|
||||||
|
"~:count": 0,
|
||||||
|
"~:sample": []
|
||||||
|
},
|
||||||
|
"~:media": {
|
||||||
|
"~:count": 0,
|
||||||
|
"~:sample": []
|
||||||
|
},
|
||||||
|
"~:colors": {
|
||||||
|
"~:count": 1,
|
||||||
|
"~:sample": [
|
||||||
|
{
|
||||||
|
"~:path": "",
|
||||||
|
"~:color": "#0087ff",
|
||||||
|
"~:name": "#0087ff",
|
||||||
|
"~:modified-at": "~m1739356244863",
|
||||||
|
"~:opacity": 1,
|
||||||
|
"~:id": "~u0449ccff-62fe-805c-8005-b9b194b094dd"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:typographies": {
|
||||||
|
"~:count": 0,
|
||||||
|
"~:sample": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -72,7 +72,7 @@ export class DashboardPage extends BaseWebSocketPage {
|
||||||
|
|
||||||
this.draftsLink = this.sidebar.getByText("Drafts");
|
this.draftsLink = this.sidebar.getByText("Drafts");
|
||||||
this.fontsLink = this.sidebar.getByText("Fonts");
|
this.fontsLink = this.sidebar.getByText("Fonts");
|
||||||
this.libsLink = this.sidebar.getByText("Libraries");
|
this.librariesLink = this.sidebar.getByText("Libraries");
|
||||||
|
|
||||||
this.searchButton = page.getByRole("button", { name: "dashboard-search" });
|
this.searchButton = page.getByRole("button", { name: "dashboard-search" });
|
||||||
this.searchInput = page.getByPlaceholder("Search…");
|
this.searchInput = page.getByPlaceholder("Search…");
|
||||||
|
@ -281,6 +281,13 @@ export class DashboardPage extends BaseWebSocketPage {
|
||||||
|
|
||||||
await this.userProfileOption.click();
|
await this.userProfileOption.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async goToLibraries() {
|
||||||
|
await this.page.goto(
|
||||||
|
`#/dashboard/libraries?team-id=${DashboardPage.anyTeamId}`,
|
||||||
|
);
|
||||||
|
await expect(this.mainHeading).toHaveText("Libraries");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DashboardPage;
|
export default DashboardPage;
|
||||||
|
|
33
frontend/playwright/ui/specs/dashboard-libraries.spec.js
Normal file
33
frontend/playwright/ui/specs/dashboard-libraries.spec.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import DashboardPage from "../pages/DashboardPage";
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await DashboardPage.init(page);
|
||||||
|
await DashboardPage.mockRPC(
|
||||||
|
page,
|
||||||
|
"get-profile",
|
||||||
|
"logged-in-user/get-profile-logged-in-no-onboarding.json",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("BUG 10421 - Fix libraries context menu", async ({ page }) => {
|
||||||
|
const dashboardPage = new DashboardPage(page);
|
||||||
|
await dashboardPage.mockRPC(
|
||||||
|
"get-team-shared-files?team-id=*",
|
||||||
|
"dashboard/get-team-shared-files-10142.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
await dashboardPage.mockRPC(
|
||||||
|
"get-all-projects",
|
||||||
|
"dashboard/get-all-projects.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
await dashboardPage.goToLibraries();
|
||||||
|
|
||||||
|
const libraryItem = page.getByTitle(/Lorem Ipsum/);
|
||||||
|
|
||||||
|
await expect(libraryItem).toBeVisible();
|
||||||
|
await libraryItem.getByRole("button", { name: "Options" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText("Rename")).toBeVisible();
|
||||||
|
});
|
|
@ -47,7 +47,7 @@ test("User goes to an empty libraries page", async ({ page }) => {
|
||||||
await dashboardPage.setupLibrariesEmpty();
|
await dashboardPage.setupLibrariesEmpty();
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
await dashboardPage.libsLink.click();
|
await dashboardPage.librariesLink.click();
|
||||||
|
|
||||||
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
|
@ -100,7 +100,7 @@ test("User goes to a full library page", async ({ page }) => {
|
||||||
await dashboardPage.setupDashboardFull();
|
await dashboardPage.setupDashboardFull();
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
await dashboardPage.libsLink.click();
|
await dashboardPage.librariesLink.click();
|
||||||
|
|
||||||
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
|
|
BIN
frontend/resources/images/features/2.5-copy.gif
Normal file
BIN
frontend/resources/images/features/2.5-copy.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 366 KiB |
BIN
frontend/resources/images/features/2.5-gradients.gif
Normal file
BIN
frontend/resources/images/features/2.5-gradients.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 697 KiB |
BIN
frontend/resources/images/features/2.5-link.gif
Normal file
BIN
frontend/resources/images/features/2.5-link.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
BIN
frontend/resources/images/features/2.5-mention.gif
Normal file
BIN
frontend/resources/images/features/2.5-mention.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 158 KiB |
BIN
frontend/resources/images/features/2.5-slide-0.png
Normal file
BIN
frontend/resources/images/features/2.5-slide-0.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 270 KiB |
|
@ -188,8 +188,8 @@
|
||||||
(ptk/reify ::show-file-menu-with-position
|
(ptk/reify ::show-file-menu-with-position
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update state :dashboard-local
|
(update state :dashboard-local assoc
|
||||||
assoc :menu-open true
|
:menu-open true
|
||||||
:menu-pos pos
|
:menu-pos pos
|
||||||
:file-id file-id))))
|
:file-id file-id))))
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,6 @@
|
||||||
(dissoc state :current-project-id)
|
(dissoc state :current-project-id)
|
||||||
state)))))
|
state)))))
|
||||||
|
|
||||||
|
|
||||||
(defn- files-fetched
|
(defn- files-fetched
|
||||||
[project-id files]
|
[project-id files]
|
||||||
(ptk/reify ::files-fetched
|
(ptk/reify ::files-fetched
|
||||||
|
@ -67,14 +66,14 @@
|
||||||
(assoc project :count (count files))))))))
|
(assoc project :count (count files))))))))
|
||||||
|
|
||||||
(defn fetch-files
|
(defn fetch-files
|
||||||
[project-id]
|
([] (fetch-files nil))
|
||||||
(assert (uuid? project-id) "expected valid uuid for `project-id`")
|
([project-id]
|
||||||
(ptk/reify ::fetch-files
|
(ptk/reify ::fetch-files
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ state _]
|
||||||
(->> (rp/cmd! :get-project-files {:project-id project-id})
|
(when-let [project-id (or project-id (:current-project-id state))]
|
||||||
(rx/map (partial files-fetched project-id))))))
|
(->> (rp/cmd! :get-project-files {:project-id project-id})
|
||||||
|
(rx/map (partial files-fetched project-id))))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -227,26 +227,6 @@
|
||||||
(->> (rp/cmd! :get-webhooks {:team-id team-id})
|
(->> (rp/cmd! :get-webhooks {:team-id team-id})
|
||||||
(rx/map (partial webhooks-fetched team-id)))))))
|
(rx/map (partial webhooks-fetched team-id)))))))
|
||||||
|
|
||||||
(defn- shared-files-fetched
|
|
||||||
[files]
|
|
||||||
(ptk/reify ::shared-files-fetched
|
|
||||||
ptk/UpdateEvent
|
|
||||||
(update [_ state]
|
|
||||||
(let [files (d/index-by :id files)]
|
|
||||||
(assoc state :shared-files files)))))
|
|
||||||
|
|
||||||
(defn fetch-shared-files
|
|
||||||
"Event mainly used for fetch a list of shared libraries for a team,
|
|
||||||
this list does not includes the content of the library per se. It
|
|
||||||
is used mainly for show available libraries and a summary of it."
|
|
||||||
[]
|
|
||||||
(ptk/reify ::fetch-shared-files
|
|
||||||
ptk/WatchEvent
|
|
||||||
(watch [_ state _]
|
|
||||||
(let [team-id (:current-team-id state)]
|
|
||||||
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
|
|
||||||
(rx/map shared-files-fetched))))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Data Modification
|
;; Data Modification
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -567,6 +547,25 @@
|
||||||
(rx/of (fetch-webhooks)))))
|
(rx/of (fetch-webhooks)))))
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
(defn- shared-files-fetched
|
||||||
|
[files]
|
||||||
|
(ptk/reify ::shared-files-fetched
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [files (d/index-by :id files)]
|
||||||
|
(update state :shared-files merge files)))))
|
||||||
|
|
||||||
|
(defn fetch-shared-files
|
||||||
|
"Event mainly used for fetch a list of shared libraries for a team,
|
||||||
|
this list does not includes the content of the library per se. It
|
||||||
|
is used mainly for show available libraries and a summary of it."
|
||||||
|
([] (fetch-shared-files nil))
|
||||||
|
([team-id]
|
||||||
|
(ptk/reify ::fetch-shared-files
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(when-let [team-id (or team-id (:current-team-id state))]
|
||||||
|
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
|
||||||
|
(rx/map shared-files-fetched)))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -197,8 +197,8 @@
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [route (:route state)
|
(let [route (:route state)
|
||||||
qparams (:query-params route)
|
qparams (:query-params route)
|
||||||
index (:index qparams)
|
index (some-> (:index qparams) parse-long)
|
||||||
frame-id (:frame-id qparams)]
|
frame-id (some-> (:frame-id qparams) uuid/parse)]
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(rx/of (case (:zoom qparams)
|
(rx/of (case (:zoom qparams)
|
||||||
"fit" zoom-to-fit
|
"fit" zoom-to-fit
|
||||||
|
@ -520,8 +520,8 @@
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [route (:route state)
|
(let [route (:route state)
|
||||||
qparams (:query-params route)
|
qparams (:query-params route)
|
||||||
page-id (:page-id qparams)
|
page-id (some-> (:page-id qparams) uuid/parse)
|
||||||
index (:index qparams)
|
index (some-> (:index qparams) parse-long)
|
||||||
frames (get-in state [:viewer :pages page-id :frames])
|
frames (get-in state [:viewer :pages page-id :frames])
|
||||||
frame (get frames index)]
|
frame (get frames index)]
|
||||||
(cond-> state
|
(cond-> state
|
||||||
|
@ -538,7 +538,7 @@
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [route (:route state)
|
(let [route (:route state)
|
||||||
qparams (:query-params route)
|
qparams (:query-params route)
|
||||||
page-id (:page-id qparams)
|
page-id (some-> (:page-id qparams) uuid/parse)
|
||||||
frames (get-in state [:viewer :pages page-id :frames])
|
frames (get-in state [:viewer :pages page-id :frames])
|
||||||
index (d/index-of-pred frames #(= (:id %) frame-id))]
|
index (d/index-of-pred frames #(= (:id %) frame-id))]
|
||||||
(rx/of (go-to-frame-by-index (or index 0))))))))
|
(rx/of (go-to-frame-by-index (or index 0))))))))
|
||||||
|
@ -550,7 +550,7 @@
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [route (:route state)
|
(let [route (:route state)
|
||||||
qparams (:query-params route)
|
qparams (:query-params route)
|
||||||
page-id (:page-id qparams)
|
page-id (some-> (:page-id qparams) uuid/parse)
|
||||||
flows (get-in state [:viewer :pages page-id :options :flows])]
|
flows (get-in state [:viewer :pages page-id :options :flows])]
|
||||||
(if (seq flows)
|
(if (seq flows)
|
||||||
(let [frame-id (:starting-frame (first flows))]
|
(let [frame-id (:starting-frame (first flows))]
|
||||||
|
@ -622,7 +622,7 @@
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [route (:route state)
|
(let [route (:route state)
|
||||||
qparams (:query-params route)
|
qparams (:query-params route)
|
||||||
page-id (:page-id qparams)
|
page-id (some-> (:page-id qparams) uuid/parse)
|
||||||
frames (dm/get-in state [:viewer :pages page-id :all-frames])
|
frames (dm/get-in state [:viewer :pages page-id :all-frames])
|
||||||
frame (d/seek #(= (:id %) frame-id) frames)
|
frame (d/seek #(= (:id %) frame-id) frames)
|
||||||
overlays (:viewer-overlays state)]
|
overlays (:viewer-overlays state)]
|
||||||
|
@ -654,7 +654,7 @@
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [route (:route state)
|
(let [route (:route state)
|
||||||
qparams (:query-params route)
|
qparams (:query-params route)
|
||||||
page-id (:page-id qparams)
|
page-id (some-> (:page-id qparams) uuid/parse)
|
||||||
frames (get-in state [:viewer :pages page-id :all-frames])
|
frames (get-in state [:viewer :pages page-id :all-frames])
|
||||||
frame (d/seek #(= (:id %) frame-id) frames)
|
frame (d/seek #(= (:id %) frame-id) frames)
|
||||||
overlays (:viewer-overlays state)]
|
overlays (:viewer-overlays state)]
|
||||||
|
@ -718,7 +718,7 @@
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [route (:route state)
|
(let [route (:route state)
|
||||||
qparams (:query-params route)
|
qparams (:query-params route)
|
||||||
page-id (:page-id qparams)
|
page-id (some-> (:page-id qparams) uuid/parse)
|
||||||
objects (get-in state [:viewer :pages page-id :objects])
|
objects (get-in state [:viewer :pages page-id :objects])
|
||||||
selection (-> state
|
selection (-> state
|
||||||
(get-in [:viewer-local :selected] #{})
|
(get-in [:viewer-local :selected] #{})
|
||||||
|
@ -734,8 +734,8 @@
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [route (:route state)
|
(let [route (:route state)
|
||||||
qparams (:query-params route)
|
qparams (:query-params route)
|
||||||
page-id (:page-id qparams)
|
page-id (some-> (:page-id qparams) uuid/parse)
|
||||||
index (:index qparams)
|
index (some-> (:index qparams) parse-long)
|
||||||
objects (get-in state [:viewer :pages page-id :objects])
|
objects (get-in state [:viewer :pages page-id :objects])
|
||||||
frame-id (get-in state [:viewer :pages page-id :frames index :id])
|
frame-id (get-in state [:viewer :pages page-id :frames index :id])
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
[app.main.data.workspace.drawing :as dwd]
|
[app.main.data.workspace.drawing :as dwd]
|
||||||
[app.main.data.workspace.edition :as dwe]
|
[app.main.data.workspace.edition :as dwe]
|
||||||
[app.main.data.workspace.selection :as dws]
|
[app.main.data.workspace.selection :as dws]
|
||||||
[app.main.data.workspace.viewport :as dwv]
|
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.router :as rt]
|
[app.main.router :as rt]
|
||||||
[app.main.streams :as ms]
|
[app.main.streams :as ms]
|
||||||
|
@ -118,7 +117,7 @@
|
||||||
:page-id (:page-id thread)))
|
:page-id (:page-id thread)))
|
||||||
|
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
(rx/filter (ptk/type? ::dcmt/comment-threads-fetched))
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
(rx/mapcat #(rx/of (center-to-comment-thread thread)
|
(rx/mapcat #(rx/of (center-to-comment-thread thread)
|
||||||
(dwd/select-for-drawing :comments)
|
(dwd/select-for-drawing :comments)
|
||||||
|
|
|
@ -219,7 +219,7 @@
|
||||||
plugin-url (some-> params :plugin)
|
plugin-url (some-> params :plugin)
|
||||||
template-url (some-> params :template)]
|
template-url (some-> params :template)]
|
||||||
[:?
|
[:?
|
||||||
#_[:& app.main.ui.releases/release-notes-modal {:version "2.4"}]
|
#_[:& app.main.ui.releases/release-notes-modal {:version "2.5"}]
|
||||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||||
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
[:validation :email-as-password]
|
[:validation :email-as-password]
|
||||||
(swap! form assoc-in [:errors :password]
|
(swap! form assoc-in [:errors :password]
|
||||||
{:code "errors.email-as-password"})
|
{:message (tr "errors.email-as-password")})
|
||||||
|
|
||||||
(st/emit! (ntf/error (tr "errors.generic")))))))
|
(st/emit! (ntf/error (tr "errors.generic")))))))
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
(def mentions-context (mf/create-context nil))
|
(def mentions-context (mf/create-context nil))
|
||||||
(def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)")
|
(def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)")
|
||||||
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
|
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
|
||||||
|
(def zero-width-space \u200B)
|
||||||
|
|
||||||
(defn- parse-comment
|
(defn- parse-comment
|
||||||
"Parse a comment into its elements (texts and mentions)"
|
"Parse a comment into its elements (texts and mentions)"
|
||||||
|
@ -78,7 +79,7 @@
|
||||||
([text]
|
([text]
|
||||||
(-> (dom/create-element "span")
|
(-> (dom/create-element "span")
|
||||||
(dom/set-data! "type" "text")
|
(dom/set-data! "type" "text")
|
||||||
(dom/set-html! (if (empty? text) "​" text)))))
|
(dom/set-html! (if (empty? text) zero-width-space text)))))
|
||||||
|
|
||||||
(defn- create-mention-node
|
(defn- create-mention-node
|
||||||
"Creates a mention node"
|
"Creates a mention node"
|
||||||
|
@ -127,7 +128,7 @@
|
||||||
(or (str/blank? content)
|
(or (str/blank? content)
|
||||||
(str/empty? content)
|
(str/empty? content)
|
||||||
;; If only one char and it's the zero-width whitespace
|
;; If only one char and it's the zero-width whitespace
|
||||||
(and (= 1 (count content)) (= (first content) \u200B))))
|
(and (= 1 (count content)) (= (first content) zero-width-space))))
|
||||||
|
|
||||||
;; Component that renders the component content
|
;; Component that renders the component content
|
||||||
(mf/defc comment-content*
|
(mf/defc comment-content*
|
||||||
|
@ -183,7 +184,7 @@
|
||||||
;; If a node is empty we set the content to "empty"
|
;; If a node is empty we set the content to "empty"
|
||||||
(when (and (= (dom/get-data child-node "type") "text")
|
(when (and (= (dom/get-data child-node "type") "text")
|
||||||
(empty? (dom/get-text child-node)))
|
(empty? (dom/get-text child-node)))
|
||||||
(dom/set-html! child-node "​"))
|
(dom/set-html! child-node zero-width-space))
|
||||||
|
|
||||||
;; Remove mentions that have been modified
|
;; Remove mentions that have been modified
|
||||||
(when (and (= (dom/get-data child-node "type") "mention")
|
(when (and (= (dom/get-data child-node "type") "mention")
|
||||||
|
@ -301,7 +302,7 @@
|
||||||
after-span (create-text-node (dm/str " " suffix))
|
after-span (create-text-node (dm/str " " suffix))
|
||||||
sel (wapi/get-selection)]
|
sel (wapi/get-selection)]
|
||||||
|
|
||||||
(dom/set-html! span-node (if (empty? prefix) "​" prefix))
|
(dom/set-html! span-node (if (empty? prefix) zero-width-space prefix))
|
||||||
(dom/insert-after! node span-node mention-span)
|
(dom/insert-after! node span-node mention-span)
|
||||||
(dom/insert-after! node mention-span after-span)
|
(dom/insert-after! node mention-span after-span)
|
||||||
(wapi/set-cursor-after! after-span)
|
(wapi/set-cursor-after! after-span)
|
||||||
|
@ -368,7 +369,7 @@
|
||||||
|
|
||||||
(when span-node
|
(when span-node
|
||||||
(let [txt (.-textContent span-node)]
|
(let [txt (.-textContent span-node)]
|
||||||
(dom/set-html! span-node (dm/str (subs txt 0 offset) "\n​" (subs txt offset)))
|
(dom/set-html! span-node (dm/str (subs txt 0 offset) "\n" zero-width-space (subs txt offset)))
|
||||||
(wapi/set-cursor! span-node (inc offset))
|
(wapi/set-cursor! span-node (inc offset))
|
||||||
(handle-input)))))
|
(handle-input)))))
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,8 @@
|
||||||
more-classes (get props :class)
|
more-classes (get props :class)
|
||||||
auto-focus? (get props :auto-focus? false)
|
auto-focus? (get props :auto-focus? false)
|
||||||
|
|
||||||
|
data-testid (d/nilv data-testid input-name)
|
||||||
|
|
||||||
form (or form (mf/use-ctx form-ctx))
|
form (or form (mf/use-ctx form-ctx))
|
||||||
|
|
||||||
type' (mf/use-state input-type)
|
type' (mf/use-state input-type)
|
||||||
|
@ -45,7 +47,9 @@
|
||||||
(= @type' "email"))
|
(= @type' "email"))
|
||||||
placeholder (when is-text? (or placeholder label))
|
placeholder (when is-text? (or placeholder label))
|
||||||
|
|
||||||
touched? (get-in @form [:touched input-name])
|
touched? (and (contains? (:data @form) input-name)
|
||||||
|
(get-in @form [:touched input-name]))
|
||||||
|
|
||||||
error (get-in @form [:errors input-name])
|
error (get-in @form [:errors input-name])
|
||||||
|
|
||||||
value (get-in @form [:data input-name] "")
|
value (get-in @form [:data input-name] "")
|
||||||
|
@ -153,6 +157,14 @@
|
||||||
children])
|
children])
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
|
(and touched? (:message error) show-error)
|
||||||
|
(let [message (:message error)]
|
||||||
|
[:div {:id (dm/str "error-" input-name)
|
||||||
|
:class (stl/css :error)
|
||||||
|
:data-testid (dm/str data-testid "-error")}
|
||||||
|
message])
|
||||||
|
|
||||||
|
;; FIXME: DEPRECATED
|
||||||
(and touched? (:code error) show-error)
|
(and touched? (:code error) show-error)
|
||||||
(let [code (:code error)]
|
(let [code (:code error)]
|
||||||
[:div {:id (dm/str "error-" input-name)
|
[:div {:id (dm/str "error-" input-name)
|
||||||
|
@ -173,7 +185,9 @@
|
||||||
|
|
||||||
focus? (mf/use-state false)
|
focus? (mf/use-state false)
|
||||||
|
|
||||||
touched? (get-in @form [:touched input-name])
|
touched? (and (contains? (:data @form) input-name)
|
||||||
|
(get-in @form [:touched input-name]))
|
||||||
|
|
||||||
error (get-in @form [:errors input-name])
|
error (get-in @form [:errors input-name])
|
||||||
|
|
||||||
value (get-in @form [:data input-name] "")
|
value (get-in @form [:data input-name] "")
|
||||||
|
@ -211,6 +225,9 @@
|
||||||
[:label {:class (stl/css :textarea-label)} label]
|
[:label {:class (stl/css :textarea-label)} label]
|
||||||
[:> :textarea props]
|
[:> :textarea props]
|
||||||
(cond
|
(cond
|
||||||
|
(and touched? (:message error))
|
||||||
|
[:span {:class (stl/css :error)} (:message error)]
|
||||||
|
|
||||||
(and touched? (:code error))
|
(and touched? (:code error))
|
||||||
[:span {:class (stl/css :error)} (tr (:code error))]
|
[:span {:class (stl/css :error)} (tr (:code error))]
|
||||||
|
|
||||||
|
|
|
@ -316,19 +316,25 @@
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
|
|
||||||
(when-not selected?
|
(when-not selected?
|
||||||
(when-not (kbd/shift? event)
|
(when-not (kbd/shift? event)
|
||||||
(st/emit! (dd/clear-selected-files)))
|
(st/emit! (dd/clear-selected-files)))
|
||||||
(st/emit! (dd/toggle-file-select file)))
|
(do
|
||||||
|
(st/emit! (dd/toggle-file-select file))))
|
||||||
|
|
||||||
|
(let [client-position
|
||||||
|
(dom/get-client-position event)
|
||||||
|
|
||||||
|
position
|
||||||
|
(if (and (nil? (:y client-position)) (nil? (:x client-position)))
|
||||||
|
(let [target-element (dom/get-target event)
|
||||||
|
points (dom/get-bounding-rect target-element)
|
||||||
|
y (:top points)
|
||||||
|
x (:left points)]
|
||||||
|
(gpt/point x y))
|
||||||
|
client-position)]
|
||||||
|
|
||||||
(let [client-position (dom/get-client-position event)
|
|
||||||
position (if (and (nil? (:y client-position)) (nil? (:x client-position)))
|
|
||||||
(let [target-element (dom/get-target event)
|
|
||||||
points (dom/get-bounding-rect target-element)
|
|
||||||
y (:top points)
|
|
||||||
x (:left points)]
|
|
||||||
(gpt/point x y))
|
|
||||||
client-position)]
|
|
||||||
(st/emit! (dd/show-file-menu-with-position file-id position)))))
|
(st/emit! (dd/show-file-menu-with-position file-id position)))))
|
||||||
|
|
||||||
on-context-menu
|
on-context-menu
|
||||||
|
@ -401,50 +407,53 @@
|
||||||
[:h3 (:name file)])
|
[:h3 (:name file)])
|
||||||
[:& grid-item-metadata {:modified-at (:modified-at file)}]]
|
[:& grid-item-metadata {:modified-at (:modified-at file)}]]
|
||||||
|
|
||||||
(when-not is-library-view
|
[:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))}
|
||||||
[:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))}
|
[:div
|
||||||
[:div
|
{:class (stl/css :project-th-icon :menu)
|
||||||
{:class (stl/css :project-th-icon :menu)
|
:tab-index "0"
|
||||||
:tab-index "0"
|
:role "button"
|
||||||
:ref menu-ref
|
:aria-label (tr "dashboard.options")
|
||||||
:id (str file-id "-action-menu")
|
:ref menu-ref
|
||||||
:on-click on-menu-click
|
:id (str file-id "-action-menu")
|
||||||
:on-key-down (fn [event]
|
:on-click on-menu-click
|
||||||
(when (kbd/enter? event)
|
:on-key-down (fn [event]
|
||||||
(dom/stop-propagation event)
|
(when (kbd/enter? event)
|
||||||
(on-menu-click event)))}
|
(dom/stop-propagation event)
|
||||||
menu-icon
|
(on-menu-click event)))}
|
||||||
(when (and selected? file-menu-open?)
|
menu-icon
|
||||||
|
(when (and selected? file-menu-open?)
|
||||||
;; When the menu is open we disable events in the dashboard. We need to force pointer events
|
;; When the menu is open we disable events in the dashboard. We need to force pointer events
|
||||||
;; so the menu can be handled
|
;; so the menu can be handled
|
||||||
[:div {:style {:pointer-events "all"}}
|
[:div {:style {:pointer-events "all"}}
|
||||||
[:> file-menu* {:files (vals selected-files)
|
[:> file-menu* {:files (vals selected-files)
|
||||||
:left (+ 24 (:x (:menu-pos dashboard-local)))
|
:left (+ 24 (:x (:menu-pos dashboard-local)))
|
||||||
:top (:y (:menu-pos dashboard-local))
|
:top (:y (:menu-pos dashboard-local))
|
||||||
:can-edit can-edit
|
:can-edit can-edit
|
||||||
:navigate true
|
:navigate true
|
||||||
:on-edit on-edit
|
:on-edit on-edit
|
||||||
:on-menu-close on-menu-close
|
:on-menu-close on-menu-close
|
||||||
:origin origin
|
:origin origin
|
||||||
:parent-id (dm/str file-id "-action-menu")}]])]])]]]))
|
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
|
||||||
|
|
||||||
(mf/defc grid
|
(mf/defc grid
|
||||||
{::mf/props :obj}
|
{::mf/props :obj}
|
||||||
[{:keys [files project origin limit create-fn can-edit selected-files]}]
|
[{:keys [files project origin limit create-fn can-edit selected-files]}]
|
||||||
(let [dragging? (mf/use-state false)
|
(let [dragging? (mf/use-state false)
|
||||||
project-id (:id project)
|
project-id (get project :id)
|
||||||
|
team-id (get project :team-id)
|
||||||
|
|
||||||
node-ref (mf/use-var nil)
|
node-ref (mf/use-var nil)
|
||||||
|
|
||||||
on-finish-import
|
on-finish-import
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
(mf/deps project-id team-id)
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit! (dpj/fetch-files project-id)
|
(st/emit! (dpj/fetch-files project-id)
|
||||||
(dtm/fetch-shared-files)
|
(dtm/fetch-shared-files team-id)
|
||||||
(dd/clear-selected-files))))
|
(dd/clear-selected-files))))
|
||||||
|
|
||||||
|
import-files
|
||||||
|
(use-import-file project-id on-finish-import)
|
||||||
import-files (use-import-file project-id on-finish-import)
|
|
||||||
|
|
||||||
on-drag-enter
|
on-drag-enter
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
|
|
@ -15,23 +15,38 @@
|
||||||
[app.main.ui.hooks :as hooks]
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
[okulary.core :as l]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(def ^:private ref:selected-files
|
||||||
|
(l/derived (fn [state]
|
||||||
|
(let [selected (get state :selected-files)
|
||||||
|
files (get state :shared-files)]
|
||||||
|
(refs/extract-selected-files files selected)))
|
||||||
|
st/state))
|
||||||
|
|
||||||
(mf/defc libraries-page*
|
(mf/defc libraries-page*
|
||||||
{::mf/props :obj}
|
{::mf/props :obj}
|
||||||
[{:keys [team default-project]}]
|
[{:keys [team default-project]}]
|
||||||
(let [files
|
(let [files
|
||||||
(mf/deref refs/shared-files)
|
(mf/deref refs/shared-files)
|
||||||
|
|
||||||
files
|
team-id
|
||||||
(mf/with-memo [files]
|
(get team :id)
|
||||||
(->> (vals files)
|
|
||||||
(sort-by :modified-at)
|
|
||||||
(reverse)))
|
|
||||||
|
|
||||||
can-edit
|
can-edit
|
||||||
(-> team :permissions :can-edit)
|
(-> team :permissions :can-edit)
|
||||||
|
|
||||||
|
files
|
||||||
|
(mf/with-memo [files team-id]
|
||||||
|
(->> (vals files)
|
||||||
|
(filter #(= team-id (:team-id %)))
|
||||||
|
(sort-by :modified-at)
|
||||||
|
(reverse)))
|
||||||
|
|
||||||
|
selected-files
|
||||||
|
(mf/deref ref:selected-files)
|
||||||
|
|
||||||
[rowref limit]
|
[rowref limit]
|
||||||
(hooks/use-dynamic-grid-item-width 350)]
|
(hooks/use-dynamic-grid-item-width 350)]
|
||||||
|
|
||||||
|
@ -41,16 +56,19 @@
|
||||||
(:name team))]
|
(:name team))]
|
||||||
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))
|
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))
|
||||||
|
|
||||||
(mf/with-effect [team]
|
(mf/with-effect [team-id]
|
||||||
(st/emit! (dtm/fetch-shared-files)
|
(st/emit! (dtm/fetch-shared-files team-id)
|
||||||
(dd/clear-selected-files)))
|
(dd/clear-selected-files)))
|
||||||
|
|
||||||
[:*
|
[:*
|
||||||
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
||||||
[:div#dashboard-libraries-title {:class (stl/css :dashboard-title)}
|
[:div#dashboard-libraries-title {:class (stl/css :dashboard-title)}
|
||||||
[:h1 (tr "dashboard.libraries-title")]]]
|
[:h1 (tr "dashboard.libraries-title")]]]
|
||||||
[:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared) :ref rowref}
|
|
||||||
|
[:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared)
|
||||||
|
:ref rowref}
|
||||||
[:& grid {:files files
|
[:& grid {:files files
|
||||||
|
:selected-files selected-files
|
||||||
:project default-project
|
:project default-project
|
||||||
:origin :libraries
|
:origin :libraries
|
||||||
:limit limit
|
:limit limit
|
||||||
|
|
|
@ -34,3 +34,8 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
|
@ -18,3 +18,7 @@
|
||||||
max-width: $s-960;
|
max-width: $s-960;
|
||||||
z-index: $z-index-modal;
|
z-index: $z-index-modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
[app.main.ui.releases.v2-2]
|
[app.main.ui.releases.v2-2]
|
||||||
[app.main.ui.releases.v2-3]
|
[app.main.ui.releases.v2-3]
|
||||||
[app.main.ui.releases.v2-4]
|
[app.main.ui.releases.v2-4]
|
||||||
|
[app.main.ui.releases.v2-5]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[app.util.timers :as tm]
|
[app.util.timers :as tm]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
@ -95,4 +96,4 @@
|
||||||
|
|
||||||
(defmethod rc/render-release-notes "0.0"
|
(defmethod rc/render-release-notes "0.0"
|
||||||
[params]
|
[params]
|
||||||
(rc/render-release-notes (assoc params :version "2.4")))
|
(rc/render-release-notes (assoc params :version "2.5")))
|
||||||
|
|
175
frontend/src/app/main/ui/releases/v2_5.cljs
Normal file
175
frontend/src/app/main/ui/releases/v2_5.cljs
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.main.ui.releases.v2-5
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.main.ui.releases.common :as c]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
;; TODO: Review all copies and alt text
|
||||||
|
(defmethod c/render-release-notes "2.5"
|
||||||
|
[{:keys [slide klass next finish navigate version]}]
|
||||||
|
(mf/html
|
||||||
|
(case slide
|
||||||
|
:start
|
||||||
|
[:div {:class (stl/css-case :modal-overlay true)}
|
||||||
|
[:div.animated {:class klass}
|
||||||
|
[:div {:class (stl/css :modal-container)}
|
||||||
|
[:img {:src "images/features/2.5-slide-0.png"
|
||||||
|
:class (stl/css :start-image)
|
||||||
|
:border "0"
|
||||||
|
:alt "A graphic illustration with Penpot style"}]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:div {:class (stl/css :modal-header)}
|
||||||
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
|
"What’s new in Penpot?"]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :version-tag)}
|
||||||
|
(dm/str "Version " version)]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :features-block)}
|
||||||
|
[:span {:class (stl/css :feature-title)}
|
||||||
|
"We’re thrilled to introduce Penpot 2.5"]
|
||||||
|
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"This release brings multi-step gradients, along with comment notifications, making it easier than ever to communicate with your team members. Now you also can easily copy/paste groups of styles between layers and share direct links to specific boards, among other new capabilities considered true gems for designers and team collaboration."]
|
||||||
|
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"But that’s not all—we’ve also tackled numerous bug fixes and optimizations that will improve performance when working with long texts."]
|
||||||
|
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"Let’s dive in!"]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :navigation)}
|
||||||
|
[:button {:class (stl/css :next-btn)
|
||||||
|
:on-click next} "Continue"]]]]]]
|
||||||
|
|
||||||
|
0
|
||||||
|
[:div {:class (stl/css-case :modal-overlay true)}
|
||||||
|
[:div.animated {:class klass}
|
||||||
|
[:div {:class (stl/css :modal-container)}
|
||||||
|
[:img {:src "images/features/2.5-gradients.gif"
|
||||||
|
:class (stl/css :start-image)
|
||||||
|
:border "0"
|
||||||
|
:alt "Multi-step gradients and more"}]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:div {:class (stl/css :modal-header)}
|
||||||
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
|
"Multi-step gradients and more"]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :feature)}
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"We’re so happy to bring you one of our most requested features—multi-step gradients! Now, you can create smooth, complex color transitions with multiple stops, giving you more creative options to customize your designs."]
|
||||||
|
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"And that’s not all. We’ve also added quick actions to flip and rotate gradients, plus now you can adjust the radius for radial gradients. More control, more flexibility, more fun."]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :navigation)}
|
||||||
|
[:& c/navigation-bullets
|
||||||
|
{:slide slide
|
||||||
|
:navigate navigate
|
||||||
|
:total 4}]
|
||||||
|
|
||||||
|
[:button {:on-click next
|
||||||
|
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||||
|
|
||||||
|
1
|
||||||
|
[:div {:class (stl/css-case :modal-overlay true)}
|
||||||
|
[:div.animated {:class klass}
|
||||||
|
[:div {:class (stl/css :modal-container)}
|
||||||
|
[:img {:src "images/features/2.5-mention.gif"
|
||||||
|
:class (stl/css :start-image)
|
||||||
|
:border "0"
|
||||||
|
:alt "Comment mentions and manage notifications"}]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:div {:class (stl/css :modal-header)}
|
||||||
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
|
"Comment mentions and manage notifications"]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :feature)}
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"No more lost comments! You can now tag teammates in comments, and they’ll get a notification so they never miss direct feedback. Plus, now you can filter mentions—just select 'Only your mentions' to quickly find discussions that matter to you."]
|
||||||
|
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"We’ve also added a new section in your profile where you can customize your notifications, choosing what to receive on your dashboard and via email. On top of that, comments got a UI refresh, making everything clearer and better organized. And this is just the first batch of improvements—expect even more comment-related upgrades in the next Penpot release."]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :navigation)}
|
||||||
|
[:& c/navigation-bullets
|
||||||
|
{:slide slide
|
||||||
|
:navigate navigate
|
||||||
|
:total 4}]
|
||||||
|
|
||||||
|
[:button {:on-click next
|
||||||
|
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||||
|
|
||||||
|
2
|
||||||
|
[:div {:class (stl/css-case :modal-overlay true)}
|
||||||
|
[:div.animated {:class klass}
|
||||||
|
[:div {:class (stl/css :modal-container)}
|
||||||
|
[:img {:src "images/features/2.5-copy.gif"
|
||||||
|
:class (stl/css :start-image)
|
||||||
|
:border "0"
|
||||||
|
:alt "Copy/paste styles, CSS, and text"}]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:div {:class (stl/css :modal-header)}
|
||||||
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
|
"Copy/paste styles, CSS, and text"]]
|
||||||
|
[:div {:class (stl/css :feature)}
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"Easily copy and apply styles across your designs with just a few clicks. With the new Copy/Paste options, you can quickly transfer fills, strokes, shadows, and other properties from one layer to another—or multiple layers at once. Reusing styles is no longer a repetitive task."]
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"And we’ve also added more copy options:"]
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"- 'Copy as CSS' to grab the code instantly."]
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"- 'Copy as text' if you just need the content."]
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"Less manual work for a faster workflow. We hope you find it as useful as we do."]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :navigation)}
|
||||||
|
[:& c/navigation-bullets
|
||||||
|
{:slide slide
|
||||||
|
:navigate navigate
|
||||||
|
:total 4}]
|
||||||
|
|
||||||
|
[:button {:on-click next
|
||||||
|
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||||
|
|
||||||
|
3
|
||||||
|
[:div {:class (stl/css-case :modal-overlay true)}
|
||||||
|
[:div.animated {:class klass}
|
||||||
|
[:div {:class (stl/css :modal-container)}
|
||||||
|
[:img {:src "images/features/2.5-link.gif"
|
||||||
|
:class (stl/css :start-image)
|
||||||
|
:border "0"
|
||||||
|
:alt "Links to specific boards"}]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:div {:class (stl/css :modal-header)}
|
||||||
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
|
"Links to specific boards"]]
|
||||||
|
[:div {:class (stl/css :feature)}
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"In a single Penpot file, it's common to have multiple individual screens or designs spread across different boards. Now, you can generate direct links to each board, making it easy to share them with team members or include direct links in documentation."]
|
||||||
|
[:p {:class (stl/css :feature-content)}
|
||||||
|
"No more navigating through the design workspace of a file to find a specific screen—just send a link and take your team straight to the intended board."]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :navigation)}
|
||||||
|
|
||||||
|
[:& c/navigation-bullets
|
||||||
|
{:slide slide
|
||||||
|
:navigate navigate
|
||||||
|
:total 4}]
|
||||||
|
|
||||||
|
[:button {:on-click finish
|
||||||
|
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
||||||
|
|
102
frontend/src/app/main/ui/releases/v2_5.scss
Normal file
102
frontend/src/app/main/ui/releases/v2_5.scss
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
@import "refactor/common-refactor.scss";
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
@extend .modal-overlay-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: $s-324 1fr;
|
||||||
|
height: $s-500;
|
||||||
|
width: $s-888;
|
||||||
|
border-radius: $br-8;
|
||||||
|
background-color: var(--modal-background-color);
|
||||||
|
border: $s-2 solid var(--modal-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-image {
|
||||||
|
width: $s-324;
|
||||||
|
border-radius: $br-8 0 0 $br-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: $s-40;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr $s-32;
|
||||||
|
gap: $s-24;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--button-primary-background-color-rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: grid;
|
||||||
|
gap: $s-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-tag {
|
||||||
|
@include flexCenter;
|
||||||
|
@include headlineSmallTypography;
|
||||||
|
height: $s-32;
|
||||||
|
width: $s-96;
|
||||||
|
background-color: var(--communication-tag-background-color);
|
||||||
|
color: var(--communication-tag-foreground-color);
|
||||||
|
border-radius: $br-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
@include headlineLargeTypography;
|
||||||
|
color: var(--modal-title-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $s-16;
|
||||||
|
width: $s-440;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $s-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
@include bodyLargeTypography;
|
||||||
|
color: var(--modal-title-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-content {
|
||||||
|
@include bodyMediumTypography;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--modal-text-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list {
|
||||||
|
@include bodyMediumTypography;
|
||||||
|
color: var(--modal-text-foreground-color);
|
||||||
|
list-style: disc;
|
||||||
|
display: grid;
|
||||||
|
gap: $s-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "bullets button";
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn {
|
||||||
|
@extend .button-primary;
|
||||||
|
width: $s-100;
|
||||||
|
justify-self: flex-end;
|
||||||
|
grid-area: button;
|
||||||
|
}
|
|
@ -59,7 +59,7 @@
|
||||||
[:map {:title "EmailChangeForm"}
|
[:map {:title "EmailChangeForm"}
|
||||||
[:email-1 ::sm/email]
|
[:email-1 ::sm/email]
|
||||||
[:email-2 ::sm/email]]
|
[:email-2 ::sm/email]]
|
||||||
[:fn {:error/code "errors.invalid-email-confirmation"
|
[:fn {:error/fn #(tr "errors.invalid-email-confirmation")
|
||||||
:error/field :email-2}
|
:error/field :email-2}
|
||||||
(fn [data]
|
(fn [data]
|
||||||
(let [email-1 (:email-1 data)
|
(let [email-1 (:email-1 data)
|
||||||
|
|
|
@ -21,10 +21,10 @@
|
||||||
(case (:code (ex-data error))
|
(case (:code (ex-data error))
|
||||||
:old-password-not-match
|
:old-password-not-match
|
||||||
(swap! form assoc-in [:errors :password-old]
|
(swap! form assoc-in [:errors :password-old]
|
||||||
{:code "errors.wrong-old-password"})
|
{:message (tr "errors.wrong-old-password")})
|
||||||
:email-as-password
|
:email-as-password
|
||||||
(swap! form assoc-in [:errors :password-1]
|
(swap! form assoc-in [:errors :password-1]
|
||||||
{:code "errors.email-as-password"})
|
{:message (tr "errors.email-as-password")})
|
||||||
|
|
||||||
(let [msg (tr "generic.error")]
|
(let [msg (tr "generic.error")]
|
||||||
(st/emit! (ntf/error msg)))))
|
(st/emit! (ntf/error msg)))))
|
||||||
|
|
|
@ -142,6 +142,8 @@
|
||||||
(let [id (:id library)
|
(let [id (:id library)
|
||||||
importing? (deref importing)
|
importing? (deref importing)
|
||||||
|
|
||||||
|
team-id (mf/use-ctx ctx/current-team-id)
|
||||||
|
|
||||||
on-error
|
on-error
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [_]
|
(fn [_]
|
||||||
|
@ -150,11 +152,13 @@
|
||||||
|
|
||||||
on-success
|
on-success
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
(mf/deps team-id)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(st/emit! (dtm/fetch-shared-files))))
|
(st/emit! (dtm/fetch-shared-files team-id))))
|
||||||
|
|
||||||
import-library
|
import-library
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
(mf/deps on-success on-error)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(reset! importing id)
|
(reset! importing id)
|
||||||
(st/emit! (dd/clone-template
|
(st/emit! (dd/clone-template
|
||||||
|
@ -565,6 +569,7 @@
|
||||||
file (deref refs/file)
|
file (deref refs/file)
|
||||||
|
|
||||||
file-id (:id file)
|
file-id (:id file)
|
||||||
|
team-id (:team-id file)
|
||||||
shared? (:is-shared file)
|
shared? (:is-shared file)
|
||||||
|
|
||||||
linked-libraries
|
linked-libraries
|
||||||
|
@ -611,8 +616,8 @@
|
||||||
:id "updates"
|
:id "updates"
|
||||||
:content updates-tab}]]
|
:content updates-tab}]]
|
||||||
|
|
||||||
(mf/with-effect []
|
(mf/with-effect [team-id]
|
||||||
(st/emit! (dtm/fetch-shared-files)))
|
(st/emit! (dtm/fetch-shared-files team-id)))
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-overlay)
|
[:div {:class (stl/css :modal-overlay)
|
||||||
:on-click close-dialog-outside
|
:on-click close-dialog-outside
|
||||||
|
|
|
@ -120,16 +120,17 @@
|
||||||
[:& layer-name {:ref name-ref
|
[:& layer-name {:ref name-ref
|
||||||
:shape-id id
|
:shape-id id
|
||||||
:shape-name name
|
:shape-name name
|
||||||
:shape-touched? touched?
|
:is-shape-touched touched?
|
||||||
:disabled-double-click read-only?
|
:disabled-double-click read-only?
|
||||||
:on-start-edit on-disable-drag
|
:on-start-edit on-disable-drag
|
||||||
:on-stop-edit on-enable-drag
|
:on-stop-edit on-enable-drag
|
||||||
:depth depth
|
:depth depth
|
||||||
|
:is-blocked blocked?
|
||||||
:parent-size parent-size
|
:parent-size parent-size
|
||||||
:selected? selected?
|
:is-selected selected?
|
||||||
:type-comp component-tree?
|
:type-comp component-tree?
|
||||||
:type-frame (cfh/frame-shape? item)
|
:type-frame (cfh/frame-shape? item)
|
||||||
:hidden? hidden?}]
|
:is-hidden hidden?}]
|
||||||
|
|
||||||
(when (not read-only?)
|
(when (not read-only?)
|
||||||
[:div {:class (stl/css-case
|
[:div {:class (stl/css-case
|
||||||
|
|
|
@ -27,9 +27,9 @@
|
||||||
(mf/defc layer-name
|
(mf/defc layer-name
|
||||||
{::mf/wrap-props false
|
{::mf/wrap-props false
|
||||||
::mf/forward-ref true}
|
::mf/forward-ref true}
|
||||||
[{:keys [shape-id shape-name shape-touched? disabled-double-click
|
[{:keys [shape-id shape-name is-shape-touched disabled-double-click
|
||||||
on-start-edit on-stop-edit depth parent-size selected?
|
on-start-edit on-stop-edit depth parent-size is-selected
|
||||||
type-comp type-frame hidden?]} external-ref]
|
type-comp type-frame is-hidden is-blocked]} external-ref]
|
||||||
(let [edition* (mf/use-state false)
|
(let [edition* (mf/use-state false)
|
||||||
edition? (deref edition*)
|
edition? (deref edition*)
|
||||||
|
|
||||||
|
@ -42,9 +42,10 @@
|
||||||
|
|
||||||
start-edit
|
start-edit
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps disabled-double-click on-start-edit shape-id)
|
(mf/deps disabled-double-click on-start-edit shape-id is-blocked)
|
||||||
(fn []
|
(fn []
|
||||||
(when (not disabled-double-click)
|
(when (and (not is-blocked)
|
||||||
|
(not disabled-double-click))
|
||||||
(on-start-edit)
|
(on-start-edit)
|
||||||
(reset! edition* true)
|
(reset! edition* true)
|
||||||
(st/emit! (dw/start-rename-shape shape-id)))))
|
(st/emit! (dw/start-rename-shape shape-id)))))
|
||||||
|
@ -102,8 +103,8 @@
|
||||||
{:class (stl/css-case
|
{:class (stl/css-case
|
||||||
:element-name true
|
:element-name true
|
||||||
:left-ellipsis has-path?
|
:left-ellipsis has-path?
|
||||||
:selected selected?
|
:selected is-selected
|
||||||
:hidden hidden?
|
:hidden is-hidden
|
||||||
:type-comp type-comp
|
:type-comp type-comp
|
||||||
:type-frame type-frame)
|
:type-frame type-frame)
|
||||||
:style {"--depth" depth "--parent-size" parent-size}
|
:style {"--depth" depth "--parent-size" parent-size}
|
||||||
|
@ -112,5 +113,5 @@
|
||||||
(if (dbg/enabled? :show-ids)
|
(if (dbg/enabled? :show-ids)
|
||||||
(str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
|
(str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
|
||||||
(d/nilv shape-name ""))]
|
(d/nilv shape-name ""))]
|
||||||
(when (and (dbg/enabled? :show-touched) ^boolean shape-touched?)
|
(when (and (dbg/enabled? :show-touched) ^boolean is-shape-touched)
|
||||||
[:span {:class (stl/css :element-name-touched)} "*"])])))
|
[:span {:class (stl/css :element-name-touched)} "*"])])))
|
||||||
|
|
|
@ -96,6 +96,8 @@
|
||||||
"var(--color-accent-tertiary)")
|
"var(--color-accent-tertiary)")
|
||||||
"#8f9da3") ;; TODO: Set this color on the DS
|
"#8f9da3") ;; TODO: Set this color on the DS
|
||||||
|
|
||||||
|
blocked? (:blocked frame)
|
||||||
|
|
||||||
on-pointer-down
|
on-pointer-down
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps (:id frame) on-frame-select workspace-read-only?)
|
(mf/deps (:id frame) on-frame-select workspace-read-only?)
|
||||||
|
@ -145,9 +147,10 @@
|
||||||
|
|
||||||
start-edit
|
start-edit
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps frame-id edition?)
|
(mf/deps frame-id edition? blocked? workspace-read-only?)
|
||||||
(fn []
|
(fn []
|
||||||
(when-not (-> @st/state :workspace-global :read-only?)
|
(when (and (not blocked?)
|
||||||
|
(not workspace-read-only?))
|
||||||
(if (not edition?)
|
(if (not edition?)
|
||||||
(reset! edition* true)
|
(reset! edition* true)
|
||||||
(st/emit! (dw/start-rename-shape frame-id))))))
|
(st/emit! (dw/start-rename-shape frame-id))))))
|
||||||
|
|
|
@ -87,4 +87,6 @@
|
||||||
padding-left: $s-6;
|
padding-left: $s-6;
|
||||||
border: $s-1 solid var(--input-border-color-focus);
|
border: $s-1 solid var(--input-border-color-focus);
|
||||||
color: var(--layer-row-foreground-color);
|
color: var(--layer-row-foreground-color);
|
||||||
|
width: 100%;
|
||||||
|
max-width: initial;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,43 +10,78 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[malli.core :as m]
|
[malli.core :as m]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
;; --- Handlers Helpers
|
;; --- Handlers Helpers
|
||||||
|
|
||||||
|
(defn- translate-code
|
||||||
|
[code]
|
||||||
|
(if (vector? code)
|
||||||
|
(tr (nth code 0) (i18n/c (nth code 1)))
|
||||||
|
(tr code)))
|
||||||
|
|
||||||
|
(defn- handle-error-fn
|
||||||
|
[props problem]
|
||||||
|
(let [v-fn (:error/fn props)
|
||||||
|
result (v-fn problem)]
|
||||||
|
(if (string? result)
|
||||||
|
{:message result}
|
||||||
|
{:message (or (some-> (get result :code)
|
||||||
|
(translate-code))
|
||||||
|
(get result :message)
|
||||||
|
(tr "errors.invalid-data"))})))
|
||||||
|
|
||||||
|
(defn- handle-error-message
|
||||||
|
[props]
|
||||||
|
{:message (get props :error/message)})
|
||||||
|
|
||||||
|
(defn- handle-error-code
|
||||||
|
[props]
|
||||||
|
(let [code (get props :error/code)]
|
||||||
|
{:message (translate-code code)}))
|
||||||
|
|
||||||
(defn- interpret-schema-problem
|
(defn- interpret-schema-problem
|
||||||
[acc {:keys [schema in value] :as problem}]
|
[acc {:keys [schema in value type] :as problem}]
|
||||||
(let [props (merge (m/type-properties schema)
|
(let [props (m/properties schema)
|
||||||
(m/properties schema))
|
tprops (m/type-properties schema)
|
||||||
field (or (first in) (:error/field props))]
|
field (or (first in)
|
||||||
|
(:error/field props))]
|
||||||
|
|
||||||
(if (contains? acc field)
|
(if (contains? acc field)
|
||||||
acc
|
acc
|
||||||
(cond
|
(cond
|
||||||
(nil? value)
|
(nil? field)
|
||||||
(assoc acc field {:code "errors.field-missing"})
|
acc
|
||||||
|
|
||||||
(contains? props :error/code)
|
(or (= type :malli.core/missing-key)
|
||||||
(assoc acc field {:code (:error/code props)})
|
(nil? value))
|
||||||
|
(assoc acc field {:message (tr "errors.field-missing")})
|
||||||
|
|
||||||
|
;; --- CHECK on schema props
|
||||||
|
(contains? props :error/fn)
|
||||||
|
(assoc acc field (handle-error-fn props problem))
|
||||||
|
|
||||||
(contains? props :error/message)
|
(contains? props :error/message)
|
||||||
(assoc acc field {:code (:error/message props)})
|
(assoc acc field (handle-error-message props))
|
||||||
|
|
||||||
(contains? props :error/fn)
|
(contains? props :error/code)
|
||||||
(let [v-fn (:error/fn props)
|
(assoc acc field (handle-error-code props))
|
||||||
code (v-fn problem)]
|
|
||||||
(assoc acc field {:code code}))
|
|
||||||
|
|
||||||
(contains? props :error/validators)
|
;; --- CHECK on type props
|
||||||
(let [validators (:error/validators props)
|
(contains? tprops :error/fn)
|
||||||
props (reduce #(%2 %1 value) props validators)]
|
(assoc acc field (handle-error-fn tprops problem))
|
||||||
(assoc acc field {:code (d/nilv (:error/code props) "errors.invalid-data")}))
|
|
||||||
|
(contains? tprops :error/message)
|
||||||
|
(assoc acc field (handle-error-message tprops))
|
||||||
|
|
||||||
|
(contains? tprops :error/code)
|
||||||
|
(assoc acc field (handle-error-code tprops))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(assoc acc field {:code "errors.invalid-data"})))))
|
(assoc acc field {:message (tr "errors.invalid-data")})))))
|
||||||
|
|
||||||
(defn- use-rerender-fn
|
(defn- use-rerender-fn
|
||||||
[]
|
[]
|
||||||
|
@ -177,21 +212,6 @@
|
||||||
|
|
||||||
;; --- Helper Components
|
;; --- Helper Components
|
||||||
|
|
||||||
(mf/defc field-error
|
|
||||||
[{:keys [form field type]
|
|
||||||
:as props}]
|
|
||||||
(let [{:keys [message] :as error} (dm/get-in form [:errors field])
|
|
||||||
touched? (dm/get-in form [:touched field])
|
|
||||||
show? (and touched? error message
|
|
||||||
(cond
|
|
||||||
(nil? type) true
|
|
||||||
(keyword? type) (= (:type error) type)
|
|
||||||
(ifn? type) (type (:type error))
|
|
||||||
:else false))]
|
|
||||||
(when show?
|
|
||||||
[:ul
|
|
||||||
[:li {:key (:code error)} (tr (:message error))]])))
|
|
||||||
|
|
||||||
(defn error-class
|
(defn error-class
|
||||||
[form field]
|
[form field]
|
||||||
(when (and (dm/get-in form [:errors field])
|
(when (and (dm/get-in form [:errors field])
|
||||||
|
|
|
@ -1234,6 +1234,18 @@ msgstr "Something wrong has happened."
|
||||||
msgid "errors.invalid-color"
|
msgid "errors.invalid-color"
|
||||||
msgstr "Invalid color"
|
msgstr "Invalid color"
|
||||||
|
|
||||||
|
#: src/app/util/forms.cljs
|
||||||
|
msgid "errors.invalid-data"
|
||||||
|
msgstr "Invalid data"
|
||||||
|
|
||||||
|
#: src/app/util/forms.cljs
|
||||||
|
msgid "errors.field-missing"
|
||||||
|
msgstr "Empty field"
|
||||||
|
|
||||||
|
#: src/app/util/forms.cljs
|
||||||
|
msgid "errors.invalid-text"
|
||||||
|
msgstr "Invalid text"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
|
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
|
||||||
#, unused
|
#, unused
|
||||||
msgid "errors.invalid-email"
|
msgid "errors.invalid-email"
|
||||||
|
|
|
@ -1231,6 +1231,18 @@ msgstr "Ha ocurrido algún error."
|
||||||
msgid "errors.invalid-color"
|
msgid "errors.invalid-color"
|
||||||
msgstr "Color no válido"
|
msgstr "Color no válido"
|
||||||
|
|
||||||
|
#: src/app/util/forms.cljs
|
||||||
|
msgid "errors.invalid-data"
|
||||||
|
msgstr "Datos no válidos"
|
||||||
|
|
||||||
|
#: src/app/util/forms.cljs
|
||||||
|
msgid "errors.field-missing"
|
||||||
|
msgstr "Campo vacio"
|
||||||
|
|
||||||
|
#: src/app/util/forms.cljs
|
||||||
|
msgid "errors.invalid-text"
|
||||||
|
msgstr "Texto no válido"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
|
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
|
||||||
#, unused
|
#, unused
|
||||||
msgid "errors.invalid-email"
|
msgid "errors.invalid-email"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue