mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-09 19:40:19 +00:00
Merge pull request #866 from ae-utbm/openapi
Typescript support and auto generated typescript client API
This commit is contained in:
@ -123,37 +123,3 @@ function updateQueryString(key, value, action = History.REPLACE, url = null) {
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// TODO : If one day a test workflow is made for JS in this project
|
||||
// please test this function. A all cost.
|
||||
/**
|
||||
* Given a paginated endpoint, fetch all the items of this endpoint,
|
||||
* performing multiple API calls if necessary.
|
||||
* @param {string} url The paginated endpoint to fetch
|
||||
* @return {Promise<Object[]>}
|
||||
*/
|
||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
||||
async function fetchPaginated(url) {
|
||||
const maxPerPage = 199;
|
||||
const paginatedUrl = new URL(url, document.location.origin);
|
||||
paginatedUrl.searchParams.set("page_size", maxPerPage.toString());
|
||||
paginatedUrl.searchParams.set("page", "1");
|
||||
|
||||
const firstPage = await (await fetch(paginatedUrl)).json();
|
||||
const results = firstPage.results;
|
||||
|
||||
const nbPictures = firstPage.count;
|
||||
const nbPages = Math.ceil(nbPictures / maxPerPage);
|
||||
|
||||
if (nbPages > 1) {
|
||||
const promises = [];
|
||||
for (let i = 2; i <= nbPages; i++) {
|
||||
paginatedUrl.searchParams.set("page", i.toString());
|
||||
promises.push(
|
||||
fetch(paginatedUrl).then((res) => res.json().then((json) => json.results)),
|
||||
);
|
||||
}
|
||||
results.push(...(await Promise.all(promises)).flat());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
@ -2,25 +2,21 @@
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import "easymde/src/css/easymde.css";
|
||||
import easyMde from "easymde";
|
||||
|
||||
// This scripts dependens on Alpine but it should be loaded on every page
|
||||
import { markdownRenderMarkdown } from "#openapi";
|
||||
|
||||
/**
|
||||
* Create a new easymde based textarea
|
||||
* @param {HTMLTextAreaElement} textarea to use
|
||||
* @param {string} link to the markdown api
|
||||
**/
|
||||
window.easymdeFactory = (textarea, markdownApiUrl) => {
|
||||
window.easymdeFactory = (textarea) => {
|
||||
const easymde = new easyMde({
|
||||
element: textarea,
|
||||
spellChecker: false,
|
||||
autoDownloadFontAwesome: false,
|
||||
previewRender: Alpine.debounce(async (plainText, preview) => {
|
||||
const res = await fetch(markdownApiUrl, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text: plainText }),
|
||||
});
|
||||
preview.innerHTML = await res.text();
|
||||
preview.innerHTML = (
|
||||
await markdownRenderMarkdown({ body: { text: plainText } })
|
||||
).data;
|
||||
return null;
|
||||
}, 300),
|
||||
forceSync: true, // Avoid validation error on generic create view
|
||||
|
@ -1,16 +1,26 @@
|
||||
import cytoscape from "cytoscape";
|
||||
import cxtmenu from "cytoscape-cxtmenu";
|
||||
import klay from "cytoscape-klay";
|
||||
import { familyGetFamilyGraph } from "#openapi";
|
||||
|
||||
cytoscape.use(klay);
|
||||
cytoscape.use(cxtmenu);
|
||||
|
||||
async function getGraphData(url, godfathersDepth, godchildrenDepth) {
|
||||
const data = await (
|
||||
await fetch(
|
||||
`${url}?godfathers_depth=${godfathersDepth}&godchildren_depth=${godchildrenDepth}`,
|
||||
)
|
||||
).json();
|
||||
async function getGraphData(userId, godfathersDepth, godchildrenDepth) {
|
||||
const data = (
|
||||
await familyGetFamilyGraph({
|
||||
path: {
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
user_id: userId,
|
||||
},
|
||||
query: {
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
godfathers_depth: godfathersDepth,
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
godchildren_depth: godchildrenDepth,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
return [
|
||||
...data.users.map((user) => {
|
||||
return { data: user };
|
||||
@ -160,15 +170,14 @@ function createGraph(container, data, activeUserId) {
|
||||
|
||||
/**
|
||||
* @typedef FamilyGraphConfig
|
||||
* @param {string} apiUrl Base url for fetching the tree as a string
|
||||
* @param {string} activeUser Id of the user to fetch the tree from
|
||||
* @param {number} depthMin Minimum tree depth for godfathers and godchildren
|
||||
* @param {number} depthMax Maximum tree depth for godfathers and godchildren
|
||||
* @property {number} activeUser Id of the user to fetch the tree from
|
||||
* @property {number} depthMin Minimum tree depth for godfathers and godchildren
|
||||
* @property {number} depthMax Maximum tree depth for godfathers and godchildren
|
||||
**/
|
||||
|
||||
/**
|
||||
* Create a family graph of an user
|
||||
* @param {FamilyGraphConfig} Configuration
|
||||
* @param {FamilyGraphConfig} config
|
||||
**/
|
||||
window.loadFamilyGraph = (config) => {
|
||||
document.addEventListener("alpine:init", () => {
|
||||
@ -248,7 +257,7 @@ window.loadFamilyGraph = (config) => {
|
||||
|
||||
async fetchGraphData() {
|
||||
this.graphData = await getGraphData(
|
||||
config.apiUrl,
|
||||
config.activeUser,
|
||||
this.godfathersDepth,
|
||||
this.godchildrenDepth,
|
||||
);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { paginated } from "#core:utils/api";
|
||||
import { HttpReader, ZipWriter } from "@zip.js/zip.js";
|
||||
import { showSaveFilePicker } from "native-file-system-adapter";
|
||||
import { picturesFetchPictures } from "#openapi";
|
||||
|
||||
/**
|
||||
* @typedef UserProfile
|
||||
@ -28,12 +30,12 @@ import { showSaveFilePicker } from "native-file-system-adapter";
|
||||
|
||||
/**
|
||||
* @typedef PicturePageConfig
|
||||
* @param {string} apiUrl Url of the api endpoint to fetch pictures from the user
|
||||
* @property {number} userId Id of the user to get the pictures from
|
||||
**/
|
||||
|
||||
/**
|
||||
* Load user picture page with a nice download bar
|
||||
* @param {PicturePageConfig} Configuration
|
||||
* @param {PicturePageConfig} config
|
||||
**/
|
||||
window.loadPicturePage = (config) => {
|
||||
document.addEventListener("alpine:init", () => {
|
||||
@ -44,8 +46,10 @@ window.loadPicturePage = (config) => {
|
||||
albums: {},
|
||||
|
||||
async init() {
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: imported from script.json
|
||||
this.pictures = await fetchPaginated(config.apiUrl);
|
||||
this.pictures = await paginated(picturesFetchPictures, {
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
query: { users_identified: [config.userId] },
|
||||
});
|
||||
this.albums = this.pictures.reduce((acc, picture) => {
|
||||
if (!acc[picture.album]) {
|
||||
acc[picture.album] = [];
|
||||
|
81
core/static/webpack/utils/api.ts
Normal file
81
core/static/webpack/utils/api.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { Client, Options, RequestResult } from "@hey-api/client-fetch";
|
||||
import { client } from "#openapi";
|
||||
|
||||
interface PaginatedResponse<T> {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: T[];
|
||||
}
|
||||
|
||||
interface PaginatedRequest {
|
||||
query?: {
|
||||
page?: number;
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
page_size?: number;
|
||||
};
|
||||
}
|
||||
|
||||
type PaginatedEndpoint<T> = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<PaginatedRequest, ThrowOnError>,
|
||||
) => RequestResult<PaginatedResponse<T>, unknown, ThrowOnError>;
|
||||
|
||||
// TODO : If one day a test workflow is made for JS in this project
|
||||
// please test this function. A all cost.
|
||||
export const paginated = async <T>(
|
||||
endpoint: PaginatedEndpoint<T>,
|
||||
options?: PaginatedRequest,
|
||||
) => {
|
||||
const maxPerPage = 199;
|
||||
options.query.page_size = maxPerPage;
|
||||
options.query.page = 1;
|
||||
|
||||
const firstPage = (await endpoint(options)).data;
|
||||
const results = firstPage.results;
|
||||
|
||||
const nbElements = firstPage.count;
|
||||
const nbPages = Math.ceil(nbElements / maxPerPage);
|
||||
|
||||
if (nbPages > 1) {
|
||||
const promises: Promise<T[]>[] = [];
|
||||
for (let i = 2; i <= nbPages; i++) {
|
||||
const nextPage = structuredClone(options);
|
||||
nextPage.query.page = i;
|
||||
promises.push(endpoint(nextPage).then((res) => res.data.results));
|
||||
}
|
||||
results.push(...(await Promise.all(promises)).flat());
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
interface Request {
|
||||
client?: Client;
|
||||
}
|
||||
|
||||
interface InterceptorOptions {
|
||||
url: string;
|
||||
}
|
||||
|
||||
type GenericEndpoint = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<Request, ThrowOnError>,
|
||||
) => RequestResult<unknown, unknown, ThrowOnError>;
|
||||
|
||||
/**
|
||||
* Return the endpoint url of the endpoint
|
||||
**/
|
||||
export const makeUrl = async (endpoint: GenericEndpoint) => {
|
||||
let url = "";
|
||||
const interceptor = (_request: undefined, options: InterceptorOptions) => {
|
||||
url = options.url;
|
||||
throw new Error("We don't want to send the request");
|
||||
};
|
||||
|
||||
client.interceptors.request.use(interceptor);
|
||||
try {
|
||||
await endpoint({ client: client });
|
||||
} catch (_error) {
|
||||
/* do nothing */
|
||||
}
|
||||
client.interceptors.request.eject(interceptor);
|
||||
return url;
|
||||
};
|
@ -24,7 +24,7 @@
|
||||
<script src="{{ static('webpack/alpine-index.js') }}" defer></script>
|
||||
<!-- Jquery declared here to be accessible in every django widgets -->
|
||||
<script src="{{ static('webpack/jquery-index.js') }}"></script>
|
||||
<!-- Put here to always have acces to those functions on django widgets -->
|
||||
<!-- Put here to always have access to those functions on django widgets -->
|
||||
<script src="{{ static('core/js/script.js') }}"></script>
|
||||
<script defer src="{{ static('vendored/select2/select2.min.js') }}"></script>
|
||||
<script defer src="{{ static('core/js/sith-select2.js') }}"></script>
|
||||
|
@ -92,8 +92,7 @@
|
||||
<script>
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
loadFamilyGraph({
|
||||
apiUrl: "{{ api_url }}",
|
||||
activeUser: "{{ object.id }}",
|
||||
activeUser: {{ object.id }},
|
||||
depthMin: {{ depth_min }},
|
||||
depthMax: {{ depth_max }},
|
||||
});
|
||||
|
@ -62,9 +62,7 @@
|
||||
{{ super() }}
|
||||
<script>
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
loadPicturePage({
|
||||
apiUrl: "{{ url("api:pictures") }}?users_identified={{ object.id }}"
|
||||
});
|
||||
loadPicturePage({ userId: {{ object.id }} });
|
||||
})
|
||||
</script>
|
||||
{% endblock script %}
|
||||
|
@ -7,9 +7,7 @@
|
||||
<script type="text/javascript">
|
||||
addEventListener("DOMContentLoaded", (event) => {
|
||||
easymdeFactory(
|
||||
document.getElementById("{{ widget.attrs.id }}"),
|
||||
"{{ markdown_api_url }}",
|
||||
);
|
||||
document.getElementById("{{ widget.attrs.id }}"));
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
|
@ -39,7 +39,6 @@ from django.forms import (
|
||||
TextInput,
|
||||
)
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
@ -76,7 +75,6 @@ class MarkdownInput(Textarea):
|
||||
"js": static("webpack/easymde-index.js"),
|
||||
"css": static("webpack/easymde-index.css"),
|
||||
}
|
||||
context["markdown_api_url"] = reverse("api:markdown")
|
||||
return context
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user