From e96e6c1fa2127206e3cef4a2a8cd6d01217566ba Mon Sep 17 00:00:00 2001 From: caichi <54824604+caichi-t@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:01:53 +0900 Subject: [PATCH 1/9] fix(web): max size of popover on content page (#1250) fix: popover bug --- .../molecules/Content/RenderField/index.tsx | 7 ++++++- web/src/index.css | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/web/src/components/molecules/Content/RenderField/index.tsx b/web/src/components/molecules/Content/RenderField/index.tsx index f61a380321..00218b58a9 100644 --- a/web/src/components/molecules/Content/RenderField/index.tsx +++ b/web/src/components/molecules/Content/RenderField/index.tsx @@ -80,7 +80,12 @@ export const renderField = ( ); return ( - + {items.length > 1 && x{items.length}} diff --git a/web/src/index.css b/web/src/index.css index 341aba2298..922ec7a1e3 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -47,3 +47,19 @@ code { color: inherit; cursor: inherit; } + +.contentPopover { + .ant-popover-title, + .ant-popover-inner-content { + max-width: 20vw; + } + .ant-popover-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .ant-popover-inner-content { + max-height: 20vh; + overflow: auto; + } +} From 477da7f78a83bdd521d457d004f85f602c167177 Mon Sep 17 00:00:00 2001 From: caichi <54824604+caichi-t@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:00:59 +0900 Subject: [PATCH 2/9] fix(web): correct validation behavior when handling invalid values (#1225) * fix: text on field delete modal * fix: validate defaut value * fix: how to show invalid field * fix: error handling on geometry field * fix: error handling when creating new item * fix: e2e tests * rename * use unkown * forwardRef * fix integer validate logic * fix any type * fix integer validate logic --------- Co-authored-by: Kazuma Tsuchiya <> --- web/e2e/project/item/fields/group.spec.ts | 11 +++--- web/e2e/project/item/fields/int.spec.ts | 9 ++--- web/e2e/project/item/fields/markdown.spec.ts | 26 +++++-------- web/e2e/project/item/metadata/text.spec.ts | 3 +- web/e2e/project/request.spec.ts | 4 +- web/package.json | 1 + web/src/components/atoms/Input/index.tsx | 29 +++++++++++++- .../components/atoms/InputNumber/index.tsx | 19 ++++++++- web/src/components/atoms/Markdown/index.tsx | 19 ++++++--- web/src/components/atoms/Password/index.tsx | 3 ++ web/src/components/atoms/Search/index.tsx | 3 +- web/src/components/atoms/TextArea/index.tsx | 29 +++++++++++++- .../molecules/Asset/AssetListTable/index.tsx | 4 +- .../Common/CommentsPanel/Comment.tsx | 4 +- .../molecules/Common/CommentsPanel/Editor.tsx | 4 +- .../Common/Form/GeometryItem/index.tsx | 28 ++++--------- .../components/molecules/Common/Form/utils.ts | 8 ++++ .../Common/LinkAssetModal/LinkAssetModal.tsx | 4 +- .../Common/MultiValueField/index.tsx | 2 +- .../fields/FieldComponents/DefaultField.tsx | 29 ++++++++++---- .../fields/FieldComponents/GeometryField.tsx | 16 +++----- .../fields/FieldComponents/IntegerField.tsx | 36 +++++++++++++---- .../fields/FieldComponents/MarkdownField.tsx | 28 ++++++++++--- .../fields/FieldComponents/TextareaField.tsx | 29 ++++++++++---- .../molecules/Content/Form/index.tsx | 35 ++++++++++------- .../molecules/Content/LinkItemModal/index.tsx | 4 +- .../LinkItemRequestModal.tsx | 4 +- .../molecules/Content/Table/index.tsx | 5 ++- .../Integration/IntegrationTable/index.tsx | 4 +- .../molecules/Member/MemberAddModal/index.tsx | 3 +- .../molecules/Member/MemberTable/index.tsx | 4 +- .../MyIntegrations/Settings/Form.tsx | 3 +- .../molecules/Request/Details/Comment.tsx | 4 +- .../molecules/Request/Details/Editor.tsx | 4 +- .../molecules/Request/Details/ItemForm.tsx | 4 ++ .../molecules/Request/Table/index.tsx | 4 +- .../FieldCreationModalWithSteps/index.tsx | 3 -- .../GeometryField/index.tsx | 36 +++++++++++++++-- .../FieldDefaultInputs/IntegerField/index.tsx | 39 ++++++++++++++++--- .../FieldDefaultInputs/Markdown/index.tsx | 32 +++++++++++++-- .../FieldDefaultInputs/TextArea/index.tsx | 31 ++++++++++++--- .../FieldDefaultInputs/TextField/index.tsx | 32 +++++++++++++-- .../FieldModal/FieldDefaultInputs/index.tsx | 18 ++++++--- .../FieldValidationInputs/index.tsx | 34 ++++++++-------- .../molecules/Schema/FieldModal/hooks.ts | 6 +++ .../molecules/Schema/FieldModal/index.tsx | 8 +++- .../molecules/Schema/ModelFieldList.tsx | 12 +++--- .../molecules/Workspace/WorkspaceHeader.tsx | 4 +- web/src/i18n/translations/en.yml | 2 +- web/src/i18n/translations/ja.yml | 2 +- web/yarn.lock | 5 +++ 51 files changed, 485 insertions(+), 205 deletions(-) create mode 100644 web/src/components/atoms/Password/index.tsx diff --git a/web/e2e/project/item/fields/group.spec.ts b/web/e2e/project/item/fields/group.spec.ts index a0358c8f66..db71283ff0 100644 --- a/web/e2e/project/item/fields/group.spec.ts +++ b/web/e2e/project/item/fields/group.spec.ts @@ -94,8 +94,7 @@ test("Group field creating and updating has succeeded", async ({ page }) => { await page.getByRole("button", { name: "plus New" }).click(); await page.getByLabel("Set default value").click(); await page.getByLabel("Set default value").fill("text12"); - await page.getByRole("button", { name: "OK" }).click(); - await closeNotification(page, false); + await expect(page.getByRole("button", { name: "OK" })).toBeDisabled(); await page.getByLabel("Set default value").click(); await page.getByLabel("Set default value").fill("text1"); await page.getByRole("button", { name: "OK" }).click(); @@ -220,12 +219,12 @@ test("Group field editing has succeeded", async ({ page }) => { await expect(page.getByRole("main")).toContainText("new group1 (2)"); await page .locator("div") - .filter({ hasText: /^0 \/ 500text1 description$/ }) + .filter({ hasText: /^0text1 description$/ }) .getByLabel("text1") .click(); await page .locator("div") - .filter({ hasText: /^0 \/ 500text1 description$/ }) + .filter({ hasText: /^0text1 description$/ }) .getByLabel("text1") .fill("text1-2"); await page.getByRole("button", { name: "Save" }).click(); @@ -235,13 +234,13 @@ test("Group field editing has succeeded", async ({ page }) => { await expect( page .locator("div") - .filter({ hasText: /^5 \/ 500text1 description$/ }) + .filter({ hasText: /^5text1 description$/ }) .getByLabel("text1"), ).toHaveValue("text1"); await expect( page .locator("div") - .filter({ hasText: /^7 \/ 500text1 description$/ }) + .filter({ hasText: /^7text1 description$/ }) .getByLabel("text1"), ).toHaveValue("text1-2"); await page.getByLabel("Back").click(); diff --git a/web/e2e/project/item/fields/int.spec.ts b/web/e2e/project/item/fields/int.spec.ts index 42246a5a75..1b46ac512a 100644 --- a/web/e2e/project/item/fields/int.spec.ts +++ b/web/e2e/project/item/fields/int.spec.ts @@ -85,8 +85,7 @@ test("Int field editing has succeeded", async ({ page }) => { await page.getByLabel("Set minimum value").fill("10"); await page.getByLabel("Set maximum value").click(); await page.getByLabel("Set maximum value").fill("2"); - await page.getByRole("button", { name: "OK" }).click(); - await closeNotification(page, false); + await expect(page.getByRole("button", { name: "OK" })).toBeDisabled(); await page.getByLabel("Set minimum value").click(); await page.getByLabel("Set minimum value").fill("2"); await page.getByLabel("Set maximum value").click(); @@ -96,12 +95,10 @@ test("Int field editing has succeeded", async ({ page }) => { await page.getByRole("tab", { name: "Default value" }).click(); await expect(page.getByLabel("Set default value")).toBeVisible(); await expect(page.getByLabel("Set default value")).toHaveValue("1"); - await page.getByRole("button", { name: "OK" }).click(); - await closeNotification(page, false); + await expect(page.getByRole("button", { name: "OK" })).toBeDisabled(); await page.getByLabel("Set default value").click(); await page.getByLabel("Set default value").fill("11"); - await page.getByRole("button", { name: "OK" }).click(); - await closeNotification(page, false); + await expect(page.getByRole("button", { name: "OK" })).toBeDisabled(); await page.getByLabel("Set default value").click(); await page.getByLabel("Set default value").fill("2"); await page.getByRole("button", { name: "plus New" }).click(); diff --git a/web/e2e/project/item/fields/markdown.spec.ts b/web/e2e/project/item/fields/markdown.spec.ts index 9434804394..2e14fe77ae 100644 --- a/web/e2e/project/item/fields/markdown.spec.ts +++ b/web/e2e/project/item/fields/markdown.spec.ts @@ -73,14 +73,12 @@ test("Markdown field editing has succeeded", async ({ page }) => { await page.getByRole("tab", { name: "Default value" }).click(); await expect(page.getByLabel("Set default value")).toHaveValue("text1 default value"); await page.getByRole("button", { name: "plus New" }).click(); - await page.locator("div:nth-child(2) > .css-1ago99h > .css-7olgor").click(); + await page.locator("div:nth-child(2) > .css-1ago99h").click(); await page.getByRole("textbox").fill("text2"); - await page.locator("div:nth-child(1) > .css-1ago99h > .css-7olgor").click(); + await page.locator("div:nth-child(1) > .css-1ago99h").click(); await page.getByRole("textbox").fill("text1"); await page.getByRole("button", { name: "arrow-down" }).first().click(); - await expect(page.locator("div:nth-child(2) > .css-1ago99h > .css-7olgor")).toContainText( - "text1", - ); + await expect(page.locator("div:nth-child(2) > .css-1ago99h")).toContainText("text1"); await page.getByRole("button", { name: "OK" }).click(); await closeNotification(page); await expect(page.getByText("new text1 *#new-text1(unique)")).toBeVisible(); @@ -92,12 +90,8 @@ test("Markdown field editing has succeeded", async ({ page }) => { await expect(page.getByText("new text1(unique)")).toBeVisible(); await page.getByText("new text1 description").click(); await expect(page.getByText("new text1 description")).toBeVisible(); - await expect(page.locator("div:nth-child(1) > .css-1ago99h > .css-7olgor")).toContainText( - "text2", - ); - await expect(page.locator("div:nth-child(2) > .css-1ago99h > .css-7olgor")).toContainText( - "text1", - ); + await expect(page.locator("div:nth-child(1) > .css-1ago99h")).toContainText("text2"); + await expect(page.locator("div:nth-child(2) > .css-1ago99h")).toContainText("text1"); await page.getByRole("button", { name: "Save" }).click(); await closeNotification(page); await page.getByLabel("Back").click(); @@ -110,17 +104,15 @@ test("Markdown field editing has succeeded", async ({ page }) => { await page.getByRole("button", { name: "plus New" }).click(); await page.getByRole("button", { name: "Save" }).click(); await closeNotification(page, false); - await page.locator("div:nth-child(1) > .css-1ago99h > .css-7olgor").click(); + await page.locator("div:nth-child(1) > .css-1ago99h").click(); await page.getByRole("textbox").fill("text"); await page.getByRole("button", { name: "plus New" }).click(); await page.getByRole("button", { name: "plus New" }).click(); - await page.locator("div:nth-child(2) > .css-1ago99h > .css-7olgor").click(); + await page.locator("div:nth-child(2) > .css-1ago99h").click(); await page.getByRole("textbox").fill("text2"); await page.getByRole("button", { name: "arrow-up" }).nth(1).click(); - await expect(page.locator("div:nth-child(1) > .css-1ago99h > .css-7olgor")).toContainText( - "text2", - ); - await expect(page.locator("div:nth-child(2) > .css-1ago99h > .css-7olgor")).toContainText("text"); + await expect(page.locator("div:nth-child(1) > .css-1ago99h")).toContainText("text2"); + await expect(page.locator("div:nth-child(2) > .css-1ago99h")).toContainText("text"); await page.getByRole("button", { name: "Save" }).click(); await closeNotification(page); await page.getByLabel("Back").click(); diff --git a/web/e2e/project/item/metadata/text.spec.ts b/web/e2e/project/item/metadata/text.spec.ts index d3c95051d8..cbd721832c 100644 --- a/web/e2e/project/item/metadata/text.spec.ts +++ b/web/e2e/project/item/metadata/text.spec.ts @@ -114,8 +114,7 @@ test("Text metadata editing has succeeded", async ({ page }) => { await page.getByRole("button", { name: "plus New" }).click(); await page.locator("#defaultValue").nth(1).click(); await page.locator("#defaultValue").nth(1).fill("text2"); - await page.getByRole("button", { name: "OK" }).click(); - await closeNotification(page, false); + await expect(page.getByRole("button", { name: "OK" })).toBeDisabled(); await page.locator("#defaultValue").nth(0).click(); await page.locator("#defaultValue").nth(0).fill("text1"); await page.getByRole("button", { name: "arrow-down" }).first().click(); diff --git a/web/e2e/project/request.spec.ts b/web/e2e/project/request.spec.ts index b4ec4c6a84..d19c84bd69 100644 --- a/web/e2e/project/request.spec.ts +++ b/web/e2e/project/request.spec.ts @@ -133,8 +133,8 @@ test("Creating a new request and adding to request has succeeded", async ({ page await closeNotification(page); await page.getByText("Request", { exact: true }).click(); await page.getByLabel("edit").locator("svg").click(); - await expect(page.getByRole("button", { name: "right e2e model name" }).first()).toBeVisible(); - await expect(page.getByRole("button", { name: "right e2e model name" }).nth(1)).toBeVisible(); + await expect(page.getByRole("button", { name: "collapsed e2e model name" }).nth(0)).toBeVisible(); + await expect(page.getByRole("button", { name: "collapsed e2e model name" }).nth(1)).toBeVisible(); }); test("Navigating from request to item has succeeded", async ({ page }) => { diff --git a/web/package.json b/web/package.json index 2cc00f98a2..67a6728dfb 100644 --- a/web/package.json +++ b/web/package.json @@ -136,6 +136,7 @@ "react-svg": "16.1.34", "remark-gfm": "4.0.0", "resium": "1.18.2", + "runes2": "1.1.4", "ulid": "2.3.0" } } diff --git a/web/src/components/atoms/Input/index.tsx b/web/src/components/atoms/Input/index.tsx index 91c7aae7bc..e86df62641 100644 --- a/web/src/components/atoms/Input/index.tsx +++ b/web/src/components/atoms/Input/index.tsx @@ -1,6 +1,33 @@ -import { Input, InputProps } from "antd"; +import { Input as AntDInput, InputProps, InputRef } from "antd"; +import { forwardRef, useMemo } from "react"; +import { runes } from "runes2"; export type { SearchProps } from "antd/lib/input"; +type Props = { + value?: string; +} & InputProps; + +const Input = forwardRef(({ value, maxLength, ...props }, ref) => { + const status = useMemo(() => { + if (maxLength && value && runes(value).length > maxLength) { + return "error"; + } + }, [maxLength, value]); + + return ( + runes(txt).length, + }} + value={value} + ref={ref} + status={status} + {...props} + /> + ); +}); + export default Input; export type { InputProps }; diff --git a/web/src/components/atoms/InputNumber/index.tsx b/web/src/components/atoms/InputNumber/index.tsx index 2adb2e1fe3..f276249a29 100644 --- a/web/src/components/atoms/InputNumber/index.tsx +++ b/web/src/components/atoms/InputNumber/index.tsx @@ -1,3 +1,20 @@ -import { InputNumber } from "antd"; +import { InputNumber as AntDInputNumber, InputNumberProps } from "antd"; +import { useMemo } from "react"; + +const InputNumber: ( + props: React.PropsWithChildren> & React.RefAttributes, +) => React.ReactElement = ({ value, ...props }) => { + const status = useMemo(() => { + if (value) { + if (props.max && Number(value) > Number(props.max)) { + return "error"; + } else if (props.min && Number(value) < Number(props.min)) { + return "error"; + } + } + }, [props.max, props.min, value]); + + return ; +}; export default InputNumber; diff --git a/web/src/components/atoms/Markdown/index.tsx b/web/src/components/atoms/Markdown/index.tsx index cd48c45057..f2ba814861 100644 --- a/web/src/components/atoms/Markdown/index.tsx +++ b/web/src/components/atoms/Markdown/index.tsx @@ -1,6 +1,7 @@ import styled from "@emotion/styled"; -import { useRef, useState, FocusEvent, useCallback } from "react"; +import { useRef, useState, FocusEvent, useCallback, useMemo } from "react"; import ReactMarkdown from "react-markdown"; +import { runes } from "runes2"; import TextArea, { TextAreaProps } from "@reearth-cms/components/atoms/TextArea"; @@ -9,9 +10,13 @@ type Props = { onChange?: (value: string) => void; } & TextAreaProps; -const MarkdownInput: React.FC = ({ value = "", onChange, ...props }) => { +const MarkdownInput: React.FC = ({ value, onChange, ...props }) => { const [showMD, setShowMD] = useState(true); const textareaRef = useRef(null); + const isError = useMemo( + () => (props.maxLength && value ? runes(value).length > props.maxLength : false), + [props.maxLength, value], + ); const handleBlur = useCallback((event: FocusEvent) => { event.stopPropagation(); @@ -40,7 +45,7 @@ const MarkdownInput: React.FC = ({ value = "", onChange, ...props }) => { ref={textareaRef} showCount /> -