mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-09 19:40:19 +00:00
Go for a more generic js bundling architecture
* Don't tie the output name to webpack itself * Don't call js bundling webpack in python code * Make the doc more generic about js bundling
This commit is contained in:
83
core/static/bundled/utils/api.ts
Normal file
83
core/static/bundled/utils/api.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import type { Client, Options, RequestResult } from "@hey-api/client-fetch";
|
||||
import { client } from "#openapi";
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: T[];
|
||||
}
|
||||
|
||||
export 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;
|
||||
const queryParams = options ?? {};
|
||||
queryParams.query = queryParams.query ?? {};
|
||||
queryParams.query.page_size = maxPerPage;
|
||||
queryParams.query.page = 1;
|
||||
|
||||
const firstPage = (await endpoint(queryParams)).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(queryParams);
|
||||
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;
|
||||
};
|
21
core/static/bundled/utils/globals.ts
Normal file
21
core/static/bundled/utils/globals.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { Alpine as AlpineType } from "alpinejs";
|
||||
|
||||
declare global {
|
||||
const Alpine: AlpineType;
|
||||
const gettext: (text: string) => string;
|
||||
const interpolate: <T>(fmt: string, args: string[] | T, isNamed?: boolean) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to export typescript functions to regular html and jinja files
|
||||
* Without it, you either have to use the any keyword and suppress warnings or do a
|
||||
* very painful type conversion workaround which is only here to please the linter
|
||||
*
|
||||
* This is only useful if you're using typescript, this is equivalent to doing
|
||||
* window.yourFunction = yourFunction
|
||||
**/
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
|
||||
export function exportToHtml(name: string, func: any) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
|
||||
(window as any)[name] = func;
|
||||
}
|
40
core/static/bundled/utils/history.ts
Normal file
40
core/static/bundled/utils/history.ts
Normal file
@ -0,0 +1,40 @@
|
||||
export enum History {
|
||||
None = 0,
|
||||
Push = 1,
|
||||
Replace = 2,
|
||||
}
|
||||
|
||||
export const initialUrlParams = new URLSearchParams(window.location.search);
|
||||
export const getCurrentUrlParams = () => {
|
||||
return new URLSearchParams(window.location.search);
|
||||
};
|
||||
|
||||
export function updateQueryString(
|
||||
key: string,
|
||||
value?: string | string[],
|
||||
action?: History,
|
||||
url?: string,
|
||||
) {
|
||||
const historyAction = action ?? History.Replace;
|
||||
const ret = new URL(url ?? window.location.href);
|
||||
|
||||
if (value === undefined || value === null || value === "") {
|
||||
// If the value is null, undefined or empty => delete it
|
||||
ret.searchParams.delete(key);
|
||||
} else if (Array.isArray(value)) {
|
||||
ret.searchParams.delete(key);
|
||||
for (const v of value) {
|
||||
ret.searchParams.append(key, v);
|
||||
}
|
||||
} else {
|
||||
ret.searchParams.set(key, value);
|
||||
}
|
||||
|
||||
if (historyAction === History.Push) {
|
||||
window.history.pushState(null, "", ret.toString());
|
||||
} else if (historyAction === History.Replace) {
|
||||
window.history.replaceState(null, "", ret.toString());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
56
core/static/bundled/utils/web-components.ts
Normal file
56
core/static/bundled/utils/web-components.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Class decorator to register components easily
|
||||
* It's a wrapper around window.customElements.define
|
||||
* What's nice about it is that you don't separate the component registration
|
||||
* and the class definition
|
||||
**/
|
||||
export function registerComponent(name: string, options?: ElementDefinitionOptions) {
|
||||
return (component: CustomElementConstructor) => {
|
||||
window.customElements.define(name, component, options);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Safari doesn't support inheriting from HTML tags on web components
|
||||
* The technique is to:
|
||||
* create a new web component
|
||||
* create the desired type inside
|
||||
* pass all attributes to the child component
|
||||
* store is at as `node` inside the parent
|
||||
*
|
||||
* Since we can't use the generic type to instantiate the node, we create a generator function
|
||||
*
|
||||
* ```js
|
||||
* class MyClass extends inheritHtmlElement("select") {
|
||||
* // do whatever
|
||||
* }
|
||||
* ```
|
||||
**/
|
||||
export function inheritHtmlElement<K extends keyof HTMLElementTagNameMap>(tagName: K) {
|
||||
return class Inherited extends HTMLElement {
|
||||
protected node: HTMLElementTagNameMap[K];
|
||||
|
||||
connectedCallback(autoAddNode?: boolean) {
|
||||
this.node = document.createElement(tagName);
|
||||
const attributes: Attr[] = []; // We need to make a copy to delete while iterating
|
||||
for (const attr of this.attributes) {
|
||||
if (attr.name in this.node) {
|
||||
attributes.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr of attributes) {
|
||||
this.removeAttributeNode(attr);
|
||||
this.node.setAttributeNode(attr);
|
||||
}
|
||||
|
||||
this.node.innerHTML = this.innerHTML;
|
||||
this.innerHTML = "";
|
||||
|
||||
// Automatically add node to DOM if autoAddNode is true or unspecified
|
||||
if (autoAddNode === undefined || autoAddNode) {
|
||||
this.appendChild(this.node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user