From 877defff352d219f3e111c29b9f94a1c4e5daf9c Mon Sep 17 00:00:00 2001 From: caichi Date: Mon, 2 Dec 2024 21:00:22 +0900 Subject: [PATCH 01/10] fix: sort order of tags --- .../molecules/Content/Form/index.tsx | 22 ++++++++++------ web/src/components/molecules/Content/utils.ts | 4 +++ .../molecules/Schema/FieldModal/hooks.ts | 14 +++++++++-- web/src/components/molecules/Schema/types.ts | 2 +- .../Project/Content/ContentList/hooks.ts | 25 +++++++++++++------ 5 files changed, 49 insertions(+), 18 deletions(-) diff --git a/web/src/components/molecules/Content/Form/index.tsx b/web/src/components/molecules/Content/Form/index.tsx index 266e9d0d82..5e143feb1f 100644 --- a/web/src/components/molecules/Content/Form/index.tsx +++ b/web/src/components/molecules/Content/Form/index.tsx @@ -24,6 +24,7 @@ import { ItemField, ItemValue, } from "@reearth-cms/components/molecules/Content/types"; +import { selectedTagIdsGet } from "@reearth-cms/components/molecules/Content/utils"; import { Model } from "@reearth-cms/components/molecules/Model/types"; import { Request, @@ -333,12 +334,17 @@ const ContentForm: React.FC = ({ [formItemsData], ); - const inputValueGet = useCallback((value: ItemValue, multiple: boolean) => { - if (multiple) { + const inputValueGet = useCallback((value: ItemValue, field: Field) => { + if (field.multiple) { if (Array.isArray(value)) { - return value.map(v => - v === "" ? undefined : dayjs.isDayjs(v) ? transformDayjsToString(v) : v, - ); + if (field.type === "Tag") { + const tags = field.typeProperty?.tags; + return tags ? selectedTagIdsGet(value as string[], tags) : []; + } else { + return value.map(v => + v === "" ? undefined : dayjs.isDayjs(v) ? transformDayjsToString(v) : v, + ); + } } else { return []; } @@ -364,7 +370,7 @@ const ContentForm: React.FC = ({ const metaField = metaFieldsMap.get(key); if (metaField) { result.push({ - value: inputValueGet(value as ItemValue, metaField.multiple), + value: inputValueGet(value as ItemValue, metaField), schemaFieldId: key, type: metaField.type, }); @@ -394,7 +400,7 @@ const ContentForm: React.FC = ({ const modelField = modelFields.get(key); if (modelField) { fields.push({ - value: inputValueGet(value as ItemValue, modelField.multiple), + value: inputValueGet(value as ItemValue, modelField), schemaFieldId: key, type: modelField.type, }); @@ -403,7 +409,7 @@ const ContentForm: React.FC = ({ const groupField = groupFields.get(key); if (groupField) { fields.push({ - value: inputValueGet(groupFieldValue, groupField.multiple), + value: inputValueGet(groupFieldValue, groupField), schemaFieldId: key, itemGroupId: groupFieldKey, type: groupField.type, diff --git a/web/src/components/molecules/Content/utils.ts b/web/src/components/molecules/Content/utils.ts index b598076613..908f61fd69 100644 --- a/web/src/components/molecules/Content/utils.ts +++ b/web/src/components/molecules/Content/utils.ts @@ -1,4 +1,5 @@ import { ColorType, StateType } from "@reearth-cms/components/molecules/Content/Table/types"; +import { Tag } from "@reearth-cms/components/molecules/Schema/types"; export const stateColors: { [K in StateType]: ColorType; @@ -7,3 +8,6 @@ export const stateColors: { PUBLIC: "#52C41A", REVIEW: "#FA8C16", }; + +export const selectedTagIdsGet = (value: string[], tags: Tag[]) => + value.length ? tags.filter(tag => value.includes(tag.id)).map(({ id }) => id) : []; diff --git a/web/src/components/molecules/Schema/FieldModal/hooks.ts b/web/src/components/molecules/Schema/FieldModal/hooks.ts index 9af508154f..6a8f16eff3 100644 --- a/web/src/components/molecules/Schema/FieldModal/hooks.ts +++ b/web/src/components/molecules/Schema/FieldModal/hooks.ts @@ -197,10 +197,20 @@ export default ( return { date: { defaultValue: transformDayjsToString(values.defaultValue) ?? "" }, }; - case "Tag": + case "Tag": { + const defaultValue = + Array.isArray(values.defaultValue) && values.defaultValue.length + ? values.tags + ?.filter(tag => values.defaultValue.includes(tag.name)) + .map(({ name }) => name) + : values.defaultValue; return { - tag: { defaultValue: values.defaultValue, tags: values.tags ?? [] }, + tag: { + defaultValue, + tags: values.tags ?? [], + }, }; + } case "Checkbox": return { checkbox: { defaultValue: values.defaultValue }, diff --git a/web/src/components/molecules/Schema/types.ts b/web/src/components/molecules/Schema/types.ts index 596257f874..4239b207e4 100644 --- a/web/src/components/molecules/Schema/types.ts +++ b/web/src/components/molecules/Schema/types.ts @@ -109,7 +109,7 @@ export type FieldTypePropertyInput = { bool?: { defaultValue?: boolean }; select?: { defaultValue: string; values: string[] }; tag?: { - defaultValue?: string; + defaultValue?: string | string[]; tags: Tag[]; }; checkbox?: { defaultValue?: boolean }; diff --git a/web/src/components/organisms/Project/Content/ContentList/hooks.ts b/web/src/components/organisms/Project/Content/ContentList/hooks.ts index e7639355ec..fadf3d17de 100644 --- a/web/src/components/organisms/Project/Content/ContentList/hooks.ts +++ b/web/src/components/organisms/Project/Content/ContentList/hooks.ts @@ -12,6 +12,7 @@ import { ItemField, Metadata, } from "@reearth-cms/components/molecules/Content/types"; +import { selectedTagIdsGet } from "@reearth-cms/components/molecules/Content/utils"; import { Request, RequestItem } from "@reearth-cms/components/molecules/Request/types"; import { ConditionInput, @@ -201,6 +202,11 @@ export default () => { [getItem], ); + const metaFieldsMap = useMemo( + () => new Map((currentModel?.metadataSchema.fields || []).map(field => [field.id, field])), + [currentModel?.metadataSchema.fields], + ); + const handleMetaItemUpdate = useCallback( async ( updateItemId: string, @@ -209,7 +215,7 @@ export default () => { index?: number, ) => { const target = data?.searchItem.nodes.find(item => item?.id === updateItemId); - if (!target || !currentModel?.metadataSchema?.id || !currentModel.metadataSchema.fields) { + if (!target || !currentModel?.metadataSchema?.id || !metaFieldsMap) { Notification.error({ message: t("Failed to update item.") }); return; } else { @@ -217,8 +223,13 @@ export default () => { if (metadata?.fields && metadata.id) { const fields = metadata.fields.map(field => { if (field.schemaFieldId === key) { - if (Array.isArray(field.value) && field.type !== "Tag") { - field.value[index ?? 0] = value ?? ""; + if (Array.isArray(field.value)) { + if (field.type === "Tag") { + const tags = metaFieldsMap.get(key)?.typeProperty?.tags; + field.value = tags ? selectedTagIdsGet(value as string[], tags) : []; + } else { + field.value[index ?? 0] = value ?? ""; + } } else { field.value = value ?? ""; } @@ -239,10 +250,10 @@ export default () => { return; } } else { - const fields = currentModel.metadataSchema.fields.map(field => ({ - value: field.id === key ? value : "", + const fields = [...metaFieldsMap].map(field => ({ + value: field[1].id === key ? value : "", schemaFieldId: key, - type: field.type as SchemaFieldType, + type: field[1].type as SchemaFieldType, })); const metaItem = await createNewItem({ variables: { @@ -278,9 +289,9 @@ export default () => { [ createNewItem, currentModel?.id, - currentModel?.metadataSchema.fields, currentModel?.metadataSchema.id, data?.searchItem.nodes, + metaFieldsMap, metadataVersionSet, t, updateItemMutation, From 88b60d777e2fdc4433505c5bdd34f29cb4ee7c06 Mon Sep 17 00:00:00 2001 From: caichi Date: Mon, 2 Dec 2024 21:01:44 +0900 Subject: [PATCH 02/10] fix: convert empty to undefined --- .../components/organisms/Project/Content/ContentList/hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/organisms/Project/Content/ContentList/hooks.ts b/web/src/components/organisms/Project/Content/ContentList/hooks.ts index fadf3d17de..5dbe8a9a5d 100644 --- a/web/src/components/organisms/Project/Content/ContentList/hooks.ts +++ b/web/src/components/organisms/Project/Content/ContentList/hooks.ts @@ -228,7 +228,7 @@ export default () => { const tags = metaFieldsMap.get(key)?.typeProperty?.tags; field.value = tags ? selectedTagIdsGet(value as string[], tags) : []; } else { - field.value[index ?? 0] = value ?? ""; + field.value[index ?? 0] = value === "" ? undefined : value; } } else { field.value = value ?? ""; From c0dace98fa2640fb6149dc3498ec9a987be1f08c Mon Sep 17 00:00:00 2001 From: caichi Date: Wed, 4 Dec 2024 16:11:50 +0900 Subject: [PATCH 03/10] fix: avoid redundant queries and add validation --- .../Content/RenderField/ItemFormat.tsx | 23 +++++++-- .../molecules/Content/RenderField/index.tsx | 6 +-- .../Project/Content/ContentList/hooks.ts | 48 ++++++++++++++++--- web/src/i18n/translations/en.yml | 6 ++- web/src/i18n/translations/ja.yml | 6 ++- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/web/src/components/molecules/Content/RenderField/ItemFormat.tsx b/web/src/components/molecules/Content/RenderField/ItemFormat.tsx index 8906d3a2a4..5e05db0817 100644 --- a/web/src/components/molecules/Content/RenderField/ItemFormat.tsx +++ b/web/src/components/molecules/Content/RenderField/ItemFormat.tsx @@ -8,11 +8,13 @@ import Checkbox from "@reearth-cms/components/atoms/Checkbox"; import DatePicker from "@reearth-cms/components/atoms/DatePicker"; import Icon from "@reearth-cms/components/atoms/Icon"; import Input from "@reearth-cms/components/atoms/Input"; +import Notification from "@reearth-cms/components/atoms/Notification"; import Switch from "@reearth-cms/components/atoms/Switch"; import Tag from "@reearth-cms/components/atoms/Tag"; import Tooltip from "@reearth-cms/components/atoms/Tooltip"; import { fieldTypes } from "@reearth-cms/components/molecules/Schema/fieldTypes"; import type { Field } from "@reearth-cms/components/molecules/Schema/types"; +import { useT } from "@reearth-cms/i18n"; import { dateTimeFormat, transformDayjsToString } from "@reearth-cms/utils/format"; import { validateURL } from "@reearth-cms/utils/regex"; @@ -24,23 +26,34 @@ type Props = { }; export const ItemFormat: React.FC = ({ item, field, update, index }) => { + const t = useT(); + const [isEditable, setIsEditable] = useState(false); const [itemState, setItemState] = useState(item); const handleUrlBlur = useCallback( (e: FocusEvent) => { - if (e.target.value && !validateURL(e.target.value)) return; - update?.(e.target.value, index); - setItemState(e.target.value); + const value = e.target.value; + if (itemState === value) { + setIsEditable(false); + return; + } + if (value && !validateURL(value)) { + Notification.error({ message: t("Please input a valid URL") }); + return; + } + update?.(value, index); + setItemState(value); setIsEditable(false); }, - [index, update], + [index, itemState, t, update], ); switch (field.type) { case "Text": return update ? ( { @@ -112,7 +125,7 @@ export const ItemFormat: React.FC = ({ item, field, update, index }) => { /> ) : ( ); } else if (value === "-") { - if ( - (field.type === "Text" || field.type === "Date" || field.type === "URL") && - !field.multiple && - update - ) { + if ((field.type === "Text" || field.type === "Date" || field.type === "URL") && update) { return ; } return -; diff --git a/web/src/components/organisms/Project/Content/ContentList/hooks.ts b/web/src/components/organisms/Project/Content/ContentList/hooks.ts index 5dbe8a9a5d..6dba39d611 100644 --- a/web/src/components/organisms/Project/Content/ContentList/hooks.ts +++ b/web/src/components/organisms/Project/Content/ContentList/hooks.ts @@ -10,7 +10,6 @@ import { Item, ItemStatus, ItemField, - Metadata, } from "@reearth-cms/components/molecules/Content/types"; import { selectedTagIdsGet } from "@reearth-cms/components/molecules/Content/utils"; import { Request, RequestItem } from "@reearth-cms/components/molecules/Request/types"; @@ -190,13 +189,13 @@ export default () => { const [getItem] = useGetItemLazyQuery({ fetchPolicy: "no-cache" }); const [createNewItem] = useCreateItemMutation(); - const itemIdToMetadata = useRef(new Map()); + const itemIdToMetadataVersion = useRef(new Map()); const metadataVersionSet = useCallback( async (id: string) => { const { data } = await getItem({ variables: { id } }); const item = fromGraphQLItem(data?.node as GQLItem); if (item) { - itemIdToMetadata.current.set(id, item.metadata); + itemIdToMetadataVersion.current.set(id, item.metadata.version); } }, [getItem], @@ -219,13 +218,17 @@ export default () => { Notification.error({ message: t("Failed to update item.") }); return; } else { - const metadata = itemIdToMetadata.current.get(updateItemId) ?? target.metadata; - if (metadata?.fields && metadata.id) { + const metadata = target.metadata; + const version = itemIdToMetadataVersion.current.get(updateItemId) ?? metadata?.version; + if (metadata?.fields && metadata.id && version) { + const requiredErrorFields: string[] = []; + const maxLengthErrorFields: string[] = []; const fields = metadata.fields.map(field => { + const metaField = metaFieldsMap.get(field.schemaFieldId); if (field.schemaFieldId === key) { if (Array.isArray(field.value)) { if (field.type === "Tag") { - const tags = metaFieldsMap.get(key)?.typeProperty?.tags; + const tags = metaField?.typeProperty?.tags; field.value = tags ? selectedTagIdsGet(value as string[], tags) : []; } else { field.value[index ?? 0] = value === "" ? undefined : value; @@ -236,13 +239,44 @@ export default () => { } else { field.value = field.value ?? ""; } + const fieldValue = field.value; + if (metaField?.required) { + // use checkIfEmpty + if (Array.isArray(fieldValue)) { + if (fieldValue.every(v => v === undefined || v === null || v === "")) { + requiredErrorFields.push(metaField.key); + } + } else if (fieldValue === undefined || fieldValue === null || fieldValue === "") { + requiredErrorFields.push(metaField.key); + } + } + const maxLength = metaField?.typeProperty?.maxLength; + if (maxLength) { + if (Array.isArray(fieldValue)) { + if (fieldValue.some(v => v.length > maxLength)) { + maxLengthErrorFields.push(metaField.key); + } + } else if (fieldValue.length > maxLength) { + maxLengthErrorFields.push(metaField.key); + } + } + return field as ItemFieldInput; }); + if (requiredErrorFields.length || maxLengthErrorFields.length) { + requiredErrorFields.forEach(field => { + Notification.error({ message: t("Required field error", { field }) }); + }); + maxLengthErrorFields.forEach(field => { + Notification.error({ message: t("Maximum length error", { field }) }); + }); + return; + } const item = await updateItemMutation({ variables: { itemId: metadata.id, fields, - version: metadata.version, + version, }, }); if (item.errors || !item.data?.updateItem) { diff --git a/web/src/i18n/translations/en.yml b/web/src/i18n/translations/en.yml index d1b8f95700..6d4b5e5f4c 100644 --- a/web/src/i18n/translations/en.yml +++ b/web/src/i18n/translations/en.yml @@ -485,11 +485,12 @@ Failed to update item.: '' Successfully updated Item!: '' Failed to create request.: '' Successfully created request!: '' -Failed to update request.: '' -Successfully updated request!: '' +Required field error: '{{field}} field is required!' +Maximum length error: Character count in {{field}} field exceeds the maximum number! Failed to delete one or more items.: '' One or more items were successfully deleted!: '' One of the items already exists in the request.: '' +Failed to update request.: '' Successfully updated Request!: '' Failed to publish items.: '' Successfully published items!: '' @@ -517,6 +518,7 @@ Failed to delete model.: '' Successfully deleted model!: '' Failed to update model.: '' Successfully updated model!: '' +Successfully updated request!: '' Failed to delete one or more requests.: '' One or more requests were successfully closed!: '' Failed to approve request.: '' diff --git a/web/src/i18n/translations/ja.yml b/web/src/i18n/translations/ja.yml index 1e3942fbd5..2c0e13ddc0 100644 --- a/web/src/i18n/translations/ja.yml +++ b/web/src/i18n/translations/ja.yml @@ -485,11 +485,12 @@ Failed to update item.: アイテムの更新に失敗しました。 Successfully updated Item!: アイテムの更新に成功しました。 Failed to create request.: リクエストの作成に失敗しました。 Successfully created request!: リクエストの作成に成功しました。 -Failed to update request.: リクエストの更新に失敗しました。 -Successfully updated request!: リクエストの更新に成功しました。 +Required field error: '{{field}}フィールドは必須項目です!' +Maximum length error: '{{field}}フィールドの文字数が最大数を超えています!' Failed to delete one or more items.: アイテムの削除に失敗しました。 One or more items were successfully deleted!: アイテムの削除に成功しました。 One of the items already exists in the request.: アイテムのいずれかがすでにリクエストに存在します。 +Failed to update request.: リクエストの更新に失敗しました。 Successfully updated Request!: リクエストの更新に成功しました。 Failed to publish items.: アイテムの公開に失敗しました。 Successfully published items!: アイテムの公開に成功しました! @@ -517,6 +518,7 @@ Failed to delete model.: モデルの削除に失敗しました。 Successfully deleted model!: モデルの削除に成功しました。 Failed to update model.: モデルの更新に失敗しました。 Successfully updated model!: モデルの更新に成功しました。 +Successfully updated request!: リクエストの更新に成功しました。 Failed to delete one or more requests.: リクエストの削除に失敗しました。 One or more requests were successfully closed!: リクエストのクローズに成功しました。 Failed to approve request.: リクエストの承認に失敗しました。 From 7e4745a81ee783e7334efdbeeb7ebf979879f260 Mon Sep 17 00:00:00 2001 From: caichi Date: Wed, 4 Dec 2024 16:12:05 +0900 Subject: [PATCH 04/10] fix: e2e test --- web/e2e/common/notification.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/e2e/common/notification.ts b/web/e2e/common/notification.ts index b70d4cd610..0a7d15e151 100644 --- a/web/e2e/common/notification.ts +++ b/web/e2e/common/notification.ts @@ -3,8 +3,8 @@ import { Page } from "@playwright/test"; import { expect } from "@reearth-cms/e2e/utils"; export async function closeNotification(page: Page, isSuccess = true) { - const text = isSuccess ? /successfully|成功/i : "input: "; - await expect(page.getByRole("alert").last()).toContainText(text); + const text = isSuccess ? "check-circle" : "close-circle"; + await expect(page.getByRole("alert").last().getByRole("img")).toHaveAttribute("aria-label", text); await page .locator(".ant-notification-notice") .last() From 74a2d5eaf312ada43aa67df600610a62b5681b46 Mon Sep 17 00:00:00 2001 From: caichi Date: Mon, 2 Dec 2024 21:01:44 +0900 Subject: [PATCH 05/10] fix: convert empty to undefined --- .../components/organisms/Project/Content/ContentList/hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/organisms/Project/Content/ContentList/hooks.ts b/web/src/components/organisms/Project/Content/ContentList/hooks.ts index fadf3d17de..5dbe8a9a5d 100644 --- a/web/src/components/organisms/Project/Content/ContentList/hooks.ts +++ b/web/src/components/organisms/Project/Content/ContentList/hooks.ts @@ -228,7 +228,7 @@ export default () => { const tags = metaFieldsMap.get(key)?.typeProperty?.tags; field.value = tags ? selectedTagIdsGet(value as string[], tags) : []; } else { - field.value[index ?? 0] = value ?? ""; + field.value[index ?? 0] = value === "" ? undefined : value; } } else { field.value = value ?? ""; From ab2ac13397aa39a0fd7c749ad70176e4d72f105b Mon Sep 17 00:00:00 2001 From: caichi Date: Wed, 4 Dec 2024 16:11:50 +0900 Subject: [PATCH 06/10] fix: avoid redundant queries and add validation --- .../Content/RenderField/ItemFormat.tsx | 23 +++++++-- .../molecules/Content/RenderField/index.tsx | 6 +-- .../Project/Content/ContentList/hooks.ts | 48 ++++++++++++++++--- web/src/i18n/translations/en.yml | 6 ++- web/src/i18n/translations/ja.yml | 6 ++- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/web/src/components/molecules/Content/RenderField/ItemFormat.tsx b/web/src/components/molecules/Content/RenderField/ItemFormat.tsx index 8906d3a2a4..5e05db0817 100644 --- a/web/src/components/molecules/Content/RenderField/ItemFormat.tsx +++ b/web/src/components/molecules/Content/RenderField/ItemFormat.tsx @@ -8,11 +8,13 @@ import Checkbox from "@reearth-cms/components/atoms/Checkbox"; import DatePicker from "@reearth-cms/components/atoms/DatePicker"; import Icon from "@reearth-cms/components/atoms/Icon"; import Input from "@reearth-cms/components/atoms/Input"; +import Notification from "@reearth-cms/components/atoms/Notification"; import Switch from "@reearth-cms/components/atoms/Switch"; import Tag from "@reearth-cms/components/atoms/Tag"; import Tooltip from "@reearth-cms/components/atoms/Tooltip"; import { fieldTypes } from "@reearth-cms/components/molecules/Schema/fieldTypes"; import type { Field } from "@reearth-cms/components/molecules/Schema/types"; +import { useT } from "@reearth-cms/i18n"; import { dateTimeFormat, transformDayjsToString } from "@reearth-cms/utils/format"; import { validateURL } from "@reearth-cms/utils/regex"; @@ -24,23 +26,34 @@ type Props = { }; export const ItemFormat: React.FC = ({ item, field, update, index }) => { + const t = useT(); + const [isEditable, setIsEditable] = useState(false); const [itemState, setItemState] = useState(item); const handleUrlBlur = useCallback( (e: FocusEvent) => { - if (e.target.value && !validateURL(e.target.value)) return; - update?.(e.target.value, index); - setItemState(e.target.value); + const value = e.target.value; + if (itemState === value) { + setIsEditable(false); + return; + } + if (value && !validateURL(value)) { + Notification.error({ message: t("Please input a valid URL") }); + return; + } + update?.(value, index); + setItemState(value); setIsEditable(false); }, - [index, update], + [index, itemState, t, update], ); switch (field.type) { case "Text": return update ? ( { @@ -112,7 +125,7 @@ export const ItemFormat: React.FC = ({ item, field, update, index }) => { /> ) : ( ); } else if (value === "-") { - if ( - (field.type === "Text" || field.type === "Date" || field.type === "URL") && - !field.multiple && - update - ) { + if ((field.type === "Text" || field.type === "Date" || field.type === "URL") && update) { return ; } return -; diff --git a/web/src/components/organisms/Project/Content/ContentList/hooks.ts b/web/src/components/organisms/Project/Content/ContentList/hooks.ts index 5dbe8a9a5d..6dba39d611 100644 --- a/web/src/components/organisms/Project/Content/ContentList/hooks.ts +++ b/web/src/components/organisms/Project/Content/ContentList/hooks.ts @@ -10,7 +10,6 @@ import { Item, ItemStatus, ItemField, - Metadata, } from "@reearth-cms/components/molecules/Content/types"; import { selectedTagIdsGet } from "@reearth-cms/components/molecules/Content/utils"; import { Request, RequestItem } from "@reearth-cms/components/molecules/Request/types"; @@ -190,13 +189,13 @@ export default () => { const [getItem] = useGetItemLazyQuery({ fetchPolicy: "no-cache" }); const [createNewItem] = useCreateItemMutation(); - const itemIdToMetadata = useRef(new Map()); + const itemIdToMetadataVersion = useRef(new Map()); const metadataVersionSet = useCallback( async (id: string) => { const { data } = await getItem({ variables: { id } }); const item = fromGraphQLItem(data?.node as GQLItem); if (item) { - itemIdToMetadata.current.set(id, item.metadata); + itemIdToMetadataVersion.current.set(id, item.metadata.version); } }, [getItem], @@ -219,13 +218,17 @@ export default () => { Notification.error({ message: t("Failed to update item.") }); return; } else { - const metadata = itemIdToMetadata.current.get(updateItemId) ?? target.metadata; - if (metadata?.fields && metadata.id) { + const metadata = target.metadata; + const version = itemIdToMetadataVersion.current.get(updateItemId) ?? metadata?.version; + if (metadata?.fields && metadata.id && version) { + const requiredErrorFields: string[] = []; + const maxLengthErrorFields: string[] = []; const fields = metadata.fields.map(field => { + const metaField = metaFieldsMap.get(field.schemaFieldId); if (field.schemaFieldId === key) { if (Array.isArray(field.value)) { if (field.type === "Tag") { - const tags = metaFieldsMap.get(key)?.typeProperty?.tags; + const tags = metaField?.typeProperty?.tags; field.value = tags ? selectedTagIdsGet(value as string[], tags) : []; } else { field.value[index ?? 0] = value === "" ? undefined : value; @@ -236,13 +239,44 @@ export default () => { } else { field.value = field.value ?? ""; } + const fieldValue = field.value; + if (metaField?.required) { + // use checkIfEmpty + if (Array.isArray(fieldValue)) { + if (fieldValue.every(v => v === undefined || v === null || v === "")) { + requiredErrorFields.push(metaField.key); + } + } else if (fieldValue === undefined || fieldValue === null || fieldValue === "") { + requiredErrorFields.push(metaField.key); + } + } + const maxLength = metaField?.typeProperty?.maxLength; + if (maxLength) { + if (Array.isArray(fieldValue)) { + if (fieldValue.some(v => v.length > maxLength)) { + maxLengthErrorFields.push(metaField.key); + } + } else if (fieldValue.length > maxLength) { + maxLengthErrorFields.push(metaField.key); + } + } + return field as ItemFieldInput; }); + if (requiredErrorFields.length || maxLengthErrorFields.length) { + requiredErrorFields.forEach(field => { + Notification.error({ message: t("Required field error", { field }) }); + }); + maxLengthErrorFields.forEach(field => { + Notification.error({ message: t("Maximum length error", { field }) }); + }); + return; + } const item = await updateItemMutation({ variables: { itemId: metadata.id, fields, - version: metadata.version, + version, }, }); if (item.errors || !item.data?.updateItem) { diff --git a/web/src/i18n/translations/en.yml b/web/src/i18n/translations/en.yml index d1b8f95700..6d4b5e5f4c 100644 --- a/web/src/i18n/translations/en.yml +++ b/web/src/i18n/translations/en.yml @@ -485,11 +485,12 @@ Failed to update item.: '' Successfully updated Item!: '' Failed to create request.: '' Successfully created request!: '' -Failed to update request.: '' -Successfully updated request!: '' +Required field error: '{{field}} field is required!' +Maximum length error: Character count in {{field}} field exceeds the maximum number! Failed to delete one or more items.: '' One or more items were successfully deleted!: '' One of the items already exists in the request.: '' +Failed to update request.: '' Successfully updated Request!: '' Failed to publish items.: '' Successfully published items!: '' @@ -517,6 +518,7 @@ Failed to delete model.: '' Successfully deleted model!: '' Failed to update model.: '' Successfully updated model!: '' +Successfully updated request!: '' Failed to delete one or more requests.: '' One or more requests were successfully closed!: '' Failed to approve request.: '' diff --git a/web/src/i18n/translations/ja.yml b/web/src/i18n/translations/ja.yml index 1e3942fbd5..2c0e13ddc0 100644 --- a/web/src/i18n/translations/ja.yml +++ b/web/src/i18n/translations/ja.yml @@ -485,11 +485,12 @@ Failed to update item.: アイテムの更新に失敗しました。 Successfully updated Item!: アイテムの更新に成功しました。 Failed to create request.: リクエストの作成に失敗しました。 Successfully created request!: リクエストの作成に成功しました。 -Failed to update request.: リクエストの更新に失敗しました。 -Successfully updated request!: リクエストの更新に成功しました。 +Required field error: '{{field}}フィールドは必須項目です!' +Maximum length error: '{{field}}フィールドの文字数が最大数を超えています!' Failed to delete one or more items.: アイテムの削除に失敗しました。 One or more items were successfully deleted!: アイテムの削除に成功しました。 One of the items already exists in the request.: アイテムのいずれかがすでにリクエストに存在します。 +Failed to update request.: リクエストの更新に失敗しました。 Successfully updated Request!: リクエストの更新に成功しました。 Failed to publish items.: アイテムの公開に失敗しました。 Successfully published items!: アイテムの公開に成功しました! @@ -517,6 +518,7 @@ Failed to delete model.: モデルの削除に失敗しました。 Successfully deleted model!: モデルの削除に成功しました。 Failed to update model.: モデルの更新に失敗しました。 Successfully updated model!: モデルの更新に成功しました。 +Successfully updated request!: リクエストの更新に成功しました。 Failed to delete one or more requests.: リクエストの削除に失敗しました。 One or more requests were successfully closed!: リクエストのクローズに成功しました。 Failed to approve request.: リクエストの承認に失敗しました。 From f2f7f1a5de5f62a6184033a7333d86a3cb0802f6 Mon Sep 17 00:00:00 2001 From: caichi Date: Wed, 4 Dec 2024 16:12:05 +0900 Subject: [PATCH 07/10] fix: e2e test --- web/e2e/common/notification.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/e2e/common/notification.ts b/web/e2e/common/notification.ts index b70d4cd610..0a7d15e151 100644 --- a/web/e2e/common/notification.ts +++ b/web/e2e/common/notification.ts @@ -3,8 +3,8 @@ import { Page } from "@playwright/test"; import { expect } from "@reearth-cms/e2e/utils"; export async function closeNotification(page: Page, isSuccess = true) { - const text = isSuccess ? /successfully|成功/i : "input: "; - await expect(page.getByRole("alert").last()).toContainText(text); + const text = isSuccess ? "check-circle" : "close-circle"; + await expect(page.getByRole("alert").last().getByRole("img")).toHaveAttribute("aria-label", text); await page .locator(".ant-notification-notice") .last() From a2f5b8012d80a0e78fe53941ce9de4a112f637e4 Mon Sep 17 00:00:00 2001 From: caichi Date: Thu, 5 Dec 2024 13:33:04 +0900 Subject: [PATCH 08/10] fix: use checkIfEmpty --- .../molecules/Content/RenderField/ItemFormat.tsx | 16 +++++++++++++--- .../Project/Content/ContentList/hooks.ts | 10 +++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/web/src/components/molecules/Content/RenderField/ItemFormat.tsx b/web/src/components/molecules/Content/RenderField/ItemFormat.tsx index 5e05db0817..a392aa9973 100644 --- a/web/src/components/molecules/Content/RenderField/ItemFormat.tsx +++ b/web/src/components/molecules/Content/RenderField/ItemFormat.tsx @@ -31,6 +31,18 @@ export const ItemFormat: React.FC = ({ item, field, update, index }) => { const [isEditable, setIsEditable] = useState(false); const [itemState, setItemState] = useState(item); + const handleTextBlur = useCallback( + (e: FocusEvent) => { + const value = e.target.value; + if (itemState === value) { + return; + } + update?.(value, index); + setItemState(value); + }, + [index, itemState, update], + ); + const handleUrlBlur = useCallback( (e: FocusEvent) => { const value = e.target.value; @@ -56,9 +68,7 @@ export const ItemFormat: React.FC = ({ item, field, update, index }) => { maxLength={field.typeProperty?.maxLength} defaultValue={item} placeholder="-" - onBlur={e => { - update(e.target.value, index); - }} + onBlur={handleTextBlur} /> ) : ( item diff --git a/web/src/components/organisms/Project/Content/ContentList/hooks.ts b/web/src/components/organisms/Project/Content/ContentList/hooks.ts index 6dba39d611..8cb6edad43 100644 --- a/web/src/components/organisms/Project/Content/ContentList/hooks.ts +++ b/web/src/components/organisms/Project/Content/ContentList/hooks.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState, useRef, Key } from "react"; import { useNavigate, useParams, useLocation } from "react-router-dom"; import Notification from "@reearth-cms/components/atoms/Notification"; +import { checkIfEmpty } from "@reearth-cms/components/molecules/Content/Form/fields/utils"; import { renderField } from "@reearth-cms/components/molecules/Content/RenderField"; import { renderTitle } from "@reearth-cms/components/molecules/Content/RenderTitle"; import { ExtendedColumns } from "@reearth-cms/components/molecules/Content/Table/types"; @@ -241,22 +242,21 @@ export default () => { } const fieldValue = field.value; if (metaField?.required) { - // use checkIfEmpty if (Array.isArray(fieldValue)) { - if (fieldValue.every(v => v === undefined || v === null || v === "")) { + if (fieldValue.every(v => checkIfEmpty(v))) { requiredErrorFields.push(metaField.key); } - } else if (fieldValue === undefined || fieldValue === null || fieldValue === "") { + } else if (checkIfEmpty(fieldValue)) { requiredErrorFields.push(metaField.key); } } const maxLength = metaField?.typeProperty?.maxLength; if (maxLength) { if (Array.isArray(fieldValue)) { - if (fieldValue.some(v => v.length > maxLength)) { + if (fieldValue.some(v => typeof v === "string" && v.length > maxLength)) { maxLengthErrorFields.push(metaField.key); } - } else if (fieldValue.length > maxLength) { + } else if (typeof fieldValue === "string" && fieldValue.length > maxLength) { maxLengthErrorFields.push(metaField.key); } } From d1405649d366646fe52cf8ea18c3d68d27d3aa83 Mon Sep 17 00:00:00 2001 From: caichi Date: Thu, 5 Dec 2024 14:43:42 +0900 Subject: [PATCH 09/10] revert change --- .../organisms/Project/Content/ContentList/hooks.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/components/organisms/Project/Content/ContentList/hooks.ts b/web/src/components/organisms/Project/Content/ContentList/hooks.ts index 8cb6edad43..4dab3a52f5 100644 --- a/web/src/components/organisms/Project/Content/ContentList/hooks.ts +++ b/web/src/components/organisms/Project/Content/ContentList/hooks.ts @@ -11,6 +11,7 @@ import { Item, ItemStatus, ItemField, + Metadata, } from "@reearth-cms/components/molecules/Content/types"; import { selectedTagIdsGet } from "@reearth-cms/components/molecules/Content/utils"; import { Request, RequestItem } from "@reearth-cms/components/molecules/Request/types"; @@ -190,13 +191,13 @@ export default () => { const [getItem] = useGetItemLazyQuery({ fetchPolicy: "no-cache" }); const [createNewItem] = useCreateItemMutation(); - const itemIdToMetadataVersion = useRef(new Map()); + const itemIdToMetadata = useRef(new Map()); const metadataVersionSet = useCallback( async (id: string) => { const { data } = await getItem({ variables: { id } }); const item = fromGraphQLItem(data?.node as GQLItem); if (item) { - itemIdToMetadataVersion.current.set(id, item.metadata.version); + itemIdToMetadata.current.set(id, item.metadata); } }, [getItem], @@ -219,9 +220,8 @@ export default () => { Notification.error({ message: t("Failed to update item.") }); return; } else { - const metadata = target.metadata; - const version = itemIdToMetadataVersion.current.get(updateItemId) ?? metadata?.version; - if (metadata?.fields && metadata.id && version) { + const metadata = itemIdToMetadata.current.get(updateItemId) ?? target.metadata; + if (metadata?.fields && metadata.id) { const requiredErrorFields: string[] = []; const maxLengthErrorFields: string[] = []; const fields = metadata.fields.map(field => { @@ -276,7 +276,7 @@ export default () => { variables: { itemId: metadata.id, fields, - version, + version: metadata.version, }, }); if (item.errors || !item.data?.updateItem) { From 42d392e0525e2702c315bd735e4ebb91310ea43e Mon Sep 17 00:00:00 2001 From: caichi Date: Thu, 5 Dec 2024 14:45:14 +0900 Subject: [PATCH 10/10] fix: test --- web/e2e/common/notification.ts | 1 + web/e2e/project/item/metadata/tag.spec.ts | 2 +- web/e2e/project/item/metadata/update.spec.ts | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/e2e/common/notification.ts b/web/e2e/common/notification.ts index 0a7d15e151..9ff38b8a64 100644 --- a/web/e2e/common/notification.ts +++ b/web/e2e/common/notification.ts @@ -10,4 +10,5 @@ export async function closeNotification(page: Page, isSuccess = true) { .last() .locator(".ant-notification-notice-close") .click(); + await expect(page.getByRole("alert").last()).toBeHidden(); } diff --git a/web/e2e/project/item/metadata/tag.spec.ts b/web/e2e/project/item/metadata/tag.spec.ts index 07ff943b8b..53922b6587 100644 --- a/web/e2e/project/item/metadata/tag.spec.ts +++ b/web/e2e/project/item/metadata/tag.spec.ts @@ -73,13 +73,13 @@ test("Tag metadata creating and updating has succeeded", async ({ page }) => { await expect(page.getByText("Tag1", { exact: true })).toBeVisible(); await page.getByRole("cell").getByLabel("edit").locator("svg").click(); await page.getByLabel("close-circle").locator("svg").click(); + await closeNotification(page); await page.getByLabel("tag1").click(); await page .locator("div") .filter({ hasText: /^Tag2$/ }) .nth(1) .click(); - await page.getByText("tag1", { exact: true }).click(); await closeNotification(page); await expect(page.locator("#root").getByText("Tag2")).toBeVisible(); await page.getByLabel("Back").click(); diff --git a/web/e2e/project/item/metadata/update.spec.ts b/web/e2e/project/item/metadata/update.spec.ts index b901bd867f..859c9eca1c 100644 --- a/web/e2e/project/item/metadata/update.spec.ts +++ b/web/e2e/project/item/metadata/update.spec.ts @@ -30,8 +30,10 @@ test("Updating metadata added later from table has succeeded", async ({ page }) await closeNotification(page); await page.getByRole("switch").click(); await closeNotification(page); + await page.getByRole("switch").click(); + await closeNotification(page); await page.getByRole("cell").getByLabel("edit").locator("svg").click(); - await expect(page.getByLabel("boolean")).toHaveAttribute("aria-checked", "false"); + await expect(page.getByLabel("boolean")).toHaveAttribute("aria-checked", "true"); }); test("Updating metadata added later from edit page has succeeded", async ({ page }) => {