Add choices API endpoint and OpenAPI client setup

Introduces a new /api/choices/ endpoint for fetching model choices with multilingual labels. Updates Django models to use 'cz#' prefix for Czech labels. Adds OpenAPI client generation via orval, refactors frontend API structure, and provides documentation and helper scripts for dynamic choices and OpenAPI usage.
This commit is contained in:
David Bruno Vontor
2025-12-04 17:35:47 +01:00
parent ebab304b75
commit d94ad93222
24 changed files with 281 additions and 76 deletions

View File

@@ -0,0 +1,18 @@
import { publicApi } from "./publicClient";
export async function getChoices(queries: {
model: string;
field: string;
lang?: string;
}[]) {
const params = new URLSearchParams();
queries.forEach((q) => {
params.append("model", q.model);
params.append("field", q.field);
if (q.lang) params.append("lang", q.lang);
});
const { data } = await publicApi.get(`/choices/?${params.toString()}`);
return data; // typ: Array<{ value: string; label: string }>
}

View File

@@ -0,0 +1,9 @@
# 🌈 Choices (dynamic enums)
Získaní možných hodnot pro pole s výběrem (ChoiceField) z backendu s podporou vícejazyčných labelů, definované v modelech Django.
```tsx
const roles = await getChoices([
{ model: "User", field: "role", lang: "cz" },
]);
```

View File

@@ -0,0 +1,7 @@
# Získání seznamu objektů (např. Orders)
```typescript
import { ordersList } from "@/api/generated/private";
const orders = await ordersList();
```

View File

@@ -0,0 +1,12 @@
# 🔐 Přihlášení (public)
```typescript
import { authLoginCreate } from "@/api/generated/public";
await authLoginCreate({
email: "test@test.com",
password: "secret",
});
```

View File

@@ -0,0 +1,15 @@
# 🖼️ Podpora FileField / ImageField
Orval automaticky vytvoří endpointy s multipart/form-data.
Použití:
```typescript
import { productsUpdate } from "@/api/generated/private";
const form = new FormData();
form.append("name", values.name);
form.append("image", fileInput.files[0]);
await productsUpdate({ id: productId, data: form });
```

View File

@@ -0,0 +1 @@
v tehle složce se vygeneruje schema

View File

@@ -1,41 +0,0 @@
import Client from "./Client";
/**
* Loads enum values from an OpenAPI schema for a given path, method, and field (e.g., category).
*
* @param path - API path, e.g., "/api/service-tickets/"
* @param method - HTTP method
* @param field - field name in parameters or request
* @param schemaUrl - URL of the JSON schema, default "/api/schema/?format=json"
* @returns Promise<Array<{ value: string; label: string }>>
*/
export async function fetchEnumFromSchemaJson(
path: string,
method: "get" | "post" | "patch" | "put",
field: string,
schemaUrl: string = "/schema/?format=json"
): Promise<Array<{ value: string; label: string }>> {
try {
const schema = await Client.public.get(schemaUrl);
const methodDef = schema.paths?.[path]?.[method];
if (!methodDef) {
throw new Error(`Method ${method.toUpperCase()} for ${path} not found in schema.`);
}
// Search in "parameters" (e.g., GET query parameters)
const param = methodDef.parameters?.find((p: any) => p.name === field);
if (param?.schema?.enum) {
return param.schema.enum.map((val: string) => ({
value: val,
label: val,
}));
}
throw new Error(`Field '${field}' does not contain enum`);
} catch (error) {
console.error("Error loading enum values:", error);
throw error;
}
}

View File

@@ -1,4 +1,4 @@
import Client from "../Client";
import Client from "./Client";
// Available output containers (must match backend)
export const FORMAT_EXTS = ["mp4", "mkv", "webm", "flv", "mov", "avi", "ogg"] as const;

View File

@@ -2,7 +2,7 @@
// User API model for searching users by username
// Structure matches other model files (see order.js for reference)
import Client from '../Client';
import Client from '../legacy/Client';
const API_BASE_URL = "/account/users";

View File

@@ -0,0 +1,27 @@
import axios from "axios";
// použij tohle pro API vyžadující autentizaci
export const privateApi = axios.create({
baseURL: "/api/",
withCredentials: true, // potřebuje HttpOnly cookies
});
privateApi.interceptors.response.use(
(res) => res,
async (error) => {
const original = error.config;
if (error.response?.status === 401 && !original._retry) {
original._retry = true;
try {
await privateApi.post("/auth/refresh/");
return privateApi(original);
} catch {
// optional: logout
}
}
return Promise.reject(error);
}
);

View File

@@ -0,0 +1,7 @@
import axios from "axios";
// použij tohle pro veřejné API nevyžadující autentizaci
export const publicApi = axios.create({
baseURL: "/api/",
withCredentials: false, // veřejné API NEPOSÍLÁ cookies
});

View File

@@ -5,7 +5,7 @@ import {
FORMAT_EXTS,
type InfoResponse,
parseContentDispositionFilename,
} from "../../api/apps/Downloader";
} from "../../api/legacy/Downloader";
export default function Downloader() {
const [url, setUrl] = useState("");