Repository: reearth/reearth-visualizer
Pull request #1591: fix(web): skip project mutation update when no change [VIZ-1523]
Author: login: mkumbobeaty
Labels: name: web
Created: 2025-05-08T07:57:49Z | Merged: 2025-05-12T03:12:14Z

# Overview
- Skip update mutation when there’s no change been made in project settings
## What I've done

## What I haven't done

## How I tested

## Which point I want you to review particularly

## Memo


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Input, textarea, and number fields now support an "onChangeComplete" event, allowing updates to trigger immediately after editing is finished rather than only on blur.

- **Improvements**
  - Streamlined form interactions by removing unnecessary local state in several settings and editor panels.
  - Enhanced input responsiveness and reduced redundant updates in asset, color, and number fields.
  - Improved consistency in when updates are applied across project, workspace, and editor settings.

- **Bug Fixes**
  - Prevented redundant or unnecessary update calls when field values have not changed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/DataSource/index.tsx b/web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/DataSource/index.tsx
index 1f2e761d53..87374dcb9c 100644
--- a/web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/DataSource/index.tsx
+++ b/web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/DataSource/index.tsx
@@ -25,13 +25,8 @@ const DataSource: FC<Props> = ({
 }) => {
   const t = useT();
   const theme = useTheme();
-  const [localTitle, setLocalTitle] = useState(selectedLayer.title);
   const [localUrl, setLocalUrl] = useState(selectedLayer.config?.data?.url);
 
-  useEffect(() => {
-    setLocalTitle(selectedLayer.title);
-  }, [selectedLayer.title]);
-
   useEffect(() => {
     setLocalUrl(selectedLayer.config?.data?.url);
   }, [selectedLayer.config?.data?.url]);
@@ -68,11 +63,12 @@ const DataSource: FC<Props> = ({
     [saveCollapseState]
   );
 
-  const handleTitleUpdate = useCallback(() => {
-    if (!localTitle || localTitle === selectedLayer.title) {
-      setLocalTitle(selectedLayer.title);
-    } else onLayerNameUpdate?.({ layerId: selectedLayer.id, name: localTitle });
-  }, [localTitle, onLayerNameUpdate, selectedLayer.id, selectedLayer.title]);
+  const handleTitleUpdate = useCallback(
+    (name: string) => {
+      onLayerNameUpdate?.({ layerId: selectedLayer.id, name });
+    },
+    [onLayerNameUpdate, selectedLayer.id]
+  );
 
   const handleLayerUrlUpdate = useCallback(
     (url: string) => {
@@ -102,9 +98,8 @@ const DataSource: FC<Props> = ({
         <InputWrapper>
           <InputField
             title={t("Layer Name")}
-            value={localTitle}
-            onChange={setLocalTitle}
-            onBlur={handleTitleUpdate}
+            value={selectedLayer.title}
+            onChangeComplete={handleTitleUpdate}
           />
           <InputField
             title={t("Format")}
diff --git a/web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/FeatureInspector/CustomPropertField.tsx b/web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/FeatureInspector/CustomPropertField.tsx
index c10b84947b..4896f15245 100644
--- a/web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/FeatureInspector/CustomPropertField.tsx
+++ b/web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/FeatureInspector/CustomPropertField.tsx
@@ -46,7 +46,7 @@ export const FieldComponent = ({ field, editMode, setFields }: Props) => {
       key={field?.id}
       title={field?.title}
       value={field.value as string}
-      onBlur={handleChange}
+      onChangeComplete={handleChange}
       disabled={!editMode}
       appearance={appearance}
     />
@@ -56,7 +56,7 @@ export const FieldComponent = ({ field, editMode, setFields }: Props) => {
       title={field?.title}
       value={field.value as string}
       resizable="height"
-      onBlur={handleChange}
+      onChangeComplete={handleChange}
       disabled={!editMode}
       appearance={appearance}
     />
@@ -85,7 +85,7 @@ export const FieldComponent = ({ field, editMode, setFields }: Props) => {
       key={field?.id}
       title={field?.title}
       value={field.value as number}
-      onBlur={handleChange}
+      onChangeComplete={handleChange}
       disabled={!editMode}
       appearance={appearance}
     />
diff --git a/web/src/beta/features/Editor/Widgets/ContainerSettingsPanel/index.tsx b/web/src/beta/features/Editor/Widgets/ContainerSettingsPanel/index.tsx
index 9819d0503b..39be2d5f55 100644
--- a/web/src/beta/features/Editor/Widgets/ContainerSettingsPanel/index.tsx
+++ b/web/src/beta/features/Editor/Widgets/ContainerSettingsPanel/index.tsx
@@ -34,7 +34,7 @@ const ContainerSettingsPanel: FC = () => {
         <NumberField
           title={t("Padding top")}
           value={widgetArea?.padding?.top.toString()}
-          onBlur={(newVal) => {
+          onChangeComplete={(newVal) => {
             handleWidgetAreaStateChange({
               ...widgetArea,
               padding: {
@@ -47,7 +47,7 @@ const ContainerSettingsPanel: FC = () => {
         <NumberField
           title={t("Padding right")}
           value={widgetArea?.padding?.right.toString()}
-          onBlur={(newVal) => {
+          onChangeComplete={(newVal) => {
             handleWidgetAreaStateChange({
               ...widgetArea,
               padding: {
@@ -60,7 +60,7 @@ const ContainerSettingsPanel: FC = () => {
         <NumberField
           title={t("Padding bottom")}
           value={widgetArea?.padding?.bottom.toString()}
-          onBlur={(newVal) => {
+          onChangeComplete={(newVal) => {
             handleWidgetAreaStateChange({
               ...widgetArea,
               padding: {
@@ -73,7 +73,7 @@ const ContainerSettingsPanel: FC = () => {
         <NumberField
           title={t("Padding left")}
           value={widgetArea?.padding?.left.toString()}
-          onBlur={(newVal) => {
+          onChangeComplete={(newVal) => {
             handleWidgetAreaStateChange({
               ...widgetArea,
               padding: {
@@ -87,7 +87,7 @@ const ContainerSettingsPanel: FC = () => {
         <NumberField
           title={t("Gap spacing")}
           value={(widgetArea?.gap ?? 6).toString()}
-          onBlur={(newVal) => {
+          onChangeComplete={(newVal) => {
             handleWidgetAreaStateChange({
               ...widgetArea,
               gap: Number(newVal)
diff --git a/web/src/beta/features/PluginPlayground/ExtensionSettings/PropertyItem.tsx b/web/src/beta/features/PluginPlayground/ExtensionSettings/PropertyItem.tsx
index 9e9ffe909d..de77dd58f5 100644
--- a/web/src/beta/features/PluginPlayground/ExtensionSettings/PropertyItem.tsx
+++ b/web/src/beta/features/PluginPlayground/ExtensionSettings/PropertyItem.tsx
@@ -91,7 +91,7 @@ const PropertyItem: FC<Props> = ({ id, value, field, onUpdate }) => {
             resizable="height"
             description={field.description}
             value={(value as string) ?? ""}
-            onBlur={handleChange}
+            onChangeComplete={handleChange}
           />
         ) : (
           <InputField
@@ -99,7 +99,7 @@ const PropertyItem: FC<Props> = ({ id, value, field, onUpdate }) => {
             title={field.title}
             value={value as string}
             description={field.description}
-            onBlur={handleChange}
+            onChangeComplete={handleChange}
           />
         )
       ) : field.type === "url" ? (
@@ -152,7 +152,7 @@ const PropertyItem: FC<Props> = ({ id, value, field, onUpdate }) => {
             description={field.description}
             min={field.min}
             max={field.max}
-            onBlur={handleChange}
+            onChangeComplete={handleChange}
           />
         )
       ) : field.type === "latlng" ? (
diff --git a/web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx b/web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx
index a262756b72..118b15f7f5 100644
--- a/web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx
+++ b/web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx
@@ -46,23 +46,40 @@ const GeneralSettings: FC<Props> = ({
 }) => {
   const t = useT();
 
-  const [localName, setLocalName] = useState(project?.name ?? "");
-  const [localDescription, setLocalDescription] = useState(
-    project?.description ?? ""
+  const handleNameUpdate = useCallback(
+    (name: string) => {
+      if (!project) return;
+      onUpdateProject({
+        imageUrl: project.imageUrl || "",
+        description: project.description,
+        name
+      });
+    },
+    [project, onUpdateProject]
   );
-  const [localImageUrl, setLocalImageUrl] = useState<string | undefined>(
-    project?.imageUrl ?? ""
+
+  const handleDescriptionUpdate = useCallback(
+    (description: string) => {
+      if (!project) return;
+      onUpdateProject({
+        name: project.name,
+        imageUrl: project.imageUrl || "",
+        description
+      });
+    },
+    [project, onUpdateProject]
   );
 
-  const handleSubmit = useCallback(
+  const handleImageUpdate = useCallback(
     (imageUrl?: string) => {
+      if (!project) return;
       onUpdateProject({
-        name: localName,
-        description: localDescription,
+        name: project.name,
+        description: project.description,
         imageUrl
       });
     },
-    [localName, localDescription, onUpdateProject]
+    [project, onUpdateProject]
   );
 
   const [projectRemoveModalVisible, setProjectRemoveModalVisible] =
@@ -72,14 +89,6 @@ const GeneralSettings: FC<Props> = ({
     setProjectRemoveModalVisible(value);
   }, []);
 
-  const handleImageChange = useCallback(
-    (value?: string) => {
-      setLocalImageUrl(value);
-      handleSubmit(value);
-    },
-    [handleSubmit]
-  );
-
   return project ? (
     <InnerPage wide>
       <SettingsWrapper>
@@ -93,15 +102,13 @@ const GeneralSettings: FC<Props> = ({
             <InputField
               title={t("Project Name")}
               value={project.name}
-              onChange={(name) => setLocalName(name)}
-              onBlur={handleSubmit}
+              onChangeComplete={handleNameUpdate}
             />
             <TextareaField
               title={t("Description")}
-              value={localDescription}
+              value={project.description}
               resizable="height"
-              onChange={setLocalDescription}
-              onBlur={handleSubmit}
+              onChangeComplete={handleDescriptionUpdate}
             />
             <SettingsRow>
               <SettingsRowItem>
@@ -109,13 +116,13 @@ const GeneralSettings: FC<Props> = ({
                   title={t("Thumbnail")}
                   inputMethod="asset"
                   assetsTypes={IMAGE_TYPES}
-                  value={localImageUrl}
-                  onChange={(v) => handleImageChange(v)}
+                  value={project.imageUrl || ""}
+                  onChange={handleImageUpdate}
                 />
               </SettingsRowItem>
               <SettingsRowItem>
                 <Thumbnail
-                  src={localImageUrl || defaultProjectBackgroundImage}
+                  src={project.imageUrl || defaultProjectBackgroundImage}
                 />
               </SettingsRowItem>
             </SettingsRow>
diff --git a/web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx b/web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx
index 4aaeb13bda..fa7aefaf44 100644
--- a/web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx
+++ b/web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx
@@ -70,6 +70,7 @@ const PublicSettingsDetail: React.FC<Props> = ({
     publicDescription: settingsItem.publicDescription,
     publicImage: settingsItem.publicImage
   });
+
   const handleSubmitPublicInfo = useCallback(
     (publicImage?: string) => {
       onUpdate({
@@ -103,7 +104,7 @@ const PublicSettingsDetail: React.FC<Props> = ({
         trackingId: localGA.trackingId
       });
     }
-  }, [localGA, onUpdateGA]);
+  }, [localGA.enableGa, localGA.trackingId, onUpdateGA]);
 
   const extensions = window.REEARTH_CONFIG?.extensions?.publication;
   const [accessToken, setAccessToken] = useState<string>();
@@ -165,11 +166,11 @@ const PublicSettingsDetail: React.FC<Props> = ({
           onChange={(publicTitle: string) => {
             setLocalPublicInfo((s) => ({ ...s, publicTitle }));
           }}
-          onBlur={handleSubmitPublicInfo}
+          onChangeComplete={handleSubmitPublicInfo}
         />
         <TextAreaField
           title={t("Description")}
-          value={localPublicInfo.publicDescription ?? ""}
+          value={settingsItem.publicDescription ?? ""}
           placeholder={t("Write down your text")}
           description={t(
             "The Description setting will be applied to og:description."
@@ -178,7 +179,7 @@ const PublicSettingsDetail: React.FC<Props> = ({
           onChange={(publicDescription: string) => {
             setLocalPublicInfo((s) => ({ ...s, publicDescription }));
           }}
-          onBlur={handleSubmitPublicInfo}
+          onChangeComplete={handleSubmitPublicInfo}
         />
         <ThumbnailField>
           <AssetField
@@ -290,7 +291,7 @@ const PublicSettingsDetail: React.FC<Props> = ({
                 setBasicAuthorization((s) => ({ ...s, basicAuthUsername }));
               }}
               disabled={!localBasicAuthorization.isBasicAuthActive}
-              onBlur={handleSubmitBasicAuthorization}
+              onChangeComplete={handleSubmitBasicAuthorization}
             />
             <InputField
               title={t("Password")}
@@ -299,7 +300,7 @@ const PublicSettingsDetail: React.FC<Props> = ({
                 setBasicAuthorization((s) => ({ ...s, basicAuthPassword }));
               }}
               disabled={!localBasicAuthorization.isBasicAuthActive}
-              onBlur={handleSubmitBasicAuthorization}
+              onChangeComplete={handleSubmitBasicAuthorization}
             />
           </>
         )}
@@ -322,7 +323,7 @@ const PublicSettingsDetail: React.FC<Props> = ({
             onChange={(trackingId: string) => {
               setLocalGA((s) => ({ ...s, trackingId }));
             }}
-            onBlur={handleSubmitGA}
+            onChangeComplete={handleSubmitGA}
           />
         )}
       </SettingsFields>
diff --git a/web/src/beta/features/ProjectSettings/innerPages/StorySettings/StorySettingsDetail.tsx b/web/src/beta/features/ProjectSettings/innerPages/StorySettings/StorySettingsDetail.tsx
index 60e2a92d4d..b02650b260 100644
--- a/web/src/beta/features/ProjectSettings/innerPages/StorySettings/StorySettingsDetail.tsx
+++ b/web/src/beta/features/ProjectSettings/innerPages/StorySettings/StorySettingsDetail.tsx
@@ -26,10 +26,10 @@ const StorySettingsDetail: React.FC<Props> = ({
   );
   const handleColorChange = useCallback(
     (bgColor?: string) => {
-      setBackgroundColor(bgColor);
       onUpdateStory({
         bgColor
       });
+      setBackgroundColor(bgColor);
     },
     [onUpdateStory]
   );
diff --git a/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Camera/Editor/index.tsx b/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Camera/Editor/index.tsx
index 747decf301..e5131efcdd 100644
--- a/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Camera/Editor/index.tsx
+++ b/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Camera/Editor/index.tsx
@@ -93,7 +93,7 @@ const CameraBlockEditor: FC<Props> = ({
             title={editorProperties?.title?.title}
             description={editorProperties?.title?.description}
             value={editorProperties?.title?.value}
-            onBlur={(value) =>
+            onChangeComplete={(value) =>
               debounceOnUpdate(selected, "title", "string", value)
             }
           />
@@ -130,7 +130,7 @@ const CameraBlockEditor: FC<Props> = ({
           title={editorProperties?.cameraDuration?.title}
           description={editorProperties?.cameraDuration?.description}
           value={editorProperties?.cameraDuration?.value}
-          onBlur={(value) =>
+          onChangeComplete={(value) =>
             handleUpdate(selected, "cameraDuration", "number", value || 0)
           }
         />
diff --git a/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Layer/Editor/index.tsx b/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Layer/Editor/index.tsx
index 45a5c19cc0..be696c98e9 100644
--- a/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Layer/Editor/index.tsx
+++ b/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Layer/Editor/index.tsx
@@ -94,7 +94,7 @@ const CameraBlockEditor: FC<Props> = ({
             title={editorProperties?.title?.title}
             description={editorProperties?.title?.description}
             value={editorProperties?.title?.value}
-            onBlur={(value) =>
+            onChangeComplete={(value) =>
               debounceOnUpdate(selected, "title", "string", value)
             }
           />
diff --git a/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Link/Editor/index.tsx b/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Link/Editor/index.tsx
index 00123d7621..7752e16e7e 100644
--- a/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Link/Editor/index.tsx
+++ b/web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Link/Editor/index.tsx
@@ -89,7 +89,7 @@ const LinkBlockEditor: FC<Props> = ({
             title={editorProperties?.title?.title}
             description={editorProperties?.title?.description}
             value={editorProperties?.title?.value}
-            onBlur={(value) =>
+            onChangeComplete={(value) =>
               debounceOnUpdate(selected, "title", "string", value)
             }
           />
diff --git a/web/src/beta/features/Visualizer/shared/hooks/useFieldComponent.tsx b/web/src/beta/features/Visualizer/shared/hooks/useFieldComponent.tsx
index d19d1249ec..2b2b0b8522 100644
--- a/web/src/beta/features/Visualizer/shared/hooks/useFieldComponent.tsx
+++ b/web/src/beta/features/Visualizer/shared/hooks/useFieldComponent.tsx
@@ -249,7 +249,7 @@ export const FieldComponent = ({
         value={field?.value}
         description={field?.description}
         placeholder={field?.placeholder}
-        onBlur={handlePropertyValueUpdate(
+        onChangeComplete={handlePropertyValueUpdate(
           groupId,
           propertyId,
           fieldId,
diff --git a/web/src/beta/features/WorkspaceSetting/innerPages/Workspaces/index.tsx b/web/src/beta/features/WorkspaceSetting/innerPages/Workspaces/index.tsx
index 4f12c82794..fcab366c3b 100644
--- a/web/src/beta/features/WorkspaceSetting/innerPages/Workspaces/index.tsx
+++ b/web/src/beta/features/WorkspaceSetting/innerPages/Workspaces/index.tsx
@@ -78,7 +78,7 @@ const WorkspaceSetting: FC<Props> = ({ workspace }) => {
             onChange={setLocalWorkspaceName}
             appearance={workspace?.personal ? "readonly" : undefined}
             disabled={!!workspace?.personal || meRole !== Role.Owner}
-            onBlur={handleSubmitUpdateWorkspaceName}
+            onChangeComplete={handleSubmitUpdateWorkspaceName}
           />
         </SettingsFields>
         {!workspace?.personal && (
diff --git a/web/src/beta/ui/fields/AssetField/index.tsx b/web/src/beta/ui/fields/AssetField/index.tsx
index 190abc67b7..9f8eedfb1a 100644
--- a/web/src/beta/ui/fields/AssetField/index.tsx
+++ b/web/src/beta/ui/fields/AssetField/index.tsx
@@ -55,12 +55,12 @@ const AssetField: FC<AssetFieldProps> = ({
 
   const handleChange = useCallback(
     (url?: string, name?: string) => {
-      if (!url) {
+      if (!url && url !== value) {
         onChange?.(url, name);
         return;
       }
 
-      const extension = (url.split(".").pop() ?? "").toLowerCase();
+      const extension = (url?.split(".").pop() ?? "").toLowerCase();
       const acceptedTypes = [
         ...IMAGE_FILE_TYPES,
         ...GIS_FILE_TYPES,
@@ -77,10 +77,12 @@ const AssetField: FC<AssetFieldProps> = ({
         });
         setCurrentValue(undefined);
       } else {
-        onChange?.(url, name);
+        if (url !== value) {
+          onChange?.(url, name);
+        }
       }
     },
-    [inputMethod, onChange, setNotification, t]
+    [inputMethod, onChange, setNotification, t, value]
   );
 
   const handleInputChange = useCallback(
diff --git a/web/src/beta/ui/fields/ColorField/index.tsx b/web/src/beta/ui/fields/ColorField/index.tsx
index 80d91f792d..8b3b99d714 100644
--- a/web/src/beta/ui/fields/ColorField/index.tsx
+++ b/web/src/beta/ui/fields/ColorField/index.tsx
@@ -4,17 +4,25 @@ import CommonField, {
 } from "@reearth/beta/ui/fields/CommonField";
 import { FC } from "react";
 
-
 export type ColorInputFieldProps = CommonFieldProps & ColorInputProps;
 
 const ColorInputField: FC<ColorInputFieldProps> = ({
   title,
   description,
+  value,
+  onChange,
   ...props
 }) => {
+
+  const handleChange = (newValue: string) => {
+    if (newValue !== value) {
+      onChange?.(newValue);
+    }
+  };
+  
   return (
     <CommonField title={title} description={description}>
-      <ColorInput {...props} />
+      <ColorInput {...props} onChange={handleChange} />
     </CommonField>
   );
 };
diff --git a/web/src/beta/ui/fields/InputField/index.tsx b/web/src/beta/ui/fields/InputField/index.tsx
index ec5beebdc8..7a18ecad40 100644
--- a/web/src/beta/ui/fields/InputField/index.tsx
+++ b/web/src/beta/ui/fields/InputField/index.tsx
@@ -2,19 +2,49 @@ import { TextInput, TextInputProps } from "@reearth/beta/lib/reearth-ui";
 import CommonField, {
   CommonFieldProps
 } from "@reearth/beta/ui/fields/CommonField";
-import { FC } from "react";
+import { FC, useState, useEffect } from "react";
 
-
-export type InputFieldProps = CommonFieldProps &
+export type InputFieldProps = {
+  onChangeComplete?: (text: string) => void;
+} & CommonFieldProps &
   Pick<
     TextInputProps,
-    "value" | "placeholder" | "onChange" | "onBlur" | "disabled" | "appearance"
+    "value" | "placeholder" | "onChange" | "disabled" | "appearance"
   >;
 
-const InputField: FC<InputFieldProps> = ({ title, description, ...props }) => {
+const InputField: FC<InputFieldProps> = ({
+  title,
+  description,
+  value,
+  onChange,
+  onChangeComplete,
+  ...props
+}) => {
+  const [internalValue, setInternalValue] = useState(value);
+
+  useEffect(() => {
+    setInternalValue(value);
+  }, [value]);
+
+  const handleChangeComplete = () => {
+    if (internalValue !== value) {
+      onChangeComplete?.(internalValue as string);
+    }
+  };
+
+  const handleChange = (newValue: string) => {
+    setInternalValue(newValue);
+    onChange?.(newValue);
+  };
+
   return (
     <CommonField title={title} description={description}>
-      <TextInput {...props} />
+      <TextInput
+        {...props}
+        value={internalValue}
+        onChange={handleChange}
+        onBlur={handleChangeComplete}
+      />
     </CommonField>
   );
 };
diff --git a/web/src/beta/ui/fields/NumberField/index.tsx b/web/src/beta/ui/fields/NumberField/index.tsx
index 3abba5c9e1..c7cba2de9c 100644
--- a/web/src/beta/ui/fields/NumberField/index.tsx
+++ b/web/src/beta/ui/fields/NumberField/index.tsx
@@ -2,18 +2,44 @@ import { NumberInput, NumberInputProps } from "@reearth/beta/lib/reearth-ui";
 import CommonField, {
   CommonFieldProps
 } from "@reearth/beta/ui/fields/CommonField";
-import { FC } from "react";
+import { FC, useEffect, useState } from "react";
 
-
-export type NumberFieldProps = CommonFieldProps & NumberInputProps;
+export type NumberFieldProps = {
+  onChangeComplete?: (value?: number) => void;
+} & CommonFieldProps &
+  NumberInputProps;
 const NumberField: FC<NumberFieldProps> = ({
   title,
   description,
+  value,
+  onChangeComplete,
+  onChange,
   ...props
 }) => {
+  const [currentValue, setCurrentValue] = useState(value);
+
+  useEffect(() => {
+    setCurrentValue(value);
+  }, [value]);
+
+  const handleChangeComplete = () => {
+    if (currentValue !== value) {
+      onChangeComplete?.(currentValue as number);
+    }
+  };
+
+  const handleChange = (newValue?: number) => {
+    setCurrentValue(newValue);
+    onChange?.(newValue);
+  };
   return (
     <CommonField title={title} description={description}>
-      <NumberInput {...props} />
+      <NumberInput
+        {...props}
+        value={currentValue}
+        onBlur={handleChangeComplete}
+        onChange={handleChange}
+      />
     </CommonField>
   );
 };
diff --git a/web/src/beta/ui/fields/Properties/PropertyField.tsx b/web/src/beta/ui/fields/Properties/PropertyField.tsx
index 44c6acccdd..2dce5937d6 100644
--- a/web/src/beta/ui/fields/Properties/PropertyField.tsx
+++ b/web/src/beta/ui/fields/Properties/PropertyField.tsx
@@ -103,7 +103,7 @@ const PropertyField: FC<Props> = ({
             resizable="height"
             value={(value as string) ?? ""}
             description={schema.description}
-            onBlur={handleChange}
+            onChangeComplete={handleChange}
           />
         ) : (
           <InputField
@@ -112,7 +112,7 @@ const PropertyField: FC<Props> = ({
             value={(value as string) ?? ""}
             description={schema.description}
             placeholder={schema.placeholder}
-            onBlur={handleChange}
+            onChangeComplete={handleChange}
           />
         )
       ) : schema.type === "url" ? (
@@ -165,7 +165,7 @@ const PropertyField: FC<Props> = ({
             min={schema.min}
             max={schema.max}
             description={schema.description}
-            onBlur={handleChange}
+            onChangeComplete={handleChange}
           />
         )
       ) : schema.type === "latlng" ? (
diff --git a/web/src/beta/ui/fields/TextareaField/index.tsx b/web/src/beta/ui/fields/TextareaField/index.tsx
index 1e5916b157..aa972490a5 100644
--- a/web/src/beta/ui/fields/TextareaField/index.tsx
+++ b/web/src/beta/ui/fields/TextareaField/index.tsx
@@ -2,19 +2,46 @@ import { TextArea, TextAreaProps } from "@reearth/beta/lib/reearth-ui";
 import CommonField, {
   CommonFieldProps
 } from "@reearth/beta/ui/fields/CommonField";
-import { FC } from "react";
+import { FC, useEffect, useState } from "react";
 
-
-export type TextAreaFieldProps = CommonFieldProps & TextAreaProps;
+export type TextAreaFieldProps = {
+  onChangeComplete?: (text: string) => void;
+} & CommonFieldProps &
+  TextAreaProps;
 
 const TextAreaField: FC<TextAreaFieldProps> = ({
   title,
   description,
+  value,
+  onChangeComplete,
+  onChange,
   ...props
 }) => {
+  const [internalValue, setInternalValue] = useState(value);
+
+  useEffect(() => {
+    setInternalValue(value);
+  }, [value]);
+
+  const handleChangeComplete = () => {
+    if (internalValue !== value) {
+      onChangeComplete?.(internalValue as string);
+    }
+  };
+
+  const handleChange = (newValue: string) => {
+    setInternalValue(newValue);
+    onChange?.(newValue);
+  };
+
   return (
     <CommonField title={title} description={description}>
-      <TextArea {...props} />
+      <TextArea
+        {...props}
+        value={internalValue}
+        onChange={handleChange}
+        onBlur={handleChangeComplete}
+      />
     </CommonField>
   );
 };


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>web/src/beta/features/ProjectSettings/innerPages/StorySettings/StorySettingsDetail.tsx (1)</summary><blockquote>

`27-36`: **Great optimization for conditional updates!**

The code now skips unnecessary background color updates when the new value is the same as the existing one. This aligns with the PR objective of avoiding unnecessary mutations.

However, I noticed that `handlePositionChange` (lines 50-58) still updates unconditionally. For consistency, consider applying the same conditional pattern there too.


```diff
const handlePositionChange = useCallback(
  (panelPosition?: Position) => {
    setLocalPanelPosition(panelPosition as Position);
+   if (settingsItem.panelPosition !== panelPosition) {
      onUpdateStory({
        panelPosition
      });
+   }
  },
- [onUpdateStory]
+ [onUpdateStory, settingsItem.panelPosition]
);
```

</blockquote></details>
<details>
<summary>web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx (1)</summary><blockquote>

`57-81`: **Effective implementation of conditional updates**

The refactoring to only trigger updates when values have changed is well-implemented. The dependency array is properly updated to include all the relevant fields.

One minor observation: the update payload always includes all three fields (name, description, imageUrl) even when only one has changed. While this approach is consistent, it sends potentially unchanged data to the server.


Consider sending only the changed field to minimize payload size:

```diff
if (isUpdated) {
  onUpdateProject({
-   name: localName,
-   description: localDescription,
-   imageUrl: localImageUrl
+   ...(field === "name" ? { name: localName } : {}),
+   ...(field === "description" ? { description: localDescription } : {}),
+   ...(field === "imageUrl" ? { imageUrl: localImageUrl } : {})
  });
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3e84d89c4baed7168198f90bb67dd0d3dcfd412f and 9ee44e01eca77fb537e636f2bcd5558d2c8c4583.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx` (3 hunks)
* `web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx` (7 hunks)
* `web/src/beta/features/ProjectSettings/innerPages/StorySettings/StorySettingsDetail.tsx` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx (1)</summary><blockquote>

<details>
<summary>web/src/beta/features/ProjectSettings/innerPages/PublicSettings/index.tsx (1)</summary>

* `PublicGASettingsType` (25-28)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: ci-web / ci
* GitHub Check: Redirect rules - reearth-web
* GitHub Check: Header rules - reearth-web
* GitHub Check: Pages changed - reearth-web

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx (3)</summary>

`91-97`: **Update handler correctly modified**

The image change handler now properly passes the field parameter to `handleSubmit`.

---

`109-114`: **Consistent field-based update triggering**

Input field now correctly triggers updates with the field identifier.

---

`115-121`: **Consistent field-based update triggering**

TextareaField now correctly triggers updates with the field identifier.

</details>
<details>
<summary>web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx (8)</summary>

`73-95`: **Well-implemented conditional update logic**

The refactored `handleSubmitPublicInfo` function now intelligently checks if the field has actually changed before triggering an update. The dependency array has been properly updated to include all the fields used in the function.

---

`102-122`: **Conditional update logic properly implemented**

The basic authorization submission handler now checks if fields have actually changed before triggering an update.

---

`203-203`: **Field identifier correctly passed to handler**

The title input field now correctly passes the field identifier to the submission handler.

---

`216-216`: **Field identifier correctly passed to handler**

The description textarea field now correctly passes the field identifier to the submission handler.

---

`233-233`: **Field identifier correctly passed to handler**

The image field now correctly passes the field identifier to the submission handler.

---

`323-325`: **Field identifier correctly passed to handler**

The username input field now correctly passes the field identifier to the submission handler.

---

`332-334`: **Field identifier correctly passed to handler**

The password input field now correctly passes the field identifier to the submission handler.

---

`272-290`: **Code formatting improvement**

The formatting changes in this section improve code readability without changing functionality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:01:37Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>web/src/beta/ui/fields/NumberField/index.tsx (2)</summary><blockquote>

`16-16`: **Consider adding explicit type for useState**

The currentValue state lacks explicit typing, which might lead to type safety issues. Consider adding a type parameter to useState:

```diff
-  const [currentValue, setCurrentValue] = useState(value);
+  const [currentValue, setCurrentValue] = useState<number | undefined>(value);
```

---

`28-31`: **Consider using useCallback for memoization**

The handler functions could benefit from being memoized with useCallback to prevent unnecessary re-renders:

```diff
-  const handleChange = (newValue?: number) => {
+  const handleChange = useCallback((newValue?: number) => {
     setCurrentValue(newValue);
     onChange?.(newValue);
-  };
+  }, [onChange]);
```

Similarly for the handleBlur function.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 24de5f95bf81df523c901eb473cc59ab1dba71a2 and e193594a9f5b041d162aad2d29a2c6139946ee9b.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `web/src/beta/ui/fields/InputField/index.tsx` (1 hunks)
* `web/src/beta/ui/fields/NumberField/index.tsx` (1 hunks)
* `web/src/beta/ui/fields/TextareaField/index.tsx` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* web/src/beta/ui/fields/TextareaField/index.tsx
* web/src/beta/ui/fields/InputField/index.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>web/src/beta/ui/fields/NumberField/index.tsx (2)</summary><blockquote>

<details>
<summary>web/src/beta/ui/fields/CommonField/index.tsx (1)</summary>

* `CommonFieldProps` (5-8)

</details>
<details>
<summary>web/src/beta/lib/reearth-ui/components/NumberInput/index.tsx (2)</summary>

* `NumberInputProps` (11-25)
* `NumberInput` (27-136)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>web/src/beta/ui/fields/NumberField/index.tsx (5)</summary>

`5-5`: **Looking good - appropriate React hooks imported**

Adding useState and useEffect is appropriate for the new controlled component pattern being implemented.

---

`11-13`: **Proper prop destructuring**

Explicitly destructuring these props improves code clarity by making it obvious which props are being handled specially vs. passed through.

---

`18-20`: **Good synchronization with external value**

Using useEffect to sync the internal state with external props follows React best practices for controlled components.

---

`22-26`: **Optimization to prevent unnecessary updates**

This is the key improvement in this PR - only triggering the onBlur callback when the value has actually changed. This implements the "skip project mutation update when no change" requirement from the PR description.

---

`34-39`: **Clean implementation of controlled component pattern**

The NumberInput is now properly controlled by the internal state, with handlers for both change and blur events.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T20:00:18Z
user: login: airslice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T02:02:26Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/FeatureInspector/CustomPropertField.tsx (1)</summary><blockquote>

`79-80`: **Consider updating this InputField too**

This InputField (for URL type) still uses the old `onChange` handler which may cause unnecessary updates.

```diff
-      onChange={handleChange}
+      onChangeComplete={handleChange}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e193594a9f5b041d162aad2d29a2c6139946ee9b and 798ed9db43848dec319dce682eee72ab46a5dfe0.

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/DataSource/index.tsx` (2 hunks)
* `web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/FeatureInspector/CustomPropertField.tsx` (3 hunks)
* `web/src/beta/features/Editor/Widgets/ContainerSettingsPanel/index.tsx` (5 hunks)
* `web/src/beta/features/PluginPlayground/ExtensionSettings/PropertyItem.tsx` (2 hunks)
* `web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx` (2 hunks)
* `web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx` (7 hunks)
* `web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Camera/Editor/index.tsx` (2 hunks)
* `web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Layer/Editor/index.tsx` (1 hunks)
* `web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Link/Editor/index.tsx` (1 hunks)
* `web/src/beta/features/Visualizer/shared/hooks/useFieldComponent.tsx` (1 hunks)
* `web/src/beta/features/WorkspaceSetting/innerPages/Workspaces/index.tsx` (1 hunks)
* `web/src/beta/ui/fields/InputField/index.tsx` (1 hunks)
* `web/src/beta/ui/fields/NumberField/index.tsx` (1 hunks)
* `web/src/beta/ui/fields/Properties/PropertyField.tsx` (3 hunks)
* `web/src/beta/ui/fields/TextareaField/index.tsx` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (4)</summary>

* web/src/beta/ui/fields/TextareaField/index.tsx
* web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx
* web/src/beta/ui/fields/NumberField/index.tsx
* web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>web/src/beta/ui/fields/InputField/index.tsx (2)</summary><blockquote>

<details>
<summary>web/src/beta/ui/fields/CommonField/index.tsx (1)</summary>

* `CommonFieldProps` (5-8)

</details>
<details>
<summary>web/src/beta/lib/reearth-ui/components/TextInput/index.tsx (2)</summary>

* `TextInputProps` (12-29)
* `TextInput` (31-128)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Redirect rules - reearth-web
* GitHub Check: Header rules - reearth-web
* GitHub Check: Pages changed - reearth-web
* GitHub Check: ci-web / ci
* GitHub Check: ci-server / ci-server-lint
* GitHub Check: ci-server / ci-server-test

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Layer/Editor/index.tsx (1)</summary>

`97-99`: **LGTM! This change improves update behavior.**

The switch from `onBlur` to `onChangeComplete` aligns with the PR objective to skip unnecessary mutations when no changes are made. This handler will only trigger updates when the value actually changes, rather than on every blur event.

</details>
<details>
<summary>web/src/beta/features/WorkspaceSetting/innerPages/Workspaces/index.tsx (1)</summary>

`81-81`: **LGTM! More efficient mutation handling.**

Changing from `onBlur` to `onChangeComplete` ensures the workspace name update mutation only triggers when the value has actually changed, preventing unnecessary API calls.

</details>
<details>
<summary>web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Link/Editor/index.tsx (1)</summary>

`92-94`: **LGTM! Consistent with new update pattern.**

This change to `onChangeComplete` matches the optimization pattern seen throughout the codebase, ensuring updates only happen when the title value has actually changed.

</details>
<details>
<summary>web/src/beta/features/Editor/Widgets/ContainerSettingsPanel/index.tsx (5)</summary>

`37-45`: **LGTM! Optimized padding update handling.**

Using `onChangeComplete` instead of `onBlur` ensures the widget area state is only updated when the padding top value has actually changed.

---

`50-58`: **LGTM! Consistent update optimization.**

The change to `onChangeComplete` for padding right follows the same optimization pattern, preventing unnecessary state updates.

---

`63-71`: **LGTM! Improved update efficiency.**

The change to `onChangeComplete` for padding bottom maintains consistency with the other fields and optimizes state updates.

---

`76-84`: **LGTM! Follows the optimization pattern.**

Using `onChangeComplete` for padding left ensures consistent behavior with the other padding fields.

---

`90-95`: **LGTM! Gap spacing update optimized.**

The change to `onChangeComplete` for gap spacing completes the consistent implementation of the optimization pattern across all number fields in this component.

</details>
<details>
<summary>web/src/beta/features/PluginPlayground/ExtensionSettings/PropertyItem.tsx (3)</summary>

`102-102`: **Event handler changed to optimize updates**

Changed from `onBlur` to `onChangeComplete` to skip updates when no change occurs in the InputField.

---

`155-155`: **Event handler changed to optimize updates**

Changed from `onBlur` to `onChangeComplete` to skip updates when no change occurs in the NumberField.

---

`94-94`: <details>
<summary>❓ Verification inconclusive</summary>

**Event handler changed to optimize updates**

Changed from `onBlur` to `onChangeComplete` to skip updates when no change occurs in the TextareaField.


---

🏁 Script executed:

```shell
#!/bin/bash
# Verify the behavior of onChangeComplete in TextareaField implementation
rg -A 5 "onChangeComplete.*TextareaField" web/src/beta/ui/fields/TextareaField
```

Length of output: 80

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the TextareaField component file
FILE=$(rg -l "export (default )?const TextareaField" web/src/beta/ui/fields)
echo "Component file: $FILE"

echo
echo "===== Component Definition (first 200 lines) ====="
sed -n '1,200p' "$FILE"

echo
echo "===== onChangeComplete occurrences ====="
rg -n "onChangeComplete" "$FILE" || echo "No occurrences of onChangeComplete found in $FILE"
```

Length of output: 571

---

```bash
#!/bin/bash
# Locate any TSX files defining or using TextareaField
echo "=== Files containing TextareaField ==="
rg -l "TextareaField" --glob "*.tsx" || echo "No files found with 'TextareaField'"

echo
echo "=== onChangeComplete occurrences across TSX files ==="
rg -n "onChangeComplete" --glob "*.tsx" || echo "No onChangeComplete handlers found"
```

---



</details>

**Verify TextareaField onChangeComplete support**  
I wasn’t able to locate the `TextareaField` implementation in the repo, so please double-check that:  
- The `TextareaField` component indeed accepts an `onChangeComplete` prop.  
- That handler only fires when the user finishes editing (e.g. on blur or after debounced input), and not on every keystroke.  
Run a quick smoke test or review the component code to confirm this update skips unnecessary renders as intended.

</details>
<details>
<summary>web/src/beta/features/Visualizer/shared/hooks/useFieldComponent.tsx (1)</summary>

`252-258`: **Event handler changed to optimize updates**

Changed from `onBlur` to `onChangeComplete` for InputField in the FieldComponent. This change ensures that updates are only triggered when the input value actually changes, supporting the PR objective to skip unnecessary mutation updates.

</details>
<details>
<summary>web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/FeatureInspector/CustomPropertField.tsx (3)</summary>

`49-49`: **Event handler changed to optimize updates**

Changed from `onBlur` to `onChangeComplete` for InputField to skip updates when the value hasn't changed.

---

`59-59`: **Event handler changed to optimize updates**

Changed from `onBlur` to `onChangeComplete` for TextAreaField to skip updates when the value hasn't changed.

---

`88-88`: **Event handler changed to optimize updates**

Changed from `onBlur` to `onChangeComplete` for NumberField to skip updates when the value hasn't changed.

</details>
<details>
<summary>web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Camera/Editor/index.tsx (2)</summary>

`96-98`: **Event handler changed to optimize updates**

Changed from `onBlur` to `onChangeComplete` for the title InputField to prevent unnecessary mutation updates when the value hasn't changed.

---

`133-135`: **Event handler changed to optimize updates**

Changed from `onBlur` to `onChangeComplete` for the cameraDuration NumberField to prevent unnecessary mutation updates when the value hasn't changed.

</details>
<details>
<summary>web/src/beta/ui/fields/Properties/PropertyField.tsx (1)</summary>

`106-106`: **Event handler replacement looks good and aligns with optimization goal.**

The changes from `onBlur` to `onChangeComplete` for TextareaField, InputField, and NumberField are consistent with the PR's objective to skip updates when no changes have been made. Since `onChangeComplete` only triggers when a value has actually changed (as implemented in the field components), this will prevent unnecessary updates.



Also applies to: 115-115, 168-168

</details>
<details>
<summary>web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/DataSource/index.tsx (1)</summary>

`66-71`: **Code simplified while maintaining optimization goal.**

Great refactoring that:
1. Simplifies the title update handler to directly receive the new name
2. Removes unnecessary local state management for the title
3. Uses `onChangeComplete` which will only trigger when the value actually changes

This implementation aligns with the PR objective of skipping mutation updates when no change has occurred, while also reducing code complexity.



Also applies to: 101-102

</details>
<details>
<summary>web/src/beta/ui/fields/InputField/index.tsx (1)</summary>

`5-47`: **Core implementation that enables optimization objective.**

This is an excellent implementation that fulfills the PR's objective of skipping updates when no changes occur. The key features are:

1. The component now maintains internal state for the input value
2. It synchronizes with the external value via useEffect
3. It only calls `onChangeComplete` when the internal value differs from the external value
4. The handler naming is semantically more accurate than the previous `onBlur`

This implementation addresses the specific feedback from previous reviews about renaming `onBlur` to `onChangeComplete`, and it provides the foundation for all the other component changes to optimize update handling.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T12:03:56Z
user: login: airslice
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T03:12:01Z

Review comments:
diff_hunk: @@ -71,39 +71,74 @@ const PublicSettingsDetail: React.FC<Props> = ({
     publicImage: settingsItem.publicImage
   });
   const handleSubmitPublicInfo = useCallback(
-    (publicImage?: string) => {
-      onUpdate({
-        ...localPublicInfo,
-        publicImage
-      });
+    (field?: string) => {
+      const updated =
+        (field === "title" &&
+          localPublicInfo.publicTitle !== settingsItem.publicTitle) ||
+        (field === "description" &&
+          localPublicInfo.publicDescription !==
+            settingsItem.publicDescription) ||
+        (field === "imageUrl" &&
+          localPublicInfo.publicImage !== settingsItem.publicImage);
+
+      if (updated) {
+        onUpdate({ ...localPublicInfo });
+      }
     },
-    [localPublicInfo, onUpdate]
+    [
+      localPublicInfo,
+      onUpdate,
+      settingsItem.publicTitle,
+      settingsItem.publicDescription,
+      settingsItem.publicImage
+    ]
   );
 
   const [localBasicAuthorization, setBasicAuthorization] = useState({
     isBasicAuthActive: !!settingsItem.isBasicAuthActive,
     basicAuthUsername: settingsItem.basicAuthUsername,
     basicAuthPassword: settingsItem.basicAuthPassword
   });
-  const handleSubmitBasicAuthorization = useCallback(() => {
-    onUpdateBasicAuth({
-      ...localBasicAuthorization
-    });
-  }, [localBasicAuthorization, onUpdateBasicAuth]);
+  const handleSubmitBasicAuthorization = useCallback(
+    (field?: string) => {
+      if (
+        (field === "username" &&
+          settingsItem.basicAuthUsername !==
+            localBasicAuthorization.basicAuthUsername) ||
+        (field === "password" &&
+          settingsItem.basicAuthPassword !==
+            localBasicAuthorization.basicAuthPassword)
+      )
+        onUpdateBasicAuth({
+          ...localBasicAuthorization
+        });
+    },
+    [
+      localBasicAuthorization,
+      onUpdateBasicAuth,
+      settingsItem.basicAuthPassword,
+      settingsItem.basicAuthUsername
+    ]
+  );
 
   const [localGA, setLocalGA] = useState<PublicGASettingsType>({
     enableGa: settingsItem.enableGa,
     trackingId: settingsItem.trackingId
   });
 
   const handleSubmitGA = useCallback(() => {
-    if (onUpdateGA) {
+    if (onUpdateGA && settingsItem.trackingId !== localGA.trackingId) {
       onUpdateGA({
         enableGa: localGA.enableGa,
         trackingId: localGA.trackingId
       });
     }
-  }, [localGA, onUpdateGA]);
+  }, [
+    localGA.enableGa,
+    localGA.trackingId,
+    onUpdateGA,
+    settingsItem.trackingId
+  ]);
path: web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Missing check for enableGa changes**

While the code correctly checks if `trackingId` has changed, it doesn't verify if `enableGa` has changed. This means toggling the Google Analytics switch may not trigger an update if the tracking ID remains the same.

```diff
const handleSubmitGA = useCallback(() => {
-  if (onUpdateGA && settingsItem.trackingId !== localGA.trackingId) {
+  if (onUpdateGA && (settingsItem.trackingId !== localGA.trackingId || 
+     settingsItem.enableGa !== localGA.enableGa)) {
    onUpdateGA({
      enableGa: localGA.enableGa,
      trackingId: localGA.trackingId
    });
  }
}, [
  localGA.enableGa,
  localGA.trackingId,
  onUpdateGA,
  settingsItem.trackingId,
+ settingsItem.enableGa
]);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const handleSubmitGA = useCallback(() => {
    if (
      onUpdateGA &&
      (
        settingsItem.trackingId !== localGA.trackingId ||
        settingsItem.enableGa   !== localGA.enableGa
      )
    ) {
      onUpdateGA({
        enableGa:   localGA.enableGa,
        trackingId: localGA.trackingId
      });
    }
  }, [
    localGA.enableGa,
    localGA.trackingId,
    onUpdateGA,
    settingsItem.trackingId,
    settingsItem.enableGa
  ]);
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In
web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx
around lines 129 to 141, the handleSubmitGA function only checks if the
trackingId has changed before calling onUpdateGA, but it does not check if
enableGa has changed. Update the condition to also compare localGA.enableGa with
settingsItem.enableGa and trigger onUpdateGA if either enableGa or trackingId
has changed.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T08:01:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,12 +93,12 @@ const GeneralSettings: FC<Props> = ({
             <InputField
               title={t("Project Name")}
               value={project.name}
-              onChange={(name) => setLocalName(name)}
+              onChange={setLocalName}
path: web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx
user: login: airslice
body: Please check and refactor the submit logic.
After the refactor the update of project name / description / image are independent, therefore it's no longer necessary to make them using one submit function, actually it will override incorrectly sometimes. (for example update name / description will remove image).
created_at: 2025-05-09T01:51:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,12 +93,12 @@ const GeneralSettings: FC<Props> = ({
             <InputField
               title={t("Project Name")}
               value={project.name}
-              onChange={(name) => setLocalName(name)}
+              onChange={setLocalName}
path: web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx
user: login: airslice
body: Since the field has inner states now, maybe we no long need the localXXX here any more, try clean up all localXXX and onChange
created_at: 2025-05-09T01:53:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,29 +235,25 @@ const PublicSettingsDetail: React.FC<Props> = ({
         <TitleWrapper size="body" weight="bold">
           {t("Custom Domain")}
         </TitleWrapper>
-        { isPublished &&
-          extensions &&
-          extensions.length > 0 &&
-          accessToken ? (
-            <ExtensionComponent
-              typename={settingsItem.__typename || ""}
-              {...(settingsItem.__typename === "Project"
-                ? {
-                    projectId: settingsItem.id,
-                    projectAlias: settingsItem.alias
-                  }
-                : {
-                    storyId: settingsItem.id,
-                    storyAlias: settingsItem.alias
-                  })}
-              lang={currentLang}
-              theme={currentTheme}
-              accessToken={accessToken}
-              onNotificationChange={onNotificationChange}
-              version="visualizer"
-            />
-          )
-           : (
+        {isPublished && extensions && extensions.length > 0 && accessToken ? (
+          <ExtensionComponent
+            typename={settingsItem.__typename || ""}
+            {...(settingsItem.__typename === "Project"
+              ? {
+                  projectId: settingsItem.id,
+                  projectAlias: settingsItem.alias
+                }
+              : {
+                  storyId: settingsItem.id,
+                  storyAlias: settingsItem.alias
+                })}
+            lang={currentLang}
+            theme={currentTheme}
+            accessToken={accessToken}
+            onNotificationChange={onNotificationChange}
+            version="visualizer"
+          />
+        ) : (
path: web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx
user: login: airslice
body: This part might have conflict with your refactor alias PR, please use that one when resolve the conflict.
created_at: 2025-05-09T01:56:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,19 +2,47 @@ import { TextInput, TextInputProps } from "@reearth/beta/lib/reearth-ui";
 import CommonField, {
   CommonFieldProps
 } from "@reearth/beta/ui/fields/CommonField";
-import { FC } from "react";
-
+import { FC, useState, useEffect } from "react";
 
 export type InputFieldProps = CommonFieldProps &
   Pick<
     TextInputProps,
     "value" | "placeholder" | "onChange" | "onBlur" | "disabled" | "appearance"
   >;
 
-const InputField: FC<InputFieldProps> = ({ title, description, ...props }) => {
+const InputField: FC<InputFieldProps> = ({
+  title,
+  description,
+  value,
+  onChange,
+  onBlur,
path: web/src/beta/ui/fields/InputField/index.tsx
user: login: airslice
body: Since we added the logic of checking value, i feel `onBlur` is not a proper name now, could we rename it as `onChangeComplete` ? In this case
`onChange` -> trigger on every change
`onChangeComplete` -> trigger on blur and there do have changes
What do you think?
For fields we could remove `onBlur`, i guess no component wants blur exactly, all they need is chagneComplete, please let me know if you find any use case it needs `onBlur`, if so we can still have it.
created_at: 2025-05-09T02:01:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,19 +2,47 @@ import { TextInput, TextInputProps } from "@reearth/beta/lib/reearth-ui";
 import CommonField, {
   CommonFieldProps
 } from "@reearth/beta/ui/fields/CommonField";
-import { FC } from "react";
-
+import { FC, useState, useEffect } from "react";
 
 export type InputFieldProps = CommonFieldProps &
   Pick<
     TextInputProps,
     "value" | "placeholder" | "onChange" | "onBlur" | "disabled" | "appearance"
   >;
 
-const InputField: FC<InputFieldProps> = ({ title, description, ...props }) => {
+const InputField: FC<InputFieldProps> = ({
+  title,
+  description,
+  value,
+  onChange,
+  onBlur,
path: web/src/beta/ui/fields/InputField/index.tsx
user: login: airslice
body: Same on other fields in this PR.
created_at: 2025-05-09T02:02:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-05-08T07:57:54Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *reearth-web* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 798ed9db43848dec319dce682eee72ab46a5dfe0 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/reearth-web/deploys/6820917e3b08cd000891e799 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1591--reearth-web.netlify.app](https://deploy-preview-1591--reearth-web.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE1OTEtLXJlZWFydGgtd2ViLm5ldGxpZnkuYXBwIn0.umvqCIMIZwepsqBH1sgSghAykrwD60OqXFgDAWbqsrw)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [reearth-web Preview](https://deploy-preview-1591--reearth-web.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/reearth-web/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-08T07:57:56Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes update input field components and their consumers to use a new `onChangeComplete` event instead of `onBlur` for handling value updates. Several UI field components were refactored to manage internal state, synchronize with external props, and provide the new callback. Related feature components were updated to use `onChangeComplete` for more immediate or explicit update handling.

## Changes

| File(s)                                                                                                      | Change Summary |
|--------------------------------------------------------------------------------------------------------------|---------------|
| `web/src/beta/ui/fields/InputField/index.tsx`<br>`web/src/beta/ui/fields/TextareaField/index.tsx`<br>`web/src/beta/ui/fields/NumberField/index.tsx` | Refactored to manage internal state, sync with external `value` prop, and added optional `onChangeComplete` callback invoked on blur if value changed. Type definitions updated accordingly. |
| `web/src/beta/ui/fields/ColorField/index.tsx`                                                                | Added internal handler to only call `onChange` if color value changed. No signature changes. |
| `web/src/beta/ui/fields/AssetField/index.tsx`                                                                | Refined `handleChange` logic to prevent redundant `onChange` calls and improved file extension extraction safety. |
| `web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx`                                 | Removed local state for editable fields; now uses props directly and updates via new memoized callbacks on `onChangeComplete`. |
| `web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx`                    | Switched event handlers for various fields from `onBlur` to `onChangeComplete` and refined dependencies. |
| `web/src/beta/features/ProjectSettings/innerPages/StorySettings/StorySettingsDetail.tsx`                      | Changed order of operations in color change handler. |
| `web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/DataSource/index.tsx`                        | Removed local state for layer title; now uses prop directly and updates on `onChangeComplete`. |
| `web/src/beta/features/Editor/Map/InspectorPanel/LayerInspector/FeatureInspector/CustomPropertField.tsx`      | Changed field update handlers from `onBlur` to `onChangeComplete` for several field types. |
| `web/src/beta/features/Editor/Widgets/ContainerSettingsPanel/index.tsx`                                      | Changed spacing/padding value update handlers from `onBlur` to `onChangeComplete`. |
| `web/src/beta/features/PluginPlayground/ExtensionSettings/PropertyItem.tsx`                                  | Changed update handlers for various input fields from `onBlur` to `onChangeComplete`. |
| `web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Camera/Editor/index.tsx`<br>`web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Layer/Editor/index.tsx`<br>`web/src/beta/features/Visualizer/Crust/StoryPanel/Block/builtin/Link/Editor/index.tsx` | Changed title and duration update handlers from `onBlur` to `onChangeComplete`. |
| `web/src/beta/features/Visualizer/shared/hooks/useFieldComponent.tsx`                                        | Changed string field update handler from `onBlur` to `onChangeComplete` in field component hook. |
| `web/src/beta/features/WorkspaceSetting/innerPages/Workspaces/index.tsx`                                     | Changed workspace name update handler from `onBlur` to `onChangeComplete`. |
| `web/src/beta/ui/fields/Properties/PropertyField.tsx`                                                        | Changed property field update handlers from `onBlur` to `onChangeComplete` for several field types. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant InputField
    participant ParentComponent

    User->>InputField: Types or edits value
    InputField->>InputField: Updates internal state
    User->>InputField: Completes input (e.g., blur or enter)
    InputField->>ParentComponent: Calls onChangeComplete (if value changed)
    ParentComponent->>ParentComponent: Handles update logic
```

## Poem

> In the garden of code where the carrots grow bright,  
> Fields now listen closer, updating just right.  
> No more waiting for blur, change completes with delight—  
> Rabbits hop in with joy, for the timing is tight!  
> Onward we burrow, with inputs anew,  
> Synchronized, streamlined, and snappier too!  
> 🥕✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNwSPbABsvkCiQBHbGlcSHFcLzpIACIAM3gADwAKAHcSAQBKLkQAa3huHgp8ETFIZjxqeHwsbG5aag8U2DJIDHxGWExSSABtADUASQAtMABGAFYAJgBmAF1oyBS0ZAcBZnUaejkw5shsREoynMcBQRJqeWXrOwwTw4mATlH+LFxdmxJzijfIPqRsNBeeAAL0O/m4+EQ6nwLg0MF2zG0WG42AoEIO/FiO1QthQyAI/G44nWoJ2Hlq9RohXwDGkMnkuXy3HgGCIZLKFXE1T2dQai2aWDaHS60kgnSkkAEnywiKUYXabw8vGKolCB1w4lZiDh7kgSkQDAo+S5WEQ3FE8Hiore1HZFL55VwlW5KXgPklHjk3GWB3olvZypKapIGpZRGQ/kRLL2GAYnVZdDhADl2mhaLR1FUMIC9SHtF5kCp8Hh2fBmNxImwME6TQAaMIhMP1mH2c0MS3wBh+EgSeAkFI8fAs3DINL+am9pS0HW7XHaZj49pl5USkixeLtsgMeTbNASIcZ1kx8i0xCIZzye00ZDRxWLdLUoMaIz6YzgKBkej4LFoPCEUjkFQmwKKw7BcLw/DCKqkiitsTBKFQqjqFoOiviYUBwKgqCYDgBDEGQyjAUwoHVlwVADg4TguJK8jwcoSGaNouhgIYb6mAYaQCAA9IgFAMFxUpOlxsTnLgqLSFxVhFEGADKIaauGXEsoBVhoKQiBcQA4gRVBeHJoZakpGBKAkGgjgkHAGNE1kGBYkAAIIDPhgENPQlGItR37CgmiBuLsxEQuQ1bdsw+BSPQXg0jmiA1h4EjOPAKiRMgSQAAaRQwgJJmgbCpfW6VRV4AAi0iGsaWZ5ZABWZV4AyIqQACqFBeKlGQ7LavA9lU+xePIzRePQdDqElcWAsEyCxC2gaqq0OUkPW+plUSWb1pgfr1R4DU2AAMnCAwYDF5y0PW6itPgA4Zv4Yi9XsBz4rsqXTWIqXUgU8VeONkCTXwioYlaA3avC5K8lS8a0JEFApalYORNlbANSDJCVdDa2RCVBpGst1QI5SSP5TDJB1WpJA4w0rXoOOw5FLQ2C0vQVxsKFIJRDVXgCGgDA5PdtplmwGYNDdLL7jkHipdjiNSSqz2LOosB2oj9D/fTxnsgwqL+MF72fV5d74IqfBKxNRTMFVT24KlM5Kv4vbFsg3oapQWBeVeYaQBl0WxS83ndGt9jYGsGyu9yAgfXwSwRiQFYc1E2xC/geRHleorcnGIogRWIZZnq8BXREsiW1VBMyf76zm+gKso8ZkRE6QADC8akC9rPs5zo6UB4kZhYmQMoBgKKhAF1TsMgbQDvsovVPXIq1ywGc0C9BOQ2Efb0ASv0eOQY+I4wgJsxzXPl1sLKr80OeQFroqXaqN0Eo90mqi9yrmt8faA7qbwnNmbrZ2aXhoJcBZ2jj2QHfKWmgyzEyai1bOudBYHRoGmTEbtCr2Fis+Wylh7JeBoEBLMi52RKAYH/XB1RkBeRIAkCE3wohTX9kCLs7BMzSBfJAFM3tRRjg8LKDwa9dhQiINmMS/gyFYgoVQ4CtCQ6dkgIw8Q0gC6DyCqEVK2kXJ6XkmGRAL1fbqDtkUbgEcow1FjA3RMVkbIGAgGAIwnEeJ8QEnmYSolxIaUlrJDRhllKUFUupSSdDOz6QUq4/xDBAmaJKk6N0ZlEAWXMdEDBDknIAUIlEdyF4kGpx8kYeyko/4YByG7FkjQrhpinOgWIODe6ZkBCCV2d5qqAisCE/ak0XoxQaAXJQ5pjJbkuBQKg8gdYPSLiXdQml7JN13i3Ap4duzxHIKvdoXTPwyMofQ9QN1uQNK8OMjQZARqaTQNoiu7sdn2TMlQTmYYBi0BeiyQ6iChkeEYeObZ4yXqCCfD3VK+1+4ADE+wDRet9AMITl4RA8Jk0gfoRwyKkMFReX1jZVWqAAIVDi9W+k9TEz3LJEee8owhGiIKQPgldwYkGLgHXATSpEMBafgC23zoAUNwPZfwaAAUkCBV9Fsd4UR0tzBjcq3JR63VFKldUQSBg0GYBoAV9D0ZLRNC9K+11Lg3lhRfXuDyvxYm2bS+hDL5UhKVZjFVq0Va6LhewMUqNDizMBIgdoUKoixGRWLDA6LUSYvaJ6qeCZcVzyRgXH5fc8BcqBRNFs7MoRdl/G8GEIJnQ1AOBQbMbBD48B9CkGEblXS4DjFERUZ8ey2sXkbFgKKvUYsJf6nFs98VI0JbgYlpLC72qpaXVFyxOz2TwLAJNwIU1MpkmWN0zher1jvJpfA+AiCRActmXq4gGD4kuQnNkAwipVV+RGwFtz2EwuQGWhF9qDYerRbWrFGAA110bSGZta822HHJZELtYyJnJnaHrZofBXX4KYOOSKRBpEtnaVSJObdxzcPQeYTB2DCJ4JbbsQhxCU0iNWeImhfAFXSNka/FhbCAMoY8PwwRLikFiJhBI3DYKCPyLiS+ditjeL8UEmgJx1AXGSXvmIMJniMAqWJhpGSBAXACcUmJmEshJOIAifmaJsTrLxLso5ZyKS3KOA8oMrEAG/IeDzYcLy+Bn4Yd7uyN9JAZ6RQoHe5tzd96LCuK62g4FrbdUQFOqq6oe2cyIEUbAxkbMwiSAIIgIWKAZBerM1mMc1wwgnhgUmNBpMuCSAAb0lBF/AtnIAAF9osAG4zopHyuLXGaXZCZey5Fgr0W8Q7x8G6nOMV6yTR8OdGO8hJUhj8zkALxZgu5dC+FyL0Xv38H1sg0DXYWzLJ6bGV+94YOKA7GYhJWCcHmd4R4NDzhzPkMoTRnDnhBUMd8pY1hLrTHQa4WmHhCo+HwAEdx4RVHjvUK/HR871YmE3leAgCaboSDPhU8x6xHF0h2PY447A8BhIHo0vZM8IZI20CMiZJTllwdqaSTpVyfsqK6fYZd3UVn7OTJ8NMiz9SUfqnR03WeQ9gqzNChmK0iy5lFP5C0ep2KRQL3PY1uOItpzWA87bbz/pUqomgbM2ITrZAnVCLzIaAtaK7yiPWwXMsfj1Ll8c+gqUM0huu2VlAoRqg3VZiAgXCY7n6sN41xXBZLgqw5yJDWoR3VVrvGrfptrUoXyZbqVllyTRILvPERdrKyBQm5L7k2svmoxYZg91o3gvBgHPCJGi6B0xByxsu4ULJXZpTlwAfg0L/dQrUC4gekQSVm1bKe88Bx4GPPDZDmkaxzWkRIoixdMVz638hRdJbb/6O8m8qrO895QStJt/fqyDyHzpkdPy9IpgMqPD1x61ymfvBec6Ckgop6YtPyAk5c5ZEQ7Acpg9jTN+4DEJGmDNbEDBG6nV4WhH8DTMZJgFbnwEFieHSOkrbnWvbo3O3pZs7p0CPHrEevwHwKgK7ogMrlmsuEUBKLniGOPlgHHgdFnOHhzCaHBptohiQgdKRrmEQgdiaJhtRl9qgWdvQjIn9nIpdlAMRrditvdnKLtvQehkwWweRm9snKIp9rRuwfhlwcttGKlPTmjgeqHgiGthuOZs4B4MOI7DmMIRfrro5pzCgHiiQFWDWFmGDhYlYjYtDmxg4kJPDojtyrQBpJFujpjhQtjkxnjhpkBKktpukl5PpgYLqIokHpFnurgIzqVlhusnnEKq2rTEIhKiHlmjrg7q9EvpbgDv3HtKrtWNTLTKKGgK0P2L3DgiXoirEEFl/lsgTG3jaKEE6CLIWBUQOB/i2BfPWAFDoZqnrnLCvoHsFE/h9HjFmmPk1gWOyHHumjmFkbASYQUtPrsLPtqgvpDDOKgIiqgIqMiD6MWk9uSD0s1LIK7KlNEeGmXJEcFFcNaksc2k/PWOCH/O2EePypLvsDAjNN9EsBQIeGyF5E/KOLLMWKEJgLRKYl9G6DgmGOgmptQTticSIYwchkdtht9nIQwgocwldnwSKPgneBIWkXvs8jIdrtcf8moenCzrgCVqdJGEiCAkwPApANSfum4VwH8rXMAJybEQepLAYnoEygABJdZSAUDTq7B6ELG+AwxBz6oZG+xPGPz6I3gnprKdgbKXDphRC3j+TM5KJmEZyWEpo2HxKWIsYOH2IcZcQuGGxcQxFeHHw+HmQ442T+HJKBFabE4ZK3YGa7o3FxF3GhCzI36EpynLo3SIjZjdDWosj9znzP6oJ8j7CuwfDkEADkIC48YmZMWamBJiRQGAzMluQxcxCQ1RixGRT858iUc+BwAAouuA/HCDkrPqZiaDmCsa9K3g2uYQSo6vqcdPyJ2HLFhBgMLDQh3r3P3LmbkqiDasFNMWsbodWPob4JsZaF7rkXePMSXuMcEOoZCsabamKmXuIDUqCIWFgNGR7HyKlA+V4H0M/sCsivUnWfolmlBhWU0ARFKVVBkQBqGk0Zfl9PUZHn+W8FOTWb4BBh4L7FAQedWVuQOcYUfqYQWsMesZURfKGiytWTEUzuWPSY1mya2rlpEPQDhVVC+W+RMUbuKvdNIBvJURWryv+jCb7CHMuaeiOK8ZHO8XUrsL/p5jGJRUUM1tiWqkkd6KjvQKCegNwMqBzLAKGlej6v2bMp3OFEin7mJZ2GLjkVmm8dHFsPIHeC+R2lXNZoOcGkLnZRQIiQhttmIcIftjQcwZSdiXhrieEIRldvZKOYSD2b4PWU8UGk2pXlwEkDQNWdkK2mGG1AALx6DnwHi+qWYulCkamYo94blVSsZ2lw4I5Om5VuHeGmTmQWxMbWmQ4lWw7OHlVI5cREVOgcquk9I1UxKemqaYL44uTARpKeR6aBnhEPQdU6Gcq0lhnObIDs7ra36bnymxmYDEx5HzkliIW3SuzlGy4HAFkEqDrxxVLXlAigi0WyylixgsCXHfmmYdlNkkCtkiTSynUFIwVA5VHoW7UAXkBAWoXwXAXvmk4KLnnBRUyKClEdFdnF69lYUFKRUwHWYPoEotEi7TnxyzmWZaUUAvQCUoBYjWWrUl47ltlLxJ5Vkg3Hkv7DL2pt59nfUbkg27XIW7ysWrK02o1OUUphw3Uz74XP6EWsrsrnBNwIADR0kmmoCRkEjjy3Vs2ewPGwp03MUEgT5kgYiz6cUpw8Uqx8V8ACWIBCVRwfFshfFdS2ySXVBUUyW3TEyuUOTIkeWoleWHbSFYlsH+WcGBX4lQAhVlJRXo0kCxWQDxWspJVGishpUZX7jwCHqGEdXi2zVuHClaJhCFUWapRNVOGcaOltXTVdUHrVVKZ1Xg4NX2HcSOH2mF1uEaRJh3AUDdVY4el+GDUBGE6jUk5hHk5N3MBSgt1zWQ3hkMyaErxRlk27zyBxmbWJk3FplUh0UHUB7e6MUnlL2jRGgjTnWJSXVuqfmyl3XrBHjq2vSFHxHAIvVvUPyErFlxilnlmpRr3sAb3NoA1lo/S7CHm1lg2gU9zzX96RywrlGz59ko23oOVNoxYIBxhY0zlfhzmereoE0rlW4k0PQD1D0kUvCCyYMeCnJVSv3Vjv2qq7mL4GXL4/1oXymg1MWaVQO671nYSAJ2p2Vc5JzEOr6kNg2+xPxqR8gka/0+D5zfLYOUC4OoBSXUVRC9jlH1IkO4BkNb1ZpiKJE3SugAGVlqmZH4180Qym3djm2iVWzW0/H1F23SU0XdhnGiW2hQh4oMidSIKgnO1bZIakJ0Ee1iGYknZ+X0Z4k8FVQSPD3p35VZ296RlKF5112tUN1cShOt3ukxLZWlLlxhVZiLGo3RWPrh1JAXzh23CD2UBx2ZWJ1U57ymFPzPhQCpRJMj1kUmmkniQ8i4x+hYC522nNUF3xMAyJPN3JO9UJDZXqM6lJF34fSP6FNcDFND3Iw5Oh35MzOZ4lNRaQDpXlO3KWrG6o3LPP5FPN1lMJ2HrKWAjVDhiJ0UlIAKQvCMaV12FQ412lVCQiSSEaTNkZjiZcQACyaA3AzpB0bY4mqk5AXgXE20/8kjQLqoMIXERU1AaAMkxYfEJAZd7duOndPp3dwRY1pOQZ81elxauwRDu1c9pAlh5JbsULP0GyHgaUpy0AdL5MOioDZ4NIiUwEa471rRnQaosgJZ1QzMMKlZ9SBwkQYgdAkLsglAZkzLF93yBMTLEKKWDmSNC19gy4QInOhKwDRI6AnRs0maMUMdbIclN0Oi2NIseNGA0rlAcMJMiMlTNOdFd4kZpuWaf8MrfA26QloUvYnxYl3xyAbJXzWTvgjeXYmN/R2jRD/DP5F8b8D0lVPKIKd4XrhwHrYq5rl4d0Pm3KqoUrNLcrEK2imqyA2qvsyVJKlDVmyrkQqrHyyDizQ5zaRN9yCCeqZQG1oGR4cpfMnLhDKC7NKsGZZ9+jOxIbMJJr5wzAQI5AXN4Qi6XDsQkUA42w3K46girspLKtKs/gvUQcWAMjDtSZJYdFclbTDQ2oRg8GLt7lyGnlogohGJXt/jPtgT/twThJPkhKLBshvtsi8gPjyGnC3bSgENTTQeCLToyLqItIzF1qylzJ0Y9RI+FmLT/glpEO1dMO+dXGaRHzXzcLfzAL+0ZosLFAoL3KELNL5HwLcLfyzi/g9HlHXEtc+wBAzAwplAgpbhvhmLiSXdI1uLvdE1Yef+7D/NXFV7tzSsKZExAOlm6OQa9JV+KB1NY7ltD0+j6D0BTDgaodwKfKybIZtJSQHVkTJAGQ+UKdJdbhL0FnYtHKVnNnmRDTDnEdfykUtovs+0rRhViAGQk7ZQE9HCAolm4FmF1OTm+xL6AB9YiACAFSrs1N8QprXsZ7oQRtEdkUUIR4k0asQXhK6XR7BrAU+KpBknTy7CoaKhfHQK+UW020dyNxOzVUMkBacYcR/gkSHepaCQNzrsRNnFOFEJoXHOnYFpt7VBD7XjT7DB3lH23tkiHBF2RGN2RJdBmHUhWG77q38hX7JW9SKno9wKUFWcO3IULJklI+2HVdTzeH9pbzhHXEnz0IFAXEAA6onaQCOOx3bUiJQHJtR+C26cM/1ZtkNZpkTjpgGUSUGSN8LiCi7EeN6IXkeL7EQP862BzK7Im7Tlg4M404FMPBq65lQ9Wqg9lSHa2+oagEtVaKOBF/UgTD97QH96ncdfZbrnUbGJHtI1rm5jqp21HvF2V6TcmXl6lzSPsCrvEdW6SjeKBPzDQBaxUocP7jCdI6HeLrqJGzqlc+yJxSh7QSSXNPWKj9p2eYHmaNUECdmhj8CXwNjwUE/LxyTvUq6BzyGKnW0p7K63hQOMU5QNIgRXex40twty+14346wQdwFf9kYCmOQPd487E44i9zxlYB9L2zn//INkFhjs2WhSQdUHJrxqZh7zKhYQJ16ViwTiJ/6aEeJz/ZJ7UeBl/TmFlzLWT8PiKIrJejWtpTevZrk/PHCDJG2B2KzFgWm1NayjNXEUkOUNgm6Dzia67A1AMG52GjSZ50kEoIrt4GqMlUeNv256qR5zykkHM5r9nWNztUCAhJABfzKZCuq4r6H4nIjKOFcIgF12aCD8q0pXI8MHFDjoN8EoA4ElgHKKup04lXbkAJRC7wC0AbZMQMzz5wM07KbePng0XvIhshehKLhl8Sr7fB5ABFa7FNj/QzYwM/6Kxrli+hrsNWgIHBBtiRJzcze7tZ9uiVj5vt4+P2NbkE2T5Dw0+NpZ5j0wI48Y/gDgG8pQHY4UBOOXEKrKDy4jooaQOQASPDjX4YB2Oc0KgG92I6fdwedfAakJ2xZN84eLfBHpNWeTt9keJnUWCm0PRhlFyS7K2GQNwDyB++CYYAcnl06j9oGj6SqIzwuJHgAa8sXGJBX55ZxUoSgU4PURIAAB5ZLE60axf9tGgfdihRDP5shKBY6dYMQm8z1Jr+Lg0ejJ39z6DyiNMGgq9A952pj0enRFBT2pooNr0fqFto5VWjUFXYEQqzI2wopECshsnKIHfz4DapD+a4X8DoMtrtAAADMTS+hK5guQMN/PwSS6WheWwMKIRkMp7S8CusvKGPozagEg4B6NKrrajSidCYGl/dAbczvDEglSdQ8gSMOjTf02KsJBupQQ4GeMuBBCHgUtzj4AdP2SfAwD+3UgCFwOj2f4Yt0Ox8AduPlFboIMO7/ZCeqAT3GIMardN8OWfYRFxBkEAgD6n3WuIoJijKDxMsgVQeoM5haC4SLIWjt60MEfd0WfVDuuYMb5BFm+41GwRJ3LQOD3hwZffjylcHX40021MelOwH6U82hI/DoQZ3vR08QujPZbBEPqTxChstIFIQMLwEC910L2UlG6hbBW92Q0QdwQsHd7kD3M5jLzOPglEzEh8LPY+smXk55cfchw+sJeVVyEDT2KvQdur0qRusxR8yJAM0ElGshTG4ohTsEEmy/pDgBvFsCeyYHnQWB1BDbBH1dqPtuBsI3xvwOBG/Yv2Ig1PvVXT7YjnuzHCSASLkHEjSRuAckTJipEZRNBAgbQZqAhYshNB73b5iYIxb192Rw1TkVYO5FZJbBTQ/kTlTM6ecRRDYaspGJ8HQppRAQuUWPyM5KiwumA5BuqMSFai0hOorOILwdoK1t4d4U0XS3NH6J6hNXcVoWxhQWErRNsHqLaKJT6j24SDSMTlySD7C2QhXfYG509FPia2S8bkHAJhIVdM4SAv/DGOmzxjcMB2NgDgjuzoBUx04Gbj8Kj5ZiY+tBIEadkA7CCCSm3X9tH14GYT4RL2CjO9iwkBN8x/2TEbh1rqZ9yxGkSsUSJ4idAACXET6hpHHincoO1YUwVD2E4DiQiQ49SASxhLmVTwNranqOOco6NrhwQioaZyFFlCeJ4ZWWIaVFjcTSe1Yc7jEOqCrjJu1oNvrahdBwNcKosAmDx3IHv0BhfZOLs+O0YgpN+rIMAF4N7yGx2ooQWgO0DaChBEQhaOWLGPhHT8Nwr/AYFZyTZKhYJj6JeApV9B0E+ypvW7gP3rDNithenTGhkK5pcMNhFSXIp+N5RFcI6MogmicNTAICwJTsYSXYMuG09HKKwthIFNoFzZ6BxRRgau2TGsM2ByE29qhJRIwiMJiI/bsiMT7cENupObxgCLhGatXshHZbkNJxJ+1qJxY8QU93onvNvuMILmN6FpCSYjIwmXxF902lmho4GkbsayME7qYLBgkvFn3SMlnoZJ3IepM4NIpaS3RAo3NBQC2nRwjWxSMMfONaGLiMKhnRUfCAZ5ri4CrPTtKMlwCqtDpn046bSAdbOtYueogCXQDvGeZBYdoifLRUdEbkpekIN0UVw9EdSvR/4g0fQFqApxzh3IGrvAOQFUDGp0EhQK1N8DtSBwjqJCd8Lcq/DiSqGSaTmL24CCFp63PCeNIJD/tsJIIrwWiSW5gdYMy0rERIPw711+mlk7gpX2fheD0cfE70hyL9KDj8WI4pHjJMcld9fAPfMMkp3qTqzZAcRLMqUHmpzjD6VaYqTTzkkT9IAU/C0FoVEbv8qoxdc4IznyjPT8oU/PHqyGDkhNiek40ekgQHC7CjC2RR6Tp3lFo06eenDtkdCQRuzCKroDAM9MyI2ARQoZOOagjdC+BFabsmTknMbgoCeKdw9cQGHPEvCuGexb0TRUS7Jc5OyKaAV7HynfioBOcMrmcKHIXDVyBDWrozKgnzo6BLM+2kmI5klIuZKEnmWhP6lETBpwsnCQWLFkkZhCCIuadvOlkqj24UI0NLbNLkqTFyZzFWP+2u6ocTEA/MQaxHQicEu2Caf8PrJAiWEyIaACiKJ3zx0REIagRiKhBYgGA35xEUuAAH1E6iAGBR5n7B0AYF7Sb4MxFfnvhIADwT4AABZcFJAOYaMFEBoAAA7KQtiACBxg0wUhSQAABs0wOhbEEmACAGAtAcYBwoAActASYAwE4UMBcF4wThdMAwWQKsFkwXBUoHGCxAHg4wAQLEE4WjBaAsQKYNMAYAPAiF6QXBaQrUUMBxgDwFQEovZikLRgaASYKIrfkkBRgDwaYPotwVoAHgKigQHMNwVKLRgdCyYGgDTCTAeFBi3hXQtGDTAHgDwXBXQs+APABAFirBaQoeCcK6ADwWgAIFwXTBOFuC7haIGmDWLaAtIOhZwsmCfASApCzxUkroVoBxgyiwhaIrEUQAf56gOBe4UQVdRkFtAGBSsn0BAA= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=reearth/reearth-visualizer&utm_content=1591):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR