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

30
frontend/orval.config.js Normal file
View File

@@ -0,0 +1,30 @@
module.exports = {
public: {
input: { target: "http://localhost:8000/api/schema/" },
output: {
target: "src/api/generated/public.ts",
schemas: "src/api/generated/models",
client: "axios",
override: {
mutator: {
path: "src/api/publicClient.ts",
name: "publicApi",
},
},
},
},
private: {
input: { target: "http://localhost:8000/api/schema/" },
output: {
target: "src/api/generated/private.ts",
schemas: "src/api/generated/models",
client: "axios",
override: {
mutator: {
path: "src/api/privateClient.ts",
name: "privateApi",
},
},
},
},
};

View File

@@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"api:gen": "orval --config orval.config.js"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
@@ -33,6 +34,7 @@
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
"vite": "^7.1.2",
"openapi-generator-cli": "^2.9.0"
}
}

View File

@@ -0,0 +1,25 @@
import fs from "fs";
import path from "path";
import axios from "axios";
// Single config point
const config = { schemaUrl: "/api/schema/", baseUrl: "/api/" };
async function main() {
const outDir = path.resolve("./src/openapi");
const outFile = path.join(outDir, "schema.json");
const base = process.env.VITE_API_BASE_URL || "http://localhost:8000";
const url = new URL(config.schemaUrl, base).toString();
console.log(`[openapi] Fetching schema from ${url}`);
const res = await axios.get(url, { headers: { Accept: "application/json" } });
await fs.promises.mkdir(outDir, { recursive: true });
await fs.promises.writeFile(outFile, JSON.stringify(res.data, null, 2), "utf8");
console.log(`[openapi] Wrote ${outFile}`);
}
main().catch((err) => {
console.error("[openapi] Failed to fetch schema:", err?.message || err);
process.exit(1);
});

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("");