mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-21 13:43:20 +00:00
Merge pull request #866 from ae-utbm/openapi
Typescript support and auto generated typescript client API
This commit is contained in:
commit
29b27dc626
@ -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
|
||||
|
||||
|
||||
|
39
docs/howto/js-import-paths.md
Normal file
39
docs/howto/js-import-paths.md
Normal file
@ -0,0 +1,39 @@
|
||||
Vous avez ajouté une application et vous voulez y mettre du javascript ?
|
||||
|
||||
Vous voulez importer depuis cette nouvelle application dans votre script géré par webpack ?
|
||||
|
||||
Eh bien il faut manuellement enregistrer dans node où les trouver et c'est très simple.
|
||||
|
||||
D'abord, il faut ajouter dans node via `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
// ...
|
||||
"imports": {
|
||||
// ...
|
||||
"#mon_app:*": "./mon_app/static/webpack/*"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Ensuite, pour faire fonctionne l'auto-complétion, il faut configurer `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
// ...
|
||||
"paths": {
|
||||
// ...
|
||||
"#mon_app:*": ["./mon_app/static/webpack/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Et c'est tout !
|
||||
|
||||
!!!note
|
||||
|
||||
Il se peut qu'il soit nécessaire de redémarrer `./manage.py runserver` pour
|
||||
que les changements prennent effet.
|
@ -35,8 +35,9 @@ les fichiers sont à mettre dans un dossier `static/webpack` de l'application à
|
||||
Pour accéder au fichier, il faut utiliser `static` comme pour le reste mais en ajouter `webpack/` comme prefix.
|
||||
|
||||
```jinja
|
||||
{# Exemple pour ajouter sith/core/webpack/alpine-index.js #}
|
||||
{# Example pour ajouter sith/core/webpack/alpine-index.js #}
|
||||
<script src="{{ static('webpack/alpine-index.js') }}" defer></script>
|
||||
<script src="{{ static('webpack/other-index.ts') }}" defer></script>
|
||||
```
|
||||
|
||||
!!!note
|
||||
@ -45,6 +46,16 @@ Pour accéder au fichier, il faut utiliser `static` comme pour le reste mais en
|
||||
Les autres fichiers sont disponibles à l'import dans le JavaScript comme
|
||||
si ils étaient tous au même niveau.
|
||||
|
||||
### Les imports au sein des fichiers de webpack
|
||||
|
||||
Pour importer au sein de webpack, il faut préfixer ses imports de `#app:`.
|
||||
|
||||
Exemple:
|
||||
|
||||
```js
|
||||
import { paginated } from "#core:utils/api";
|
||||
```
|
||||
|
||||
## Comment ça fonctionne le post processing ?
|
||||
|
||||
Le post processing est géré par le module `staticfiles`. Les fichiers sont
|
||||
|
@ -72,6 +72,7 @@ nav:
|
||||
- Gérer les migrations: howto/migrations.md
|
||||
- Gérer les traductions: howto/translation.md
|
||||
- Gérer les statics: howto/statics.md
|
||||
- Ajouter un chemin d'import javascript: howto/js-import-paths.md
|
||||
- Configurer pour la production: howto/prod.md
|
||||
- Ajouter un logo de promo: howto/logo.md
|
||||
- Ajouter une cotisation: howto/subscriptions.md
|
||||
|
10
openapi-ts.config.ts
Normal file
10
openapi-ts.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// biome-ignore lint/correctness/noNodejsModules: this only used at compile time
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig } from "@hey-api/openapi-ts";
|
||||
|
||||
// biome-ignore lint/style/noDefaultExport: needed for openapi-ts
|
||||
export default defineConfig({
|
||||
client: "@hey-api/client-fetch",
|
||||
input: resolve(__dirname, "./staticfiles/generated/openapi/schema.json"),
|
||||
output: resolve(__dirname, "./staticfiles/generated/openapi"),
|
||||
});
|
680
package-lock.json
generated
680
package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@hey-api/client-fetch": "^0.4.0",
|
||||
"@zip.js/zip.js": "^2.7.52",
|
||||
"alpinejs": "^3.14.1",
|
||||
"cytoscape": "^3.30.2",
|
||||
@ -26,6 +27,7 @@
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@biomejs/biome": "1.9.3",
|
||||
"@hey-api/openapi-ts": "^0.53.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
@ -33,6 +35,8 @@
|
||||
"mini-css-extract-plugin": "^2.9.1",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
@ -50,6 +54,23 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "11.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.0.tgz",
|
||||
"integrity": "sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/philsturgeon"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
|
||||
@ -1995,6 +2016,41 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/client-fetch": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.4.0.tgz",
|
||||
"integrity": "sha512-T8T3yCl2+AiVVDP6tvfnU/rXOkEHddMTOYCZXUVbydj7URVErh5BelIa8UWBkFYZBP2/mi2nViScNhe9eBolPw=="
|
||||
},
|
||||
"node_modules/@hey-api/openapi-ts": {
|
||||
"version": "0.53.8",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.53.8.tgz",
|
||||
"integrity": "sha512-UbiaIq+JNgG00N/iWYk+LSivOBgWsfGxEHDleWEgQcQr3q7oZJTKL8oH87+KkFDDbUngm1g8lnKI/zLdu1aElQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "11.7.0",
|
||||
"c12": "2.0.1",
|
||||
"commander": "12.1.0",
|
||||
"handlebars": "4.7.8"
|
||||
},
|
||||
"bin": {
|
||||
"openapi-ts": "bin/index.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/openapi-ts/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@ -2098,6 +2154,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@ -2542,6 +2604,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/babel-loader": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz",
|
||||
@ -2679,6 +2747,18 @@
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.23.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
|
||||
@ -2717,6 +2797,34 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/c12": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz",
|
||||
"integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.1",
|
||||
"confbox": "^0.1.7",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"giget": "^1.2.3",
|
||||
"jiti": "^2.3.0",
|
||||
"mlly": "^1.7.1",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^1.1.2",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^1.2.0",
|
||||
"rc9": "^2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"magicast": "^0.3.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"magicast": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-api": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
|
||||
@ -2792,6 +2900,30 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
|
||||
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
||||
@ -2816,6 +2948,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/citty": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"consola": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-deep": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
|
||||
@ -2883,6 +3024,21 @@
|
||||
"integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz",
|
||||
"integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@ -3280,6 +3436,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz",
|
||||
"integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@ -3335,6 +3503,18 @@
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@ -3485,6 +3665,29 @@
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^8.0.1",
|
||||
"human-signals": "^5.0.0",
|
||||
"is-stream": "^3.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^5.1.0",
|
||||
"onetime": "^6.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/expose-loader": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz",
|
||||
@ -3551,6 +3754,18 @@
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-cache-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz",
|
||||
@ -3689,6 +3904,36 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@ -3707,6 +3952,37 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
||||
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/giget": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz",
|
||||
"integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.4",
|
||||
"node-fetch-native": "^1.6.3",
|
||||
"nypm": "^0.3.8",
|
||||
"ohash": "^1.1.3",
|
||||
"pathe": "^1.1.2",
|
||||
"tar": "^6.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"giget": "dist/cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
|
||||
@ -3750,6 +4026,27 @@
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.8",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
"neo-async": "^2.6.2",
|
||||
"source-map": "^0.6.1",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"handlebars": "bin/handlebars"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"uglify-js": "^3.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@ -3771,6 +4068,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@ -3846,6 +4152,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||
@ -3858,6 +4173,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@ -3920,6 +4247,15 @@
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz",
|
||||
"integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
@ -3944,6 +4280,18 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
@ -4076,6 +4424,19 @@
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@ -4097,6 +4458,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-css-extract-plugin": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz",
|
||||
@ -4184,6 +4557,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
@ -4192,6 +4574,61 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz",
|
||||
"integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.12.1",
|
||||
"pathe": "^1.1.2",
|
||||
"pkg-types": "^1.2.0",
|
||||
"ufo": "^1.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -4262,12 +4699,45 @@
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-native": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz",
|
||||
"integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
@ -4280,6 +4750,47 @@
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz",
|
||||
"integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.2.3",
|
||||
"execa": "^8.0.1",
|
||||
"pathe": "^1.1.2",
|
||||
"pkg-types": "^1.2.0",
|
||||
"ufo": "^1.5.4"
|
||||
},
|
||||
"bin": {
|
||||
"nypm": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.16.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ohash": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz",
|
||||
"integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mimic-fn": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@ -4359,6 +4870,18 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
@ -4389,6 +4912,17 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz",
|
||||
"integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
"mlly": "^1.7.2",
|
||||
"pathe": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
@ -4924,6 +5458,29 @@
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rc9": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
|
||||
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/rechoir": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
|
||||
@ -5297,6 +5854,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stylehacks": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz",
|
||||
@ -5383,6 +5952,38 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.33.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.33.0.tgz",
|
||||
@ -5444,11 +6045,84 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
|
||||
"integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"enhanced-resolve": "^5.0.0",
|
||||
"micromatch": "^4.0.0",
|
||||
"semver": "^7.3.4",
|
||||
"source-map": "^0.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*",
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/typo-js": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz",
|
||||
"integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA=="
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
||||
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.19.3",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
@ -5705,6 +6379,12 @@
|
||||
"integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
|
@ -13,10 +13,16 @@
|
||||
"author": "",
|
||||
"license": "GPL-3.0-only",
|
||||
"sideEffects": [".css"],
|
||||
"imports": {
|
||||
"#openapi": "./staticfiles/generated/openapi/index.ts",
|
||||
"#core:*": "./core/static/webpack/*",
|
||||
"#pedagogy:*": "./pedagogy/static/webpack/*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@biomejs/biome": "1.9.3",
|
||||
"@hey-api/openapi-ts": "^0.53.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
@ -24,11 +30,14 @@
|
||||
"mini-css-extract-plugin": "^2.9.1",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@hey-api/client-fetch": "^0.4.0",
|
||||
"@zip.js/zip.js": "^2.7.52",
|
||||
"alpinejs": "^3.14.1",
|
||||
"cytoscape": "^3.30.2",
|
||||
|
120
pedagogy/static/webpack/pedagogy/guide-index.js
Normal file
120
pedagogy/static/webpack/pedagogy/guide-index.js
Normal file
@ -0,0 +1,120 @@
|
||||
import { uvFetchUvList } from "#openapi";
|
||||
|
||||
const pageDefault = 1;
|
||||
const pageSizeDefault = 100;
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("uv_search", () => ({
|
||||
uvs: {
|
||||
count: 0,
|
||||
next: null,
|
||||
previous: null,
|
||||
results: [],
|
||||
},
|
||||
loading: false,
|
||||
page: pageDefault,
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
page_size: pageSizeDefault,
|
||||
search: "",
|
||||
department: [],
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
credit_type: [],
|
||||
semester: [],
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
to_change: [],
|
||||
pushstate: History.PUSH,
|
||||
|
||||
update: undefined,
|
||||
|
||||
initializeArgs() {
|
||||
const url = new URLSearchParams(window.location.search);
|
||||
this.pushstate = History.REPLACE;
|
||||
|
||||
this.page = Number.parseInt(url.get("page")) || pageDefault;
|
||||
this.page_size = Number.parseInt(url.get("page_size")) || pageSizeDefault;
|
||||
this.search = url.get("search") || "";
|
||||
this.department = url.getAll("department");
|
||||
this.credit_type = url.getAll("credit_type");
|
||||
/* The semester is easier to use on the backend as an enum (spring/autumn/both/none)
|
||||
and easier to use on the frontend as an array ([spring, autumn]).
|
||||
Thus there is some conversion involved when both communicate together */
|
||||
this.semester = url.has("semester") ? url.get("semester").split("_AND_") : [];
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.update = Alpine.debounce(async () => {
|
||||
/* Create the whole url before changing everything all at once */
|
||||
const first = this.to_change.shift();
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: defined in script.js
|
||||
let url = updateQueryString(first.param, first.value, History.NONE);
|
||||
for (const value of this.to_change) {
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: defined in script.js
|
||||
url = updateQueryString(value.param, value.value, History.NONE, url);
|
||||
}
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: defined in script.js
|
||||
updateQueryString(first.param, first.value, this.pushstate, url);
|
||||
await this.fetchData(); /* reload data on form change */
|
||||
this.to_change = [];
|
||||
this.pushstate = History.PUSH;
|
||||
}, 50);
|
||||
|
||||
const searchParams = ["search", "department", "credit_type", "semester"];
|
||||
const paginationParams = ["page", "page_size"];
|
||||
|
||||
for (const param of searchParams) {
|
||||
this.$watch(param, () => {
|
||||
if (this.pushstate !== History.PUSH) {
|
||||
/* This means that we are doing a mass param edit */
|
||||
return;
|
||||
}
|
||||
/* Reset pagination on search */
|
||||
this.page = pageDefault;
|
||||
this.page_size = pageSizeDefault;
|
||||
});
|
||||
}
|
||||
for (const param of searchParams.concat(paginationParams)) {
|
||||
this.$watch(param, (value) => {
|
||||
this.to_change.push({ param: param, value: value });
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
window.addEventListener("popstate", () => {
|
||||
this.initializeArgs();
|
||||
});
|
||||
this.initializeArgs();
|
||||
},
|
||||
|
||||
async fetchData() {
|
||||
this.loading = true;
|
||||
const args = {
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
page_size: this.page_size,
|
||||
};
|
||||
for (const [param, value] of new URL(
|
||||
window.location.href,
|
||||
).searchParams.entries()) {
|
||||
// Deal with array type params
|
||||
if (["credit_type", "department", "semester"].includes(param)) {
|
||||
if (args[param] === undefined) {
|
||||
args[param] = [];
|
||||
}
|
||||
args[param].push(value);
|
||||
} else {
|
||||
args[param] = value;
|
||||
}
|
||||
}
|
||||
this.uvs = (
|
||||
await uvFetchUvList({
|
||||
query: args,
|
||||
})
|
||||
).data;
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
maxPage() {
|
||||
return Math.ceil(this.uvs.count / this.page_size);
|
||||
},
|
||||
}));
|
||||
});
|
@ -9,6 +9,10 @@
|
||||
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_js %}
|
||||
<script src="{{ static('webpack/pedagogy/guide-index.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.6, maximum-scale=2">
|
||||
@ -113,105 +117,6 @@
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ paginate_alpine("page", "max_page()") }}
|
||||
{{ paginate_alpine("page", "maxPage()") }}
|
||||
</div>
|
||||
<script>
|
||||
{#
|
||||
How does this work :
|
||||
|
||||
The page contains two main elements : the form and the results.
|
||||
The form contains multiple inputs, allowing the user to apply the filter of its choice.
|
||||
Each modification of those filters will modify the GET parameters of the URL,
|
||||
then fetch the corresponding data from the API.
|
||||
This data will then be displayed on the result part of the page.
|
||||
#}
|
||||
const pageDefault = 1;
|
||||
const pageSizeDefault = 100;
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("uv_search", () => ({
|
||||
uvs: [],
|
||||
loading: false,
|
||||
page: pageDefault,
|
||||
pageSize: pageSizeDefault,
|
||||
search: "",
|
||||
department: [],
|
||||
credit_type: [],
|
||||
semester: [],
|
||||
to_change: [],
|
||||
pushstate: History.PUSH,
|
||||
|
||||
update: undefined,
|
||||
|
||||
async initializeArgs() {
|
||||
let url = new URLSearchParams(window.location.search);
|
||||
this.pushstate = History.REPLACE;
|
||||
|
||||
this.page = parseInt(url.get("page")) || pageDefault;;
|
||||
this.pageSize = parseInt(url.get("pageSize")) || pageSizeDefault;
|
||||
this.search = url.get("search") || "";
|
||||
this.department = url.getAll("department");
|
||||
this.credit_type = url.getAll("credit_type");
|
||||
{# The semester is easier to use on the backend as an enum (spring/autumn/both/none)
|
||||
and easier to use on the frontend as an array ([spring, autumn]).
|
||||
Thus there is some conversion involved when both communicate together #}
|
||||
this.semester = url.has("semester") ?
|
||||
url.get("semester").split("_AND_") : [];
|
||||
|
||||
this.update()
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.update = Alpine.debounce(async () => {
|
||||
{# Create the whole url before changing everything all at once #}
|
||||
let first = this.to_change.shift();
|
||||
let url = updateQueryString(first.param, first.value, History.NONE);
|
||||
this.to_change.forEach((value) => {
|
||||
url = updateQueryString(value.param, value.value, History.NONE, url);
|
||||
})
|
||||
updateQueryString(first.param, first.value, this.pushstate, url);
|
||||
await this.fetchData(); {# reload data on form change #}
|
||||
this.to_change = [];
|
||||
this.pushstate = History.PUSH;
|
||||
}, 50);
|
||||
|
||||
let search_params = ["search", "department", "credit_type", "semester"];
|
||||
let pagination_params = ["page", "pageSize"];
|
||||
|
||||
search_params.forEach((param) => {
|
||||
this.$watch(param, async (value) => {
|
||||
if (this.pushstate != History.PUSH){
|
||||
{# This means that we are doing a mass param edit #}
|
||||
return;
|
||||
}
|
||||
{# Reset pagination on search #}
|
||||
this.page = pageDefault;
|
||||
this.pageSize = pageSizeDefault;
|
||||
});
|
||||
});
|
||||
search_params.concat(pagination_params).forEach((param) => {
|
||||
this.$watch(param, async (value) => {
|
||||
this.to_change.push({ param: param, value: value })
|
||||
this.update();
|
||||
});
|
||||
});
|
||||
window.addEventListener("popstate", async (event) => {
|
||||
await this.initializeArgs();
|
||||
});
|
||||
await this.initializeArgs();
|
||||
},
|
||||
|
||||
|
||||
async fetchData() {
|
||||
this.loading = true;
|
||||
const url = "{{ url("api:fetch_uvs") }}" + window.location.search;
|
||||
this.uvs = await (await fetch(url)).json();
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
max_page() {
|
||||
return Math.ceil(this.uvs.count / this.pageSize);
|
||||
}
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
{% endblock content %}
|
||||
|
@ -1,268 +0,0 @@
|
||||
/**
|
||||
* @typedef PictureIdentification
|
||||
* @property {number} id The actual id of the identification
|
||||
* @property {UserProfile} user The identified user
|
||||
*/
|
||||
|
||||
/**
|
||||
* A container for a picture with the users identified on it
|
||||
* able to prefetch its data.
|
||||
*/
|
||||
class PictureWithIdentifications {
|
||||
identifications = null;
|
||||
imageLoading = false;
|
||||
identificationsLoading = false;
|
||||
|
||||
/**
|
||||
* @param {Picture} picture
|
||||
*/
|
||||
constructor(picture) {
|
||||
Object.assign(this, picture);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Picture} picture
|
||||
*/
|
||||
static fromPicture(picture) {
|
||||
return new PictureWithIdentifications(picture);
|
||||
}
|
||||
|
||||
/**
|
||||
* If not already done, fetch the users identified on this picture and
|
||||
* populate the identifications field
|
||||
* @param {?Object=} options
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async loadIdentifications(options) {
|
||||
if (this.identificationsLoading) {
|
||||
return; // The users are already being fetched.
|
||||
}
|
||||
if (!!this.identifications && !options?.forceReload) {
|
||||
// The users are already fetched
|
||||
// and the user does not want to force the reload
|
||||
return;
|
||||
}
|
||||
this.identificationsLoading = true;
|
||||
const url = `/api/sas/picture/${this.id}/identified`;
|
||||
this.identifications = await (await fetch(url)).json();
|
||||
this.identificationsLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload the photo and the identifications
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async preload() {
|
||||
const img = new Image();
|
||||
img.src = this.compressed_url;
|
||||
if (!img.complete) {
|
||||
this.imageLoading = true;
|
||||
img.addEventListener("load", () => {
|
||||
this.imageLoading = false;
|
||||
});
|
||||
}
|
||||
await this.loadIdentifications();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("picture_viewer", () => ({
|
||||
/**
|
||||
* All the pictures that can be displayed on this picture viewer
|
||||
* @type PictureWithIdentifications[]
|
||||
**/
|
||||
pictures: [],
|
||||
/**
|
||||
* The currently displayed picture
|
||||
* Default dummy data are pre-loaded to avoid javascript error
|
||||
* when loading the page at the beginning
|
||||
* @type PictureWithIdentifications
|
||||
**/
|
||||
currentPicture: {
|
||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
||||
is_moderated: true,
|
||||
id: null,
|
||||
name: "",
|
||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
||||
display_name: "",
|
||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
||||
compressed_url: "",
|
||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
||||
profile_url: "",
|
||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
||||
full_size_url: "",
|
||||
owner: "",
|
||||
date: new Date(),
|
||||
identifications: [],
|
||||
},
|
||||
/**
|
||||
* The picture which will be displayed next if the user press the "next" button
|
||||
* @type ?PictureWithIdentifications
|
||||
**/
|
||||
nextPicture: null,
|
||||
/**
|
||||
* The picture which will be displayed next if the user press the "previous" button
|
||||
* @type ?PictureWithIdentifications
|
||||
**/
|
||||
previousPicture: null,
|
||||
/**
|
||||
* The select2 component used to identify users
|
||||
**/
|
||||
selector: undefined,
|
||||
/**
|
||||
* true if the page is in a loading state, else false
|
||||
**/
|
||||
/**
|
||||
* Error message when a moderation operation fails
|
||||
* @type string
|
||||
**/
|
||||
moderationError: "",
|
||||
/**
|
||||
* Method of pushing new url to the browser history
|
||||
* Used by popstate event and always reset to it's default value when used
|
||||
* @type History
|
||||
**/
|
||||
pushstate: History.PUSH,
|
||||
|
||||
async init() {
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
|
||||
this.pictures = (await fetchPaginated(pictureEndpoint)).map(
|
||||
PictureWithIdentifications.fromPicture,
|
||||
);
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
|
||||
this.selector = sithSelect2({
|
||||
element: $(this.$refs.search),
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
|
||||
dataSource: remoteDataSource("/api/user/search", {
|
||||
excluded: () => [
|
||||
...(this.currentPicture.identifications || []).map((i) => i.user.id),
|
||||
],
|
||||
resultConverter: (obj) => new Object({ ...obj, text: obj.display_name }),
|
||||
}),
|
||||
pictureGetter: (user) => user.profile_pict,
|
||||
});
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from picture.jinja
|
||||
this.currentPicture = this.pictures.find((i) => i.id === firstPictureId);
|
||||
this.$watch("currentPicture", (current, previous) => {
|
||||
if (current === previous) {
|
||||
/* Avoid recursive updates */
|
||||
return;
|
||||
}
|
||||
this.updatePicture();
|
||||
});
|
||||
window.addEventListener("popstate", async (event) => {
|
||||
if (!event.state || event.state.sasPictureId === undefined) {
|
||||
return;
|
||||
}
|
||||
this.pushstate = History.REPLACE;
|
||||
this.currentPicture = this.pictures.find(
|
||||
(i) => i.id === Number.parseInt(event.state.sasPictureId),
|
||||
);
|
||||
});
|
||||
this.pushstate = History.REPLACE; /* Avoid first url push */
|
||||
await this.updatePicture();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the page.
|
||||
* Called when the `currentPicture` property changes.
|
||||
*
|
||||
* The url is modified without reloading the page,
|
||||
* and the previous picture, the next picture and
|
||||
* the list of identified users are updated.
|
||||
*/
|
||||
async updatePicture() {
|
||||
const updateArgs = [
|
||||
{ sasPictureId: this.currentPicture.id },
|
||||
"",
|
||||
`/sas/picture/${this.currentPicture.id}/`,
|
||||
];
|
||||
if (this.pushstate === History.REPLACE) {
|
||||
window.history.replaceState(...updateArgs);
|
||||
this.pushstate = History.PUSH;
|
||||
} else {
|
||||
window.history.pushState(...updateArgs);
|
||||
}
|
||||
|
||||
this.moderationError = "";
|
||||
const index = this.pictures.indexOf(this.currentPicture);
|
||||
this.previousPicture = this.pictures[index - 1] || null;
|
||||
this.nextPicture = this.pictures[index + 1] || null;
|
||||
await this.currentPicture.loadIdentifications();
|
||||
this.$refs.mainPicture?.addEventListener("load", () => {
|
||||
// once the current picture is loaded,
|
||||
// start preloading the next and previous pictures
|
||||
this.nextPicture?.preload();
|
||||
this.previousPicture?.preload();
|
||||
});
|
||||
},
|
||||
|
||||
async moderatePicture() {
|
||||
const res = await fetch(`/api/sas/picture/${this.currentPicture.id}/moderate`, {
|
||||
method: "PATCH",
|
||||
});
|
||||
if (!res.ok) {
|
||||
this.moderationError = `${gettext("Couldn't moderate picture")} : ${res.statusText}`;
|
||||
return;
|
||||
}
|
||||
this.currentPicture.is_moderated = true;
|
||||
this.currentPicture.askedForRemoval = false;
|
||||
},
|
||||
|
||||
async deletePicture() {
|
||||
const res = await fetch(`/api/sas/picture/${this.currentPicture.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!res.ok) {
|
||||
this.moderationError = `${gettext("Couldn't delete picture")} : ${res.statusText}`;
|
||||
return;
|
||||
}
|
||||
this.pictures.splice(this.pictures.indexOf(this.currentPicture), 1);
|
||||
if (this.pictures.length === 0) {
|
||||
// The deleted picture was the only one in the list.
|
||||
// As the album is now empty, go back to the parent page
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja
|
||||
document.location.href = albumUrl;
|
||||
}
|
||||
this.currentPicture = this.nextPicture || this.previousPicture;
|
||||
},
|
||||
|
||||
/**
|
||||
* Send the identification request and update the list of identified users.
|
||||
*/
|
||||
async submitIdentification() {
|
||||
const url = `/api/sas/picture/${this.currentPicture.id}/identified`;
|
||||
await fetch(url, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(this.selector.val().map((i) => Number.parseInt(i))),
|
||||
});
|
||||
// refresh the identified users list
|
||||
await this.currentPicture.loadIdentifications({ forceReload: true });
|
||||
this.selector.empty().trigger("change");
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an identification can be removed by the currently logged user
|
||||
* @param {PictureIdentification} identification
|
||||
* @return {boolean}
|
||||
*/
|
||||
canBeRemoved(identification) {
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja
|
||||
return userIsSasAdmin || identification.user.id === userId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Untag a user from the current picture
|
||||
* @param {PictureIdentification} identification
|
||||
*/
|
||||
async removeIdentification(identification) {
|
||||
const res = await fetch(`/api/sas/relation/${identification.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (res.ok && Array.isArray(this.currentPicture.identifications)) {
|
||||
this.currentPicture.identifications =
|
||||
this.currentPicture.identifications.filter((i) => i.id !== identification.id);
|
||||
}
|
||||
},
|
||||
}));
|
||||
});
|
302
sas/static/webpack/sas/viewer-index.js
Normal file
302
sas/static/webpack/sas/viewer-index.js
Normal file
@ -0,0 +1,302 @@
|
||||
import { paginated } from "#core:utils/api";
|
||||
import {
|
||||
picturesDeletePicture,
|
||||
picturesFetchIdentifications,
|
||||
picturesFetchPictures,
|
||||
picturesIdentifyUsers,
|
||||
picturesModeratePicture,
|
||||
usersidentifiedDeleteRelation,
|
||||
} from "#openapi";
|
||||
|
||||
/**
|
||||
* @typedef PictureIdentification
|
||||
* @property {number} id The actual id of the identification
|
||||
* @property {UserProfile} user The identified user
|
||||
*/
|
||||
|
||||
/**
|
||||
* A container for a picture with the users identified on it
|
||||
* able to prefetch its data.
|
||||
*/
|
||||
class PictureWithIdentifications {
|
||||
identifications = null;
|
||||
imageLoading = false;
|
||||
identificationsLoading = false;
|
||||
|
||||
/**
|
||||
* @param {Picture} picture
|
||||
*/
|
||||
constructor(picture) {
|
||||
Object.assign(this, picture);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Picture} picture
|
||||
*/
|
||||
static fromPicture(picture) {
|
||||
return new PictureWithIdentifications(picture);
|
||||
}
|
||||
|
||||
/**
|
||||
* If not already done, fetch the users identified on this picture and
|
||||
* populate the identifications field
|
||||
* @param {?Object=} options
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async loadIdentifications(options) {
|
||||
if (this.identificationsLoading) {
|
||||
return; // The users are already being fetched.
|
||||
}
|
||||
if (!!this.identifications && !options?.forceReload) {
|
||||
// The users are already fetched
|
||||
// and the user does not want to force the reload
|
||||
return;
|
||||
}
|
||||
this.identificationsLoading = true;
|
||||
this.identifications = (
|
||||
await picturesFetchIdentifications({
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
path: { picture_id: this.id },
|
||||
})
|
||||
).data;
|
||||
this.identificationsLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload the photo and the identifications
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async preload() {
|
||||
const img = new Image();
|
||||
img.src = this.compressed_url;
|
||||
if (!img.complete) {
|
||||
this.imageLoading = true;
|
||||
img.addEventListener("load", () => {
|
||||
this.imageLoading = false;
|
||||
});
|
||||
}
|
||||
await this.loadIdentifications();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef ViewerConfig
|
||||
* @property {number} userId Id of the user to get the pictures from
|
||||
* @property {number} albumId Id of the album to displlay
|
||||
* @property {number} firstPictureId id of the first picture to load on the page
|
||||
* @property {bool} userIsSasAdmin if the user is sas admin
|
||||
**/
|
||||
|
||||
/**
|
||||
* Load user picture page with a nice download bar
|
||||
* @param {ViewerConfig} config
|
||||
**/
|
||||
window.loadViewer = (config) => {
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("picture_viewer", () => ({
|
||||
/**
|
||||
* All the pictures that can be displayed on this picture viewer
|
||||
* @type PictureWithIdentifications[]
|
||||
**/
|
||||
pictures: [],
|
||||
/**
|
||||
* The currently displayed picture
|
||||
* Default dummy data are pre-loaded to avoid javascript error
|
||||
* when loading the page at the beginning
|
||||
* @type PictureWithIdentifications
|
||||
**/
|
||||
currentPicture: {
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
is_moderated: true,
|
||||
id: null,
|
||||
name: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
display_name: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
compressed_url: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
profile_url: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
full_size_url: "",
|
||||
owner: "",
|
||||
date: new Date(),
|
||||
identifications: [],
|
||||
},
|
||||
/**
|
||||
* The picture which will be displayed next if the user press the "next" button
|
||||
* @type ?PictureWithIdentifications
|
||||
**/
|
||||
nextPicture: null,
|
||||
/**
|
||||
* The picture which will be displayed next if the user press the "previous" button
|
||||
* @type ?PictureWithIdentifications
|
||||
**/
|
||||
previousPicture: null,
|
||||
/**
|
||||
* The select2 component used to identify users
|
||||
**/
|
||||
selector: undefined,
|
||||
/**
|
||||
* true if the page is in a loading state, else false
|
||||
**/
|
||||
/**
|
||||
* Error message when a moderation operation fails
|
||||
* @type string
|
||||
**/
|
||||
moderationError: "",
|
||||
/**
|
||||
* Method of pushing new url to the browser history
|
||||
* Used by popstate event and always reset to it's default value when used
|
||||
* @type History
|
||||
**/
|
||||
pushstate: History.PUSH,
|
||||
|
||||
async init() {
|
||||
this.pictures = (
|
||||
await paginated(picturesFetchPictures, {
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
query: { album_id: config.albumId },
|
||||
})
|
||||
).map(PictureWithIdentifications.fromPicture);
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from sith-select2.js
|
||||
this.selector = sithSelect2({
|
||||
element: $(this.$refs.search),
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from sith-select2.js
|
||||
dataSource: remoteDataSource("/api/user/search", {
|
||||
excluded: () => [
|
||||
...(this.currentPicture.identifications || []).map((i) => i.user.id),
|
||||
],
|
||||
resultConverter: (obj) => new Object({ ...obj, text: obj.display_name }),
|
||||
}),
|
||||
pictureGetter: (user) => user.profile_pict,
|
||||
});
|
||||
this.currentPicture = this.pictures.find((i) => i.id === config.firstPictureId);
|
||||
this.$watch("currentPicture", (current, previous) => {
|
||||
if (current === previous) {
|
||||
/* Avoid recursive updates */
|
||||
return;
|
||||
}
|
||||
this.updatePicture();
|
||||
});
|
||||
window.addEventListener("popstate", async (event) => {
|
||||
if (!event.state || event.state.sasPictureId === undefined) {
|
||||
return;
|
||||
}
|
||||
this.pushstate = History.REPLACE;
|
||||
this.currentPicture = this.pictures.find(
|
||||
(i) => i.id === Number.parseInt(event.state.sasPictureId),
|
||||
);
|
||||
});
|
||||
this.pushstate = History.REPLACE; /* Avoid first url push */
|
||||
await this.updatePicture();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the page.
|
||||
* Called when the `currentPicture` property changes.
|
||||
*
|
||||
* The url is modified without reloading the page,
|
||||
* and the previous picture, the next picture and
|
||||
* the list of identified users are updated.
|
||||
*/
|
||||
async updatePicture() {
|
||||
const updateArgs = [
|
||||
{ sasPictureId: this.currentPicture.id },
|
||||
"",
|
||||
`/sas/picture/${this.currentPicture.id}/`,
|
||||
];
|
||||
if (this.pushstate === History.REPLACE) {
|
||||
window.history.replaceState(...updateArgs);
|
||||
this.pushstate = History.PUSH;
|
||||
} else {
|
||||
window.history.pushState(...updateArgs);
|
||||
}
|
||||
|
||||
this.moderationError = "";
|
||||
const index = this.pictures.indexOf(this.currentPicture);
|
||||
this.previousPicture = this.pictures[index - 1] || null;
|
||||
this.nextPicture = this.pictures[index + 1] || null;
|
||||
await this.currentPicture.loadIdentifications();
|
||||
this.$refs.mainPicture?.addEventListener("load", () => {
|
||||
// once the current picture is loaded,
|
||||
// start preloading the next and previous pictures
|
||||
this.nextPicture?.preload();
|
||||
this.previousPicture?.preload();
|
||||
});
|
||||
},
|
||||
|
||||
async moderatePicture() {
|
||||
const res = await picturesModeratePicture({
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
path: { picture_id: this.currentPicture.id },
|
||||
});
|
||||
if (res.error) {
|
||||
this.moderationError = `${gettext("Couldn't moderate picture")} : ${res.statusText}`;
|
||||
return;
|
||||
}
|
||||
this.currentPicture.is_moderated = true;
|
||||
this.currentPicture.askedForRemoval = false;
|
||||
},
|
||||
|
||||
async deletePicture() {
|
||||
const res = await picturesDeletePicture({
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
path: { picture_id: this.currentPicture.id },
|
||||
});
|
||||
if (res.error) {
|
||||
this.moderationError = `${gettext("Couldn't delete picture")} : ${res.statusText}`;
|
||||
return;
|
||||
}
|
||||
this.pictures.splice(this.pictures.indexOf(this.currentPicture), 1);
|
||||
if (this.pictures.length === 0) {
|
||||
// The deleted picture was the only one in the list.
|
||||
// As the album is now empty, go back to the parent page
|
||||
document.location.href = config.albumUrl;
|
||||
}
|
||||
this.currentPicture = this.nextPicture || this.previousPicture;
|
||||
},
|
||||
|
||||
/**
|
||||
* Send the identification request and update the list of identified users.
|
||||
*/
|
||||
async submitIdentification() {
|
||||
await picturesIdentifyUsers({
|
||||
path: {
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
picture_id: this.currentPicture.id,
|
||||
},
|
||||
body: this.selector.val().map((i) => Number.parseInt(i)),
|
||||
});
|
||||
// refresh the identified users list
|
||||
await this.currentPicture.loadIdentifications({ forceReload: true });
|
||||
this.selector.empty().trigger("change");
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an identification can be removed by the currently logged user
|
||||
* @param {PictureIdentification} identification
|
||||
* @return {boolean}
|
||||
*/
|
||||
canBeRemoved(identification) {
|
||||
return config.userIsSasAdmin || identification.user.id === config.userId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Untag a user from the current picture
|
||||
* @param {PictureIdentification} identification
|
||||
*/
|
||||
async removeIdentification(identification) {
|
||||
const res = await usersidentifiedDeleteRelation({
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
path: { relation_id: identification.id },
|
||||
});
|
||||
if (!res.error && Array.isArray(this.currentPicture.identifications)) {
|
||||
this.currentPicture.identifications =
|
||||
this.currentPicture.identifications.filter(
|
||||
(i) => i.id !== identification.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
}));
|
||||
});
|
||||
};
|
@ -5,7 +5,7 @@
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block additional_js -%}
|
||||
<script defer src="{{ static("sas/js/viewer.js") }}"></script>
|
||||
<script defer src="{{ static("webpack/sas/viewer-index.js") }}"></script>
|
||||
{%- endblock -%}
|
||||
|
||||
{% block title %}
|
||||
@ -171,10 +171,14 @@
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
const pictureEndpoint = "{{ url("api:pictures") + "?album_id=" + album.id|string }}";
|
||||
const albumUrl = "{{ album.get_absolute_url() }}";
|
||||
const firstPictureId = {{ picture.id }}; {# id of the first picture to show after page load #}
|
||||
const userId = {{ user.id }};
|
||||
const userIsSasAdmin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
loadViewer({
|
||||
albumId: {{ album.id }} ,
|
||||
albumUrl: "{{ album.get_absolute_url() }}",
|
||||
firstPictureId: {{ picture.id }}, {# id of the first picture to show after page load #}
|
||||
userId: {{ user.id }},
|
||||
userIsSasAdmin: {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -5,8 +5,10 @@ from django.contrib.staticfiles.apps import StaticFilesConfig
|
||||
GENERATED_ROOT = Path(__file__).parent.resolve() / "generated"
|
||||
IGNORE_PATTERNS_WEBPACK = ["webpack/*"]
|
||||
IGNORE_PATTERNS_SCSS = ["*.scss"]
|
||||
IGNORE_PATTERNS_TYPESCRIPT = ["*.ts"]
|
||||
IGNORE_PATTERNS = [
|
||||
*StaticFilesConfig.ignore_patterns,
|
||||
*IGNORE_PATTERNS_TYPESCRIPT,
|
||||
*IGNORE_PATTERNS_WEBPACK,
|
||||
*IGNORE_PATTERNS_SCSS,
|
||||
]
|
||||
|
@ -7,7 +7,7 @@ from django.contrib.staticfiles.management.commands.collectstatic import (
|
||||
)
|
||||
|
||||
from staticfiles.apps import GENERATED_ROOT, IGNORE_PATTERNS_SCSS
|
||||
from staticfiles.processors import Scss, Webpack
|
||||
from staticfiles.processors import OpenApi, Scss, Webpack
|
||||
|
||||
|
||||
class Command(CollectStatic):
|
||||
@ -50,6 +50,7 @@ class Command(CollectStatic):
|
||||
return Path(location)
|
||||
|
||||
Scss.compile(self.collect_scss())
|
||||
OpenApi.compile() # This needs to be prior to webpack
|
||||
Webpack.compile()
|
||||
|
||||
collected = super().collect()
|
||||
|
@ -6,13 +6,15 @@ from django.contrib.staticfiles.management.commands.runserver import (
|
||||
)
|
||||
from django.utils.autoreload import DJANGO_AUTORELOAD_ENV
|
||||
|
||||
from staticfiles.processors import Webpack
|
||||
from staticfiles.processors import OpenApi, Webpack
|
||||
|
||||
|
||||
class Command(Runserver):
|
||||
"""Light wrapper around the statics runserver that integrates webpack auto bundling"""
|
||||
|
||||
def run(self, **options):
|
||||
# OpenApi generation needs to be before webpack
|
||||
OpenApi.compile()
|
||||
# Only run webpack server when debug is enabled
|
||||
# Also protects from re-launching the server if django reloads it
|
||||
if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and settings.DEBUG:
|
||||
|
@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from hashlib import sha1
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
@ -8,6 +9,7 @@ import rjsmin
|
||||
import sass
|
||||
from django.conf import settings
|
||||
|
||||
from sith.urls import api
|
||||
from staticfiles.apps import GENERATED_ROOT
|
||||
|
||||
|
||||
@ -71,3 +73,38 @@ class JS:
|
||||
minified = rjsmin.jsmin(p.read_text())
|
||||
p.write_text(minified)
|
||||
logging.getLogger("main").info(f"Minified {path}")
|
||||
|
||||
|
||||
class OpenApi:
|
||||
OPENAPI_DIR = GENERATED_ROOT / "openapi"
|
||||
|
||||
@classmethod
|
||||
def compile(cls):
|
||||
"""Compile a typescript client for the sith API. Only generates it if it changed"""
|
||||
logging.getLogger("django").info("Compiling open api typescript client")
|
||||
out = cls.OPENAPI_DIR / "schema.json"
|
||||
cls.OPENAPI_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
old_hash = ""
|
||||
if out.exists():
|
||||
with open(out, "rb") as f:
|
||||
old_hash = sha1(f.read()).hexdigest()
|
||||
|
||||
schema = api.get_openapi_schema()
|
||||
# Remove hash from operationIds
|
||||
# This is done for cache invalidation but this is too aggressive
|
||||
for path in schema["paths"].values():
|
||||
for action, desc in path.items():
|
||||
path[action]["operationId"] = "_".join(
|
||||
desc["operationId"].split("_")[:-1]
|
||||
)
|
||||
schema = str(schema)
|
||||
|
||||
if old_hash == sha1(schema.encode("utf-8")).hexdigest():
|
||||
logging.getLogger("django").info("✨ Api did not change, nothing to do ✨")
|
||||
return
|
||||
|
||||
with open(out, "w") as f:
|
||||
_ = f.write(schema)
|
||||
|
||||
subprocess.run(["npx", "openapi-ts"]).check_returncode()
|
||||
|
@ -12,7 +12,7 @@ from staticfiles.processors import JS, Scss
|
||||
|
||||
class ManifestPostProcessingStorage(ManifestStaticFilesStorage):
|
||||
def url(self, name: str, *, force: bool = False) -> str:
|
||||
"""Get the URL for a file, convert .scss calls to .css ones"""
|
||||
"""Get the URL for a file, convert .scss calls to .css ones and .ts to .js"""
|
||||
# This name swap has to be done here
|
||||
# Otherwise, the manifest isn't aware of the file and can't work properly
|
||||
path = Path(name)
|
||||
@ -27,6 +27,9 @@ class ManifestPostProcessingStorage(ManifestStaticFilesStorage):
|
||||
)
|
||||
name = str(path.with_suffix(".css"))
|
||||
|
||||
elif path.suffix == ".ts":
|
||||
name = str(path.with_suffix(".js"))
|
||||
|
||||
return super().url(name, force=force)
|
||||
|
||||
def post_process(
|
||||
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./staticfiles/generated/webpack/",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "es6",
|
||||
"target": "es5",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"#openapi": ["./staticfiles/generated/openapi/index.ts"],
|
||||
"#core:*": ["./core/static/webpack/*"],
|
||||
"#pedagogy:*": ["./pedagogy/static/webpack/*"]
|
||||
}
|
||||
}
|
||||
}
|
@ -6,25 +6,33 @@ const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
|
||||
module.exports = {
|
||||
entry: glob.sync("./!(static)/static/webpack/**/*?(-)index.js").reduce((obj, el) => {
|
||||
// We include the path inside the webpack folder in the name
|
||||
const relativePath = [];
|
||||
const fullPath = path.parse(el);
|
||||
for (const dir of fullPath.dir.split("/").reverse()) {
|
||||
if (dir === "webpack") {
|
||||
break;
|
||||
entry: glob
|
||||
.sync("./!(static)/static/webpack/**/*?(-)index.[j|t]s?(x)")
|
||||
.reduce((obj, el) => {
|
||||
// We include the path inside the webpack folder in the name
|
||||
const relativePath = [];
|
||||
const fullPath = path.parse(el);
|
||||
for (const dir of fullPath.dir.split("/").reverse()) {
|
||||
if (dir === "webpack") {
|
||||
break;
|
||||
}
|
||||
relativePath.push(dir);
|
||||
}
|
||||
relativePath.push(dir);
|
||||
}
|
||||
relativePath.push(fullPath.name);
|
||||
obj[relativePath.join("/")] = `./${el}`;
|
||||
return obj;
|
||||
}, {}),
|
||||
relativePath.push(fullPath.name);
|
||||
obj[relativePath.join("/")] = `./${el}`;
|
||||
return obj;
|
||||
}, {}),
|
||||
cache: {
|
||||
type: "filesystem", // This reduces typescript compilation time like crazy when you restart the server
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: path.resolve(__dirname, "./staticfiles/generated/webpack"),
|
||||
clean: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"],
|
||||
},
|
||||
plugins: [new MiniCssExtractPlugin()],
|
||||
optimization: {
|
||||
minimizer: [
|
||||
@ -62,6 +70,7 @@ module.exports = {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: ["@babel/preset-env"],
|
||||
cacheDirectory: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -70,6 +79,11 @@ module.exports = {
|
||||
enforce: "pre",
|
||||
use: ["source-map-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: require.resolve("jquery"),
|
||||
loader: "expose-loader",
|
||||
|
Loading…
Reference in New Issue
Block a user