266 lines
9.2 KiB
JavaScript
266 lines
9.2 KiB
JavaScript
import { Form, Space } from "antd";
|
|
import { useTranslation } from "react-i18next";
|
|
import AlertComponent from "../alert/alert.component";
|
|
import "./form-fields-changed.styles.scss";
|
|
import Prompt from "../../utils/prompt";
|
|
|
|
export default function FormsFieldChanged({ form, skipPrompt, onErrorNavigate, onReset, onDirtyChange }) {
|
|
const { t } = useTranslation();
|
|
|
|
const normalizeNamePath = (namePath) => (Array.isArray(namePath) ? namePath.filter((part) => part !== undefined) : [namePath]);
|
|
|
|
const getFieldIdCandidates = (namePath) => {
|
|
const normalizedNamePath = normalizeNamePath(namePath).map((part) => String(part));
|
|
const underscoreId = normalizedNamePath.join("_");
|
|
const dashId = normalizedNamePath.join("-");
|
|
const dotName = normalizedNamePath.join(".");
|
|
|
|
return [underscoreId, dashId, dotName].filter(Boolean);
|
|
};
|
|
|
|
const clearFormMeta = () => {
|
|
const fieldMeta = form.getFieldsError().map(({ name }) => ({
|
|
name,
|
|
touched: false,
|
|
validating: false,
|
|
errors: [],
|
|
warnings: []
|
|
}));
|
|
|
|
if (fieldMeta.length > 0) {
|
|
form.setFields(fieldMeta);
|
|
}
|
|
|
|
onDirtyChange?.(false);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
if (onReset) {
|
|
onReset();
|
|
} else {
|
|
form.resetFields();
|
|
}
|
|
|
|
window.requestAnimationFrame(() => {
|
|
clearFormMeta();
|
|
});
|
|
};
|
|
|
|
const getFieldDomNode = (namePath) => {
|
|
const fieldInstance = form.getFieldInstance?.(namePath);
|
|
const fieldIdCandidates = getFieldIdCandidates(namePath);
|
|
const domCandidates = [
|
|
fieldInstance?.nativeElement,
|
|
fieldInstance?.input,
|
|
fieldInstance?.resizableTextArea?.textArea,
|
|
fieldInstance
|
|
];
|
|
|
|
fieldIdCandidates.forEach((fieldId) => {
|
|
const escapedFieldId = CSS.escape(fieldId);
|
|
const directNode = document.getElementById(fieldId) || document.querySelector(`#${escapedFieldId}`);
|
|
const labelNode = document.querySelector(`label[for="${escapedFieldId}"]`);
|
|
const namedNode = document.querySelector(`[name="${escapedFieldId}"]`);
|
|
const formItemNode =
|
|
directNode?.closest?.(".ant-form-item") ||
|
|
labelNode?.closest?.(".ant-form-item") ||
|
|
namedNode?.closest?.(".ant-form-item");
|
|
|
|
domCandidates.push(directNode);
|
|
domCandidates.push(namedNode);
|
|
domCandidates.push(formItemNode);
|
|
domCandidates.push(formItemNode?.querySelector?.("input, textarea, select, .ant-select-selector"));
|
|
});
|
|
|
|
return domCandidates.find((candidate) => candidate instanceof HTMLElement) ?? null;
|
|
};
|
|
|
|
const waitForAnimationFrames = (frameCount = 1) =>
|
|
new Promise((resolve) => {
|
|
let remainingFrames = frameCount;
|
|
const nextFrame = () => {
|
|
if (remainingFrames <= 0) {
|
|
resolve();
|
|
return;
|
|
}
|
|
remainingFrames -= 1;
|
|
window.requestAnimationFrame(nextFrame);
|
|
};
|
|
window.requestAnimationFrame(nextFrame);
|
|
});
|
|
|
|
const getFieldOwningTabMeta = (namePath) => {
|
|
const fieldDomNode = getFieldDomNode(namePath);
|
|
const owningTabPane = fieldDomNode?.closest?.(".ant-tabs-tabpane");
|
|
const paneId = owningTabPane?.getAttribute?.("id") || null;
|
|
const owningTabButton = paneId
|
|
? document.querySelector(`[role="tab"][aria-controls="${paneId.replace(/"/g, '\\"')}"]`)
|
|
: null;
|
|
const tabLabel = owningTabButton?.textContent?.trim() || null;
|
|
|
|
return {
|
|
owningTabPane,
|
|
owningTabButton,
|
|
tabLabel
|
|
};
|
|
};
|
|
|
|
const openFieldOwningTab = async (namePath) => {
|
|
const { owningTabPane, owningTabButton } = getFieldOwningTabMeta(namePath);
|
|
if (!owningTabPane || owningTabPane.classList.contains("ant-tabs-tabpane-active")) return false;
|
|
|
|
if (!(owningTabButton instanceof HTMLElement)) return false;
|
|
|
|
owningTabButton.click();
|
|
|
|
for (let index = 0; index < 24; index += 1) {
|
|
await waitForAnimationFrames();
|
|
if (owningTabPane.classList.contains("ant-tabs-tabpane-active")) return true;
|
|
}
|
|
|
|
return owningTabPane.classList.contains("ant-tabs-tabpane-active");
|
|
};
|
|
|
|
const scrollToErrorField = (namePath) => {
|
|
const normalizedNamePath = normalizeNamePath(namePath);
|
|
if (!normalizedNamePath.length) return;
|
|
|
|
try {
|
|
form.scrollToField(normalizedNamePath, {
|
|
behavior: "smooth",
|
|
block: "center",
|
|
focus: true
|
|
});
|
|
window.requestAnimationFrame(() => {
|
|
const fallbackNode = getFieldDomNode(normalizedNamePath);
|
|
fallbackNode?.focus?.();
|
|
});
|
|
return;
|
|
} catch {
|
|
const fallbackTarget = document.getElementById(normalizedNamePath[0]?.toString?.() ?? "");
|
|
fallbackTarget?.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "center"
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleErrorClick = async (namePath) => {
|
|
const normalizedNamePath = normalizeNamePath(namePath);
|
|
if (!normalizedNamePath.length) return;
|
|
|
|
const switchedTab = await openFieldOwningTab(normalizedNamePath);
|
|
if (!switchedTab) {
|
|
const navigationDelayMs = onErrorNavigate?.(normalizedNamePath) ?? 0;
|
|
if (navigationDelayMs > 0) {
|
|
window.setTimeout(() => {
|
|
window.requestAnimationFrame(() => {
|
|
scrollToErrorField(normalizedNamePath);
|
|
});
|
|
}, navigationDelayMs);
|
|
return;
|
|
}
|
|
}
|
|
|
|
await waitForAnimationFrames(switchedTab ? 2 : 1);
|
|
scrollToErrorField(normalizedNamePath);
|
|
};
|
|
//if (!form.isFieldsTouched()) return <></>;
|
|
return (
|
|
<Form.Item className="form-fields-changed" shouldUpdate style={{ margin: 0, padding: 0, minHeight: "unset" }}>
|
|
{() => {
|
|
const errors = form
|
|
.getFieldsError()
|
|
.filter((fieldError) => fieldError.errors.length > 0)
|
|
.flatMap((fieldError) => {
|
|
const tabMeta = getFieldOwningTabMeta(fieldError.name);
|
|
|
|
return fieldError.errors.map((errorMessage, errorIndex) => ({
|
|
key: `${(fieldError.name || []).join(".")}-${errorIndex}-${errorMessage}`,
|
|
message: errorMessage,
|
|
namePath: fieldError.name,
|
|
tabLabel: tabMeta.tabLabel
|
|
}));
|
|
});
|
|
|
|
const groupedErrors = errors.reduce((groups, error) => {
|
|
const groupKey = error.tabLabel || "__ungrouped__";
|
|
if (!groups[groupKey]) {
|
|
groups[groupKey] = {
|
|
key: groupKey,
|
|
label: error.tabLabel,
|
|
errors: []
|
|
};
|
|
}
|
|
groups[groupKey].errors.push(error);
|
|
return groups;
|
|
}, {});
|
|
const errorGroups = Object.values(groupedErrors);
|
|
const hasTabbedErrorGroups = errorGroups.some((group) => Boolean(group.label));
|
|
|
|
if (form.isFieldsTouched())
|
|
return (
|
|
<Space orientation="vertical" style={{ width: "100%", marginBottom: 10 }}>
|
|
<Prompt when={!skipPrompt} beforeUnload={true} message={t("general.messages.unsavedchangespopup")} />
|
|
<AlertComponent
|
|
type="warning"
|
|
title={
|
|
<div>
|
|
<span>{t("general.messages.unsavedchanges")} </span>
|
|
<span
|
|
onClick={handleReset}
|
|
style={{
|
|
cursor: "pointer",
|
|
textDecoration: "underline"
|
|
}}
|
|
>
|
|
{t("general.actions.reset")}
|
|
</span>
|
|
</div>
|
|
}
|
|
/>
|
|
{errors.length > 0 && (
|
|
<AlertComponent
|
|
type="error"
|
|
title={t("general.labels.validationerror")}
|
|
description={
|
|
<div className="form-fields-changed__error-groups">
|
|
{errorGroups.map((group) => (
|
|
<div key={group.key} className="form-fields-changed__error-group">
|
|
{hasTabbedErrorGroups && group.label ? (
|
|
<div className="form-fields-changed__error-group-title">{group.label}</div>
|
|
) : null}
|
|
<ul className="form-fields-changed__error-list">
|
|
{group.errors.map((error) => (
|
|
<li key={error.key}>
|
|
{Array.isArray(error.namePath) && error.namePath.length > 0 ? (
|
|
<button
|
|
type="button"
|
|
className="form-fields-changed__error-link"
|
|
onClick={() => {
|
|
handleErrorClick(error.namePath);
|
|
}}
|
|
>
|
|
{error.message}
|
|
</button>
|
|
) : (
|
|
error.message
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</div>
|
|
}
|
|
showIcon
|
|
/>
|
|
)}
|
|
</Space>
|
|
);
|
|
return <div style={{ display: "none" }}></div>;
|
|
}}
|
|
</Form.Item>
|
|
);
|
|
}
|