fix: Fix service_name for select list in sight page
Some checks failed
release-tag / release-image (push) Failing after 1m36s

This commit is contained in:
Илья Куприец 2025-05-28 20:54:35 +03:00
parent 9218743faf
commit ffe75f3670
3 changed files with 83 additions and 105 deletions

View File

@ -3,9 +3,9 @@ import { Create, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form"; import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { EVERY_LANGUAGE, Languages, languageStore } from "@stores"; // Assuming these imports are correct and available import { EVERY_LANGUAGE, Languages, languageStore } from "@stores";
import { LanguageSwitch } from "@/components/LanguageSwitch"; // Assuming this import is correct and available import { LanguageSwitch } from "@/components/LanguageSwitch";
import { axiosInstance, axiosInstanceForGet } from "@/providers"; // Assuming these imports are correct and available import { axiosInstanceForGet } from "@/providers";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
@ -13,62 +13,55 @@ export const CityCreate = () => {
const { language, setLanguageAction } = languageStore; const { language, setLanguageAction } = languageStore;
const navigate = useNavigate(); const navigate = useNavigate();
const notification = useNotification(); const notification = useNotification();
// State to hold all language translations for the city name.
// Initialized with an object where each language key maps to an empty string. // State to manage city name translations across all supported languages.
// Initializes with empty strings for each language.
const [allLanguageNames, setAllLanguageNames] = useState< const [allLanguageNames, setAllLanguageNames] = useState<
Record<Languages, string> Record<Languages, string>
>(EVERY_LANGUAGE("")); >(EVERY_LANGUAGE(""));
const { const {
saveButtonProps, saveButtonProps,
refineCore: { formLoading }, // Indicates if the form is currently loading data refineCore: { formLoading },
register, // Function to register input fields with react-hook-form register,
control, // Object to pass to Controller for controlled components control,
setValue, // Function to programmatically set form values setValue,
watch, // Function to watch for changes in form values watch,
handleSubmit, // Function to handle form submission, including validation handleSubmit,
formState: { errors }, // Object containing form validation errors formState: { errors },
} = useForm<{ } = useForm<{
name: string; // This field will hold the name for the *currently active* language name: string;
country_code: string; // The code for the selected country country_code: string;
arms: string; // The ID of the selected media for the city's arms (e.g., coat of arms) arms: string;
}>({}); }>({});
// useEffect hook to synchronize the 'name' TextField with the 'allLanguageNames' state. // Keeps the 'name' input field synchronized with the currently active language's translation.
// Whenever the active 'language' or the 'allLanguageNames' state changes, // Updates whenever the active language or the `allLanguageNames` state changes.
// the 'name' field in the form is updated to display the correct translation.
useEffect(() => { useEffect(() => {
setValue("name", allLanguageNames[language]); setValue("name", allLanguageNames[language]);
}, [language, allLanguageNames, setValue]); }, [language, allLanguageNames, setValue]);
// Function to update the 'allLanguageNames' state with the current value from the 'name' TextField. // Captures the current value of the 'name' TextField and updates the `allLanguageNames` state.
// This is crucial to ensure that changes made by the user are saved before // This is vital for preserving user input when switching languages or before form submission.
// switching languages or submitting the form.
const updateCurrentLanguageName = () => { const updateCurrentLanguageName = () => {
const currentNameValue = watch("name"); // Get the current value typed into the 'name' TextField const currentNameValue = watch("name");
setAllLanguageNames((prev) => ({ setAllLanguageNames((prev) => ({
...prev, ...prev,
[language]: currentNameValue || "", // Update the specific language entry in the state [language]: currentNameValue || "",
})); }));
}; };
// Handler for language switch. // Handles language changes. It first saves the current input, then updates the active language.
// It first saves the current input for the active language, then changes the language.
const handleLanguageChange = (lang: Languages) => { const handleLanguageChange = (lang: Languages) => {
updateCurrentLanguageName(); // Save the current input before switching language updateCurrentLanguageName();
setLanguageAction(lang); // Update the global language state setLanguageAction(lang);
}; };
// Autocomplete props for Country selection. // Autocomplete hooks for selecting a country and city arms (media).
// 'resource' specifies the API endpoint, and it's assumed 'code' is the value
// to be stored and 'name' is the label to display.
const { autocompleteProps: countryAutocompleteProps } = useAutocomplete({ const { autocompleteProps: countryAutocompleteProps } = useAutocomplete({
resource: "country", resource: "country",
}); });
// Autocomplete props for Media selection (specifically for city's arms).
// 'resource' specifies the API endpoint, and 'onSearch' provides a custom
// search filter based on 'media_name'.
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({ const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
resource: "media", resource: "media",
onSearch: (value) => [ onSearch: (value) => [
@ -80,60 +73,61 @@ export const CityCreate = () => {
], ],
}); });
// This function is called when the form is submitted and passes validation. // --- Form Submission Logic ---
// It orchestrates the API calls for creating the city and updating its translations.
// Handles the form submission. It saves the current language's input,
// validates the Russian name, and then sends requests to create/update city data
// across different languages.
const onFinish = async (data: { const onFinish = async (data: {
name: string; name: string;
country_code: string; country_code: string;
arms: string; arms: string;
}) => { }) => {
// Ensure the very latest input for the current language is captured
// into 'allLanguageNames' before making API calls. This handles cases
// where the user might click 'Save' without blurring the 'name' field.
updateCurrentLanguageName(); updateCurrentLanguageName();
// Create a final set of names including the latest value from the form
// for the current language.
const finalNames = { const finalNames = {
...allLanguageNames, ...allLanguageNames,
[language]: data.name, // Ensure the absolute latest value from the form is included [language]: data.name,
}; };
try { try {
// Validate that the Russian name is present, as it's used for the initial POST request. if (!finalNames.ru) {
if (!finalNames["ru"]) {
console.error("Russian name is required for initial city creation."); console.error("Russian name is required for initial city creation.");
// In a real application, you might want to display a user-friendly error message here. if (notification && typeof notification.open === "function") {
notification.open({
message: "Ошибка",
description: "Русское название города обязательно для создания.",
type: "error",
});
}
return; return;
} }
console.log("Submitting with names:", finalNames); console.log("Submitting with names:", finalNames);
// 1. Create the city entry in Russian. // Create the city with the Russian name first.
// This POST request typically returns the ID of the newly created resource.
const ruResponse = await axiosInstanceForGet("ru").post("/city", { const ruResponse = await axiosInstanceForGet("ru").post("/city", {
name: finalNames["ru"], // Russian name name: finalNames.ru,
country_code: data.country_code, // Country code from the form country_code: data.country_code,
arms: data.arms, // Arms ID from the form, included in initial creation arms: data.arms,
}); });
const id = ruResponse.data.id; // Extract the ID of the newly created city const id = ruResponse.data.id;
// 2. Patch the city with the English name if available. // Update the city with English and Chinese names if available.
if (finalNames["en"]) { if (finalNames.en) {
await axiosInstanceForGet("en").patch(`/city/${id}`, { await axiosInstanceForGet("en").patch(`/city/${id}`, {
name: finalNames["en"], // English name name: finalNames.en,
country_code: data.country_code, // Country code from the form country_code: data.country_code,
arms: data.arms, // Arms ID from the form, included in initial creation arms: data.arms,
}); });
} }
// 3. Patch the city with the Chinese name if available. if (finalNames.zh) {
if (finalNames["zh"]) {
await axiosInstanceForGet("zh").patch(`/city/${id}`, { await axiosInstanceForGet("zh").patch(`/city/${id}`, {
name: finalNames["zh"], // Chinese name name: finalNames.zh,
country_code: data.country_code, // Country code from the form country_code: data.country_code,
arms: data.arms, // Arms ID from the form, included in initial creation arms: data.arms,
}); });
} }
@ -144,47 +138,38 @@ export const CityCreate = () => {
type: "success", type: "success",
}); });
} }
window.onbeforeunload = null;
navigate("/city");
// After successful creation/update, you might want to: navigate("/city", { replace: true });
// - Navigate to a different page (e.g., city list).
// - Display a success notification to the user.
} catch (error) { } catch (error) {
console.error("Error creating/updating city:", error); console.error("Error creating/updating city:", error);
// Display an error message to the user if the API calls fail.
} }
}; };
return ( return (
<Create <Create
isLoading={formLoading} // Show loading indicator if form data is being fetched isLoading={formLoading}
// Pass the handleSubmit function to the save button's onClick.
// This ensures form validation runs before 'onFinish' is called.
saveButtonProps={{ saveButtonProps={{
...saveButtonProps, ...saveButtonProps,
disabled: saveButtonProps.disabled,
onClick: handleSubmit(onFinish as any), onClick: handleSubmit(onFinish as any),
}} }}
> >
<Box <Box
component="form" component="form"
sx={{ display: "flex", flexDirection: "column" }} sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off" // Disable browser autocomplete autoComplete="off"
> >
{/* Language Switch component to change the active language */}
<LanguageSwitch action={handleLanguageChange} /> <LanguageSwitch action={handleLanguageChange} />
{/* Country Autocomplete field */}
<Controller <Controller
control={control} // Pass control from useForm control={control}
name="country_code" // Field name in the form state name="country_code"
rules={{ required: "Это поле является обязательным" }} // Validation rule rules={{ required: "Это поле является обязательным" }}
defaultValue={null} // Initial value defaultValue={null}
render={({ field }) => ( render={({ field }) => (
<Autocomplete <Autocomplete
{...countryAutocompleteProps} // Spread autocomplete props from useAutocomplete hook {...countryAutocompleteProps}
value={ value={
// Find the selected option based on the field's current value
countryAutocompleteProps.options.find( countryAutocompleteProps.options.find(
(option: { code: string; name: string; id: string }) => (option: { code: string; name: string; id: string }) =>
option.code === field.value option.code === field.value
@ -194,7 +179,6 @@ export const CityCreate = () => {
_, _,
value: { code: string; name: string; id: string } | null value: { code: string; name: string; id: string } | null
) => { ) => {
// Update the form field's value when an option is selected
field.onChange(value?.code || ""); field.onChange(value?.code || "");
}} }}
getOptionLabel={(item: { getOptionLabel={(item: {
@ -202,14 +186,12 @@ export const CityCreate = () => {
name: string; name: string;
id: string; id: string;
}) => { }) => {
// Define how to display the label for each option
return item ? item.name : ""; return item ? item.name : "";
}} }}
isOptionEqualToValue={( isOptionEqualToValue={(
option: { code: string; name: string; id: string }, option: { code: string; name: string; id: string },
value: { code: string; name: string; id: string } value: { code: string; name: string; id: string }
) => { ) => {
// Define how to compare options for equality
return option.id === value?.id; return option.id === value?.id;
}} }}
renderInput={(params) => ( renderInput={(params) => (
@ -218,39 +200,38 @@ export const CityCreate = () => {
label="Выберите страну" label="Выберите страну"
margin="normal" margin="normal"
variant="outlined" variant="outlined"
error={!!errors.country_code} // Show error state if validation fails error={!!errors.country_code}
required // Mark as required in UI helperText={errors.country_code?.message}
required
/> />
)} )}
/> />
)} )}
/> />
{/* City Name TextField for the current language */}
<TextField <TextField
{...register("name", { {...register("name", {
required: "Это поле является обязательным", // Validation rule required: "Это поле является обязательным",
onBlur: updateCurrentLanguageName, // Update translations when the field loses focus onBlur: updateCurrentLanguageName,
})} })}
error={!!errors.name} // Show error state if validation fails error={!!errors.name}
helperText={errors.name?.message}
margin="normal" margin="normal"
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
type="text" type="text"
label={"Название *"} label={"Название *"}
name="name" // HTML name attribute, matches register key name="name"
/> />
{/* Arms Autocomplete field */}
<Controller <Controller
control={control} // Pass control from useForm control={control}
name="arms" // Field name in the form state name="arms"
defaultValue={null} // Initial value defaultValue={null}
render={({ field }) => ( render={({ field }) => (
<Autocomplete <Autocomplete
{...mediaAutocompleteProps} // Spread autocomplete props from useAutocomplete hook {...mediaAutocompleteProps}
value={ value={
// Find the selected option based on the field's current value
mediaAutocompleteProps.options.find( mediaAutocompleteProps.options.find(
(option: { id: string; media_name: string }) => (option: { id: string; media_name: string }) =>
option.id === field.value option.id === field.value
@ -260,25 +241,21 @@ export const CityCreate = () => {
_, _,
value: { id: string; media_name: string } | null value: { id: string; media_name: string } | null
) => { ) => {
// Update the form field's value when an option is selected
field.onChange(value?.id || ""); field.onChange(value?.id || "");
}} }}
getOptionLabel={(item: { id: string; media_name: string }) => { getOptionLabel={(item: { id: string; media_name: string }) => {
// Define how to display the label for each option
return item ? item.media_name : ""; return item ? item.media_name : "";
}} }}
isOptionEqualToValue={( isOptionEqualToValue={(
option: { id: string; media_name: string }, option: { id: string; media_name: string },
value: { id: string; media_name: string } value: { id: string; media_name: string }
) => { ) => {
// Define how to compare options for equality
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={( filterOptions={(
options: { id: string; media_name: string }[], options: { id: string; media_name: string }[],
{ inputValue } { inputValue }
) => { ) => {
// Custom filter for options based on input value
return options.filter((option) => return options.filter((option) =>
option.media_name option.media_name
.toLowerCase() .toLowerCase()
@ -291,7 +268,8 @@ export const CityCreate = () => {
label="Выберите герб" label="Выберите герб"
margin="normal" margin="normal"
variant="outlined" variant="outlined"
error={!!errors.arms} // Show error state if validation fails error={!!errors.arms}
helperText={errors.arms?.message}
/> />
)} )}
/> />

View File

@ -499,13 +499,13 @@ export const SightCreate = observer(() => {
onChange={(_, value) => { onChange={(_, value) => {
field.onChange(value?.id ?? ""); field.onChange(value?.id ?? "");
}} }}
getOptionLabel={(item) => (item ? item.heading : "")} getOptionLabel={(item) => (item ? item.service_name : "")}
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) =>
option.id === value?.id option.id === value?.id
} }
filterOptions={(options, { inputValue }) => filterOptions={(options, { inputValue }) =>
options.filter((option) => options.filter((option) =>
option.heading option.service_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase())
) )

View File

@ -668,14 +668,14 @@ export const SightEdit = observer(() => {
setLeftArticleData(undefined); setLeftArticleData(undefined);
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
return item ? item.heading : ""; return item ? item.service_name : "";
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(option, value) => {
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter((option) => return options.filter((option) =>
option.heading option.service_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase())
); );
@ -978,14 +978,14 @@ export const SightEdit = observer(() => {
setLeftArticleData(undefined); setLeftArticleData(undefined);
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
return item ? item.heading : ""; return item ? item.service_name : "";
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(option, value) => {
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter((option) => return options.filter((option) =>
option.heading option.service_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase())
); );