diff --git a/web/e2e/project/item/fields/markdown.spec.ts b/web/e2e/project/item/fields/markdown.spec.ts index 2e14fe77ae..7758fc649e 100644 --- a/web/e2e/project/item/fields/markdown.spec.ts +++ b/web/e2e/project/item/fields/markdown.spec.ts @@ -102,8 +102,7 @@ test("Markdown field editing has succeeded", async ({ page }) => { await page.getByRole("button", { name: "delete" }).first().click(); await expect(page.getByText("Please input field!")).toBeVisible(); await page.getByRole("button", { name: "plus New" }).click(); - await page.getByRole("button", { name: "Save" }).click(); - await closeNotification(page, false); + await expect(page.getByRole("button", { name: "Save" })).toBeDisabled(); await page.locator("div:nth-child(1) > .css-1ago99h").click(); await page.getByRole("textbox").fill("text"); await page.getByRole("button", { name: "plus New" }).click(); diff --git a/web/e2e/project/item/fields/text.spec.ts b/web/e2e/project/item/fields/text.spec.ts index 4c17389860..23c52a4c8f 100644 --- a/web/e2e/project/item/fields/text.spec.ts +++ b/web/e2e/project/item/fields/text.spec.ts @@ -101,8 +101,7 @@ test("Text field editing has succeeded", async ({ page }) => { await expect(page.getByText("Please input field!")).toBeVisible(); await page.getByRole("button", { name: "plus New" }).click(); await expect(page.getByText("/ 5")).toBeVisible(); - await page.getByRole("button", { name: "Save" }).click(); - await closeNotification(page, false); + await expect(page.getByRole("button", { name: "Save" })).toBeDisabled(); await page.getByRole("textbox").nth(0).click(); await page.getByRole("textbox").nth(0).fill("text"); await page.getByRole("button", { name: "plus New" }).click(); diff --git a/web/e2e/project/item/fields/textarea.spec.ts b/web/e2e/project/item/fields/textarea.spec.ts index 0269482f4b..857ae2d8e5 100644 --- a/web/e2e/project/item/fields/textarea.spec.ts +++ b/web/e2e/project/item/fields/textarea.spec.ts @@ -102,8 +102,7 @@ test("Textarea field editing has succeeded", async ({ page }) => { await page.getByRole("button", { name: "delete" }).first().click(); await expect(page.getByText("Please input field!")).toBeVisible(); await page.getByRole("button", { name: "plus New" }).click(); - await page.getByRole("button", { name: "Save" }).click(); - await closeNotification(page, false); + await expect(page.getByRole("button", { name: "Save" })).toBeDisabled(); await page.getByRole("textbox").nth(0).click(); await page.getByRole("textbox").nth(0).fill("text"); await page.getByRole("button", { name: "plus New" }).click(); diff --git a/web/src/components/atoms/Form/index.tsx b/web/src/components/atoms/Form/index.tsx index 73a4594167..a30b6c0968 100644 --- a/web/src/components/atoms/Form/index.tsx +++ b/web/src/components/atoms/Form/index.tsx @@ -1,5 +1,5 @@ import { Form, FormInstance } from "antd"; -import { Rule } from "antd/lib/form"; +import { Rule, RuleObject } from "antd/lib/form"; import { FormItemProps } from "antd/lib/form/FormItem"; import { FormItemLabelProps } from "antd/lib/form/FormItemLabel"; import { FieldError, ValidateErrorEntity } from "rc-field-form/lib/interface"; @@ -12,5 +12,6 @@ export type { FieldError, FormInstance, Rule, + RuleObject, ValidateErrorEntity, }; diff --git a/web/src/components/atoms/Input/index.tsx b/web/src/components/atoms/Input/index.tsx index 0e2f5da9ad..e1f17b3f05 100644 --- a/web/src/components/atoms/Input/index.tsx +++ b/web/src/components/atoms/Input/index.tsx @@ -9,26 +9,32 @@ type Props = { isError?: boolean; } & InputProps; -const Input = forwardRef(({ value, isError, maxLength, ...props }, ref) => { - const status = useMemo(() => { - if (isError || (maxLength && value && runes(value).length > maxLength)) { - return "error"; - } - }, [isError, maxLength, value]); +const Input = forwardRef( + ({ value, isError, maxLength, required, ...props }, ref) => { + const status = useMemo(() => { + if ( + isError || + (required && !value) || + (maxLength && value && runes(value).length > maxLength) + ) { + return "error"; + } + }, [isError, maxLength, required, value]); - return ( - runes(txt).length, - }} - value={value} - ref={ref} - status={status} - {...props} - /> - ); -}); + return ( + runes(txt).length, + }} + value={value} + ref={ref} + status={status} + {...props} + /> + ); + }, +); export default Input; export type { InputProps }; diff --git a/web/src/components/atoms/Markdown/index.tsx b/web/src/components/atoms/Markdown/index.tsx index f2ba814861..eafc8c0a1d 100644 --- a/web/src/components/atoms/Markdown/index.tsx +++ b/web/src/components/atoms/Markdown/index.tsx @@ -8,15 +8,21 @@ import TextArea, { TextAreaProps } from "@reearth-cms/components/atoms/TextArea" type Props = { value?: string; onChange?: (value: string) => void; + isError?: boolean; } & TextAreaProps; 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 isError = useMemo(() => { + if (props.isError || (props.required && !value)) { + return true; + } else if (props.maxLength && value) { + return runes(value).length > props.maxLength; + } else { + return false; + } + }, [props, value]); const handleBlur = useCallback((event: FocusEvent) => { event.stopPropagation(); diff --git a/web/src/components/atoms/TextArea/index.tsx b/web/src/components/atoms/TextArea/index.tsx index 8f9dc2f6ae..852323b889 100644 --- a/web/src/components/atoms/TextArea/index.tsx +++ b/web/src/components/atoms/TextArea/index.tsx @@ -4,28 +4,35 @@ import { runes } from "runes2"; type Props = { value?: string; + isError?: boolean; } & TextAreaProps; -const TextArea = forwardRef(({ value, maxLength, ...props }, ref) => { - const status = useMemo(() => { - if (maxLength && value && runes(value).length > maxLength) { - return "error"; - } - }, [maxLength, value]); +const TextArea = forwardRef( + ({ value, isError, maxLength, required, ...props }, ref) => { + const status = useMemo(() => { + if ( + isError || + (required && !value) || + (maxLength && value && runes(value).length > maxLength) + ) { + return "error"; + } + }, [required, isError, maxLength, value]); - return ( - runes(txt).length, - }} - value={value} - ref={ref} - status={status} - {...props} - /> - ); -}); + return ( + runes(txt).length, + }} + value={value} + ref={ref} + status={status} + {...props} + /> + ); + }, +); export default TextArea; export type { TextAreaProps }; diff --git a/web/src/components/molecules/Common/MultiValueField/index.tsx b/web/src/components/molecules/Common/MultiValueField/index.tsx index 11488b0d66..43de23d37f 100644 --- a/web/src/components/molecules/Common/MultiValueField/index.tsx +++ b/web/src/components/molecules/Common/MultiValueField/index.tsx @@ -6,6 +6,7 @@ import Button from "@reearth-cms/components/atoms/Button"; import Icon from "@reearth-cms/components/atoms/Icon"; import { InputProps } from "@reearth-cms/components/atoms/Input"; import { TextAreaProps } from "@reearth-cms/components/atoms/TextArea"; +import { checkIfEmpty } from "@reearth-cms/components/molecules/Content/Form/fields/utils"; import { useT } from "@reearth-cms/i18n"; import { moveItemInArray } from "./moveItemArray"; @@ -26,6 +27,7 @@ const MultiValueField: React.FC = ({ onBlur, FieldInput, errorIndexes, + required, ...props }) => { const t = useT(); @@ -93,7 +95,7 @@ const MultiValueField: React.FC = ({ onChange={(e: ChangeEvent) => handleInput(e, key)} onBlur={() => onBlur?.()} value={valueItem} - isError={errorIndexes?.has(key)} + isError={(required && value.every(v => checkIfEmpty(v))) || errorIndexes?.has(key)} /> {!props.disabled && ( = ({ rules={[ { required: field.required, + validator: requiredValidator, message: t("Please input field!"), }, ]} diff --git a/web/src/components/molecules/Content/Form/fields/FieldComponents/DateField.tsx b/web/src/components/molecules/Content/Form/fields/FieldComponents/DateField.tsx index b47282ef8f..ad45046135 100644 --- a/web/src/components/molecules/Content/Form/fields/FieldComponents/DateField.tsx +++ b/web/src/components/molecules/Content/Form/fields/FieldComponents/DateField.tsx @@ -7,6 +7,7 @@ import { Field } from "@reearth-cms/components/molecules/Schema/types"; import { useT } from "@reearth-cms/i18n"; import FieldTitle from "../../FieldTitle"; +import { requiredValidator } from "../utils"; type DateFieldProps = { field: Field; @@ -24,6 +25,7 @@ const DateField: React.FC = ({ field, itemGroupId, onMetaUpdate, rules={[ { required: field.required, + validator: requiredValidator, message: t("Please input field!"), }, ]} diff --git a/web/src/components/molecules/Content/Form/fields/FieldComponents/DefaultField.tsx b/web/src/components/molecules/Content/Form/fields/FieldComponents/DefaultField.tsx index 7399f13234..7f01f130bc 100644 --- a/web/src/components/molecules/Content/Form/fields/FieldComponents/DefaultField.tsx +++ b/web/src/components/molecules/Content/Form/fields/FieldComponents/DefaultField.tsx @@ -8,6 +8,7 @@ import { Field } from "@reearth-cms/components/molecules/Schema/types"; import { useT } from "@reearth-cms/i18n"; import FieldTitle from "../../FieldTitle"; +import { requiredValidator } from "../utils"; type DefaultFieldProps = { field: Field; @@ -25,13 +26,16 @@ const DefaultField: React.FC = ({ const t = useT(); const maxLength = useMemo(() => field.typeProperty?.maxLength, [field.typeProperty?.maxLength]); + const required = useMemo(() => field.required, [field.required]); + return ( = ({ maxLength={maxLength} FieldInput={Input} disabled={disabled} + required={required} /> ) : ( - + )} ); diff --git a/web/src/components/molecules/Content/Form/fields/FieldComponents/GeometryField.tsx b/web/src/components/molecules/Content/Form/fields/FieldComponents/GeometryField.tsx index b4d2b86def..cbeeba82b4 100644 --- a/web/src/components/molecules/Content/Form/fields/FieldComponents/GeometryField.tsx +++ b/web/src/components/molecules/Content/Form/fields/FieldComponents/GeometryField.tsx @@ -7,6 +7,7 @@ import { Field } from "@reearth-cms/components/molecules/Schema/types"; import { useT } from "@reearth-cms/i18n"; import FieldTitle from "../../FieldTitle"; +import { requiredValidator } from "../utils"; type DefaultFieldProps = { field: Field; @@ -39,6 +40,7 @@ const GeometryField: React.FC = ({ field, itemGroupId, disabl rules={[ { required: field.required, + validator: requiredValidator, message: t("Please input field!"), }, { diff --git a/web/src/components/molecules/Content/Form/fields/FieldComponents/MarkdownField.tsx b/web/src/components/molecules/Content/Form/fields/FieldComponents/MarkdownField.tsx index f29b4f4ef2..e1d552d739 100644 --- a/web/src/components/molecules/Content/Form/fields/FieldComponents/MarkdownField.tsx +++ b/web/src/components/molecules/Content/Form/fields/FieldComponents/MarkdownField.tsx @@ -8,6 +8,7 @@ import { Field } from "@reearth-cms/components/molecules/Schema/types"; import { useT } from "@reearth-cms/i18n"; import FieldTitle from "../../FieldTitle"; +import { requiredValidator } from "../utils"; type DefaultFieldProps = { field: Field; @@ -19,13 +20,16 @@ const MarkdownField: React.FC = ({ field, itemGroupId, disabl const t = useT(); const maxLength = useMemo(() => field.typeProperty?.maxLength, [field.typeProperty?.maxLength]); + const required = useMemo(() => field.required, [field.required]); + return ( = ({ field, itemGroupId, disabl name={itemGroupId ? [field.id, itemGroupId] : field.id} label={}> {field.multiple ? ( - + ) : ( - + )} ); diff --git a/web/src/components/molecules/Content/Form/fields/FieldComponents/NumberField.tsx b/web/src/components/molecules/Content/Form/fields/FieldComponents/NumberField.tsx index ba91bfbe2e..bc2a3ceb93 100644 --- a/web/src/components/molecules/Content/Form/fields/FieldComponents/NumberField.tsx +++ b/web/src/components/molecules/Content/Form/fields/FieldComponents/NumberField.tsx @@ -7,6 +7,7 @@ import { Field } from "@reearth-cms/components/molecules/Schema/types"; import { useT } from "@reearth-cms/i18n"; import FieldTitle from "../../FieldTitle"; +import { requiredValidator } from "../utils"; type DefaultFieldProps = { field: Field; @@ -42,6 +43,7 @@ const NumberField: React.FC = ({ field, itemGroupId, disabled rules={[ { required: field.required, + validator: requiredValidator, message: t("Please input field!"), }, { diff --git a/web/src/components/molecules/Content/Form/fields/FieldComponents/SelectField.tsx b/web/src/components/molecules/Content/Form/fields/FieldComponents/SelectField.tsx index 67e789f312..a28d3e7ac3 100644 --- a/web/src/components/molecules/Content/Form/fields/FieldComponents/SelectField.tsx +++ b/web/src/components/molecules/Content/Form/fields/FieldComponents/SelectField.tsx @@ -5,6 +5,7 @@ import { Field } from "@reearth-cms/components/molecules/Schema/types"; import { useT } from "@reearth-cms/i18n"; import FieldTitle from "../../FieldTitle"; +import { requiredValidator } from "../utils"; type DefaultFieldProps = { field: Field; @@ -24,6 +25,7 @@ const SelectField: React.FC = ({ field, itemGroupId, disabled rules={[ { required: field.required, + validator: requiredValidator, message: t("Please select an option!"), }, ]}> diff --git a/web/src/components/molecules/Content/Form/fields/FieldComponents/TextareaField.tsx b/web/src/components/molecules/Content/Form/fields/FieldComponents/TextareaField.tsx index 03ee71eb31..571a4a60f9 100644 --- a/web/src/components/molecules/Content/Form/fields/FieldComponents/TextareaField.tsx +++ b/web/src/components/molecules/Content/Form/fields/FieldComponents/TextareaField.tsx @@ -8,6 +8,7 @@ import { Field } from "@reearth-cms/components/molecules/Schema/types"; import { useT } from "@reearth-cms/i18n"; import FieldTitle from "../../FieldTitle"; +import { requiredValidator } from "../utils"; type DefaultFieldProps = { field: Field; @@ -19,13 +20,16 @@ const TextareaField: React.FC = ({ field, itemGroupId, disabl const t = useT(); const maxLength = useMemo(() => field.typeProperty?.maxLength, [field.typeProperty?.maxLength]); + const required = useMemo(() => field.required, [field.required]); + return ( = ({ field, itemGroupId, disabl maxLength={maxLength} FieldInput={TextArea} disabled={disabled} + required={required} /> ) : ( -