This commit is contained in:
2026-03-26 15:39:58 +03:30
parent e89c3a1b16
commit 93215a7366
3 changed files with 86 additions and 18 deletions
+66 -9
View File
@@ -28,6 +28,7 @@ export class ApiClient {
constructor(baseURL: string = resolveApiBaseUrl()) { constructor(baseURL: string = resolveApiBaseUrl()) {
this.baseURL = baseURL.replace(/\/$/, ""); // Remove trailing slash this.baseURL = baseURL.replace(/\/$/, ""); // Remove trailing slash
this.defaultHeaders = { this.defaultHeaders = {
Accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
} }
@@ -95,6 +96,57 @@ export class ApiClient {
return headers; return headers;
} }
private prepareBody(
data: any,
headers: Record<string, string>,
): BodyInit | undefined {
if (data === undefined || data === null) return undefined;
if (typeof FormData !== "undefined" && data instanceof FormData) {
delete headers["Content-Type"];
return data;
}
if (
typeof URLSearchParams !== "undefined" &&
data instanceof URLSearchParams
) {
return data;
}
if (typeof Blob !== "undefined" && data instanceof Blob) {
return data;
}
if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer) {
return data;
}
if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(data)) {
return data;
}
const contentType = headers["Content-Type"]?.toLowerCase();
if (contentType?.includes("application/x-www-form-urlencoded")) {
return new URLSearchParams(
Object.entries(data).reduce<Record<string, string>>((acc, [key, value]) => {
if (value !== undefined && value !== null) {
acc[key] = String(value);
}
return acc;
}, {}),
).toString();
}
if (!contentType || contentType.includes("application/json")) {
return typeof data === "string" ? data : JSON.stringify(data);
}
return data as BodyInit;
}
/** /**
* Handle API response * Handle API response
*/ */
@@ -155,10 +207,12 @@ export class ApiClient {
customHeaders?: Record<string, string>, customHeaders?: Record<string, string>,
): Promise<T> { ): Promise<T> {
const url = `${this.baseURL}${endpoint}`; const url = `${this.baseURL}${endpoint}`;
const headers = this.getHeaders(customHeaders);
const response = await fetch(url, { const response = await fetch(url, {
method: "POST", method: "POST",
headers: this.getHeaders(customHeaders), headers,
body: data ? JSON.stringify(data) : undefined, body: this.prepareBody(data, headers),
}); });
return this.handleResponse<T>(response); return this.handleResponse<T>(response);
@@ -173,12 +227,11 @@ export class ApiClient {
customHeaders?: Record<string, string>, customHeaders?: Record<string, string>,
): Promise<T> { ): Promise<T> {
const url = `${this.baseURL}${endpoint}`; const url = `${this.baseURL}${endpoint}`;
const headers = { ...this.getHeaders(customHeaders) }; const headers = this.getHeaders(customHeaders);
delete headers["Content-Type"];
const response = await fetch(url, { const response = await fetch(url, {
method: "POST", method: "POST",
headers, headers,
body: formData, body: this.prepareBody(formData, headers),
}); });
return this.handleResponse<T>(response); return this.handleResponse<T>(response);
@@ -193,10 +246,12 @@ export class ApiClient {
customHeaders?: Record<string, string>, customHeaders?: Record<string, string>,
): Promise<T> { ): Promise<T> {
const url = `${this.baseURL}${endpoint}`; const url = `${this.baseURL}${endpoint}`;
const headers = this.getHeaders(customHeaders);
const response = await fetch(url, { const response = await fetch(url, {
method: "PUT", method: "PUT",
headers: this.getHeaders(customHeaders), headers,
body: data ? JSON.stringify(data) : undefined, body: this.prepareBody(data, headers),
}); });
return this.handleResponse<T>(response); return this.handleResponse<T>(response);
@@ -211,10 +266,12 @@ export class ApiClient {
customHeaders?: Record<string, string>, customHeaders?: Record<string, string>,
): Promise<T> { ): Promise<T> {
const url = `${this.baseURL}${endpoint}`; const url = `${this.baseURL}${endpoint}`;
const headers = this.getHeaders(customHeaders);
const response = await fetch(url, { const response = await fetch(url, {
method: "PATCH", method: "PATCH",
headers: this.getHeaders(customHeaders), headers,
body: data ? JSON.stringify(data) : undefined, body: this.prepareBody(data, headers),
}); });
return this.handleResponse<T>(response); return this.handleResponse<T>(response);
@@ -176,7 +176,7 @@ export const farmDashboardService = {
try { try {
const response = await apiClient.get< const response = await apiClient.get<
ApiResponse<FarmDashboardConfigResponse> | FarmDashboardConfigResponse ApiResponse<FarmDashboardConfigResponse> | FarmDashboardConfigResponse
>("/api/farm-dashboard-config"); >("/api/farm-dashboard-config/");
const raw = response && "data" in response ? response.data : response; const raw = response && "data" in response ? response.data : response;
if ( if (
raw && raw &&
@@ -202,7 +202,7 @@ export const farmDashboardService = {
try { try {
const response = await apiClient.patch< const response = await apiClient.patch<
ApiResponse<FarmDashboardConfigResponse> | FarmDashboardConfigResponse ApiResponse<FarmDashboardConfigResponse> | FarmDashboardConfigResponse
>("/api/farm-dashboard-config", toApiRequest(data)); >("/api/farm-dashboard-config/", toApiRequest(data));
const raw = response && "data" in response ? response.data : response; const raw = response && "data" in response ? response.data : response;
if ( if (
raw && raw &&
@@ -90,6 +90,11 @@ function mergeRowOrderAfterDrag(
return result; return result;
} }
function areStringArraysEqual(left: string[], right: string[]): boolean {
if (left.length !== right.length) return false;
return left.every((value, index) => value === right[index]);
}
const FarmDashboardWrapper = () => { const FarmDashboardWrapper = () => {
const t = useTranslations("farmDashboard"); const t = useTranslations("farmDashboard");
const { setSlotContent } = useContext(NavbarSlotContext); const { setSlotContent } = useContext(NavbarSlotContext);
@@ -149,7 +154,10 @@ const FarmDashboardWrapper = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const disabledSet = new Set(config.disabledCardIds); const disabledSet = useMemo(
() => new Set(config.disabledCardIds),
[config.disabledCardIds],
);
const hasVisibleCard = useCallback( const hasVisibleCard = useCallback(
(rowId: string) => { (rowId: string) => {
@@ -160,7 +168,10 @@ const FarmDashboardWrapper = () => {
[config.disabledCardIds], [config.disabledCardIds],
); );
const visibleRowOrder = config.rowOrder.filter(hasVisibleCard); const visibleRowOrder = useMemo(
() => config.rowOrder.filter(hasVisibleCard),
[config.rowOrder, hasVisibleCard],
);
const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop( const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop(
visibleRowOrder, visibleRowOrder,
@@ -198,18 +209,19 @@ const FarmDashboardWrapper = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (areStringArraysEqual(orderedRows, visibleRowOrder)) return;
setOrderedRows(visibleRowOrder); setOrderedRows(visibleRowOrder);
// eslint-disable-next-line react-hooks/exhaustive-deps }, [orderedRows, setOrderedRows, visibleRowOrder]);
}, [visibleRowOrder]);
useEffect(() => { useEffect(() => {
if (loading) return; if (loading) return;
if (JSON.stringify(orderedRows) === JSON.stringify(visibleRowOrder)) return; if (areStringArraysEqual(orderedRows, visibleRowOrder)) return;
const newRowOrder = mergeRowOrderAfterDrag( const newRowOrder = mergeRowOrderAfterDrag(
config.rowOrder, config.rowOrder,
orderedRows, orderedRows,
visibleRowOrder, visibleRowOrder,
); );
if (areStringArraysEqual(newRowOrder, config.rowOrder)) return;
setConfig((prev) => ({ ...prev, rowOrder: newRowOrder })); setConfig((prev) => ({ ...prev, rowOrder: newRowOrder }));
setSaving(true); setSaving(true);
farmDashboardService farmDashboardService
@@ -217,8 +229,7 @@ const FarmDashboardWrapper = () => {
.then((updated) => setConfig(updated)) .then((updated) => setConfig(updated))
.catch(() => {}) .catch(() => {})
.finally(() => setSaving(false)); .finally(() => setSaving(false));
// eslint-disable-next-line react-hooks/exhaustive-deps }, [config.rowOrder, loading, orderedRows, visibleRowOrder]);
}, [orderedRows]);
const handleToggleDragReorder = useCallback((enabled: boolean) => { const handleToggleDragReorder = useCallback((enabled: boolean) => {
setConfig((prev) => ({ ...prev, enableDragReorder: enabled })); setConfig((prev) => ({ ...prev, enableDragReorder: enabled }));