mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-01 03:48:04 +00:00
Remove select2 from vendored
* Make core/utils/select2.ts * Convert viewer-index.js to typescript
This commit is contained in:
parent
768e2867b5
commit
a5d8c96bab
1
core/static/vendored/select2/select2.min.css
vendored
1
core/static/vendored/select2/select2.min.css
vendored
File diff suppressed because one or more lines are too long
2
core/static/vendored/select2/select2.min.js
vendored
2
core/static/vendored/select2/select2.min.js
vendored
File diff suppressed because one or more lines are too long
6
core/static/webpack/utils/globals.ts
Normal file
6
core/static/webpack/utils/globals.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { Alpine as AlpineType } from "alpinejs";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const Alpine: AlpineType;
|
||||||
|
const gettext: (text: string) => string;
|
||||||
|
}
|
@ -136,52 +136,69 @@
|
|||||||
* </script>
|
* </script>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
import "select2";
|
||||||
* @typedef Select2Object
|
import type {
|
||||||
* @property {number} id
|
AjaxOptions,
|
||||||
* @property {string} text
|
DataFormat,
|
||||||
*/
|
GroupedDataFormat,
|
||||||
|
LoadingData,
|
||||||
|
Options,
|
||||||
|
PlainObject,
|
||||||
|
} from "select2";
|
||||||
|
import "select2/dist/css/select2.css";
|
||||||
|
|
||||||
/**
|
export interface Select2Object {
|
||||||
* @typedef Select2Options
|
id: number;
|
||||||
* @property {Element} element
|
text: string;
|
||||||
* @property {Object} dataSource
|
}
|
||||||
* the data source, built with `localDataSource` or `remoteDataSource`
|
|
||||||
* @property {number[]} excluded A list of ids to exclude from search
|
// biome-ignore lint/suspicious/noExplicitAny: You have to do it at some point
|
||||||
* @property {undefined | function(Object): string} pictureGetter
|
export type RemoteResult = any;
|
||||||
* A callback to get the picture field from the API response
|
export type AjaxResponse = AjaxOptions<DataFormat | GroupedDataFormat, RemoteResult>;
|
||||||
* @property {Object | undefined} overrides
|
|
||||||
* Any other select2 parameter to apply on the config
|
interface DataSource {
|
||||||
*/
|
ajax?: AjaxResponse | undefined;
|
||||||
|
data?: RemoteResult | DataFormat[] | GroupedDataFormat[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Select2Options {
|
||||||
|
element: Element;
|
||||||
|
/** the data source, built with `localDataSource` or `remoteDataSource` */
|
||||||
|
dataSource: DataSource;
|
||||||
|
excluded?: number[];
|
||||||
|
/** A callback to get the picture field from the API response */
|
||||||
|
pictureGetter?: (element: LoadingData | DataFormat | GroupedDataFormat) => string;
|
||||||
|
/** Any other select2 parameter to apply on the config */
|
||||||
|
overrides?: Options;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Select2Options} options
|
* @param {Select2Options} options
|
||||||
*/
|
*/
|
||||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
export function sithSelect2(options: Select2Options) {
|
||||||
function sithSelect2(options) {
|
const elem: PlainObject = $(options.element);
|
||||||
const elem = $(options.element);
|
|
||||||
return elem.select2({
|
return elem.select2({
|
||||||
theme: elem[0].multiple ? "classic" : "default",
|
theme: elem[0].multiple ? "classic" : "default",
|
||||||
minimumInputLength: 2,
|
minimumInputLength: 2,
|
||||||
templateResult: selectItemBuilder(options.pictureGetter),
|
templateResult: selectItemBuilder(options.pictureGetter),
|
||||||
...options.dataSource,
|
...options.dataSource,
|
||||||
...(options.overrides || {}),
|
...(options.overrides ?? {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
interface LocalSourceOptions {
|
||||||
* @typedef LocalSourceOptions
|
excluded: () => number[];
|
||||||
* @property {undefined | function(): number[]} excluded
|
}
|
||||||
* A callback to the ids to exclude from the search
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a data source for a Select2 from a local array
|
* Build a data source for a Select2 from a local array
|
||||||
* @param {Select2Object[]} source The array containing the data
|
* @param {Select2Object[]} source The array containing the data
|
||||||
* @param {RemoteSourceOptions} options
|
* @param {RemoteSourceOptions} options
|
||||||
*/
|
*/
|
||||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
export function localDataSource(
|
||||||
function localDataSource(source, options) {
|
source: Select2Object[],
|
||||||
|
options: LocalSourceOptions,
|
||||||
|
): DataSource {
|
||||||
if (options.excluded) {
|
if (options.excluded) {
|
||||||
const ids = options.excluded();
|
const ids = options.excluded();
|
||||||
return { data: source.filter((i) => !ids.includes(i.id)) };
|
return { data: source.filter((i) => !ids.includes(i.id)) };
|
||||||
@ -189,15 +206,14 @@ function localDataSource(source, options) {
|
|||||||
return { data: source };
|
return { data: source };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
interface RemoteSourceOptions {
|
||||||
* @typedef RemoteSourceOptions
|
/** A callback to the ids to exclude from the search */
|
||||||
* @property {undefined | function(): number[]} excluded
|
excluded?: () => number[];
|
||||||
* A callback to the ids to exclude from the search
|
/** A converter for a value coming from the remote api */
|
||||||
* @property {undefined | function(): Select2Object} resultConverter
|
resultConverter?: ((obj: RemoteResult) => DataFormat | GroupedDataFormat) | undefined;
|
||||||
* A converter for a value coming from the remote api
|
/** Any other select2 parameter to apply on the config */
|
||||||
* @property {undefined | Object} overrides
|
overrides?: AjaxOptions;
|
||||||
* Any other select2 parameter to apply on the config
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a data source for a Select2 from a remote url
|
* Build a data source for a Select2 from a remote url
|
||||||
@ -205,10 +221,14 @@ function localDataSource(source, options) {
|
|||||||
* @param {RemoteSourceOptions} options
|
* @param {RemoteSourceOptions} options
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
export function remoteDataSource(
|
||||||
function remoteDataSource(source, options) {
|
source: string,
|
||||||
jQuery.ajaxSettings.traditional = true;
|
options: RemoteSourceOptions,
|
||||||
const params = {
|
): DataSource {
|
||||||
|
$.ajaxSetup({
|
||||||
|
traditional: true,
|
||||||
|
});
|
||||||
|
const params: AjaxOptions = {
|
||||||
url: source,
|
url: source,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
cache: true,
|
cache: true,
|
||||||
@ -217,7 +237,7 @@ function remoteDataSource(source, options) {
|
|||||||
return {
|
return {
|
||||||
search: params.term,
|
search: params.term,
|
||||||
exclude: [
|
exclude: [
|
||||||
...(this.val() || []).map((i) => Number.parseInt(i)),
|
...(this.val() || []).map((i: string) => Number.parseInt(i)),
|
||||||
...(options.excluded ? options.excluded() : []),
|
...(options.excluded ? options.excluded() : []),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@ -234,8 +254,7 @@ function remoteDataSource(source, options) {
|
|||||||
return { ajax: params };
|
return { ajax: params };
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
export function itemFormatter(user: { loading: boolean; text: string }) {
|
||||||
function itemFormatter(user) {
|
|
||||||
if (user.loading) {
|
if (user.loading) {
|
||||||
return user.text;
|
return user.text;
|
||||||
}
|
}
|
||||||
@ -246,8 +265,8 @@ function itemFormatter(user) {
|
|||||||
* @param {null | function(Object):string} pictureGetter
|
* @param {null | function(Object):string} pictureGetter
|
||||||
* @return {function(string): jQuery|HTMLElement}
|
* @return {function(string): jQuery|HTMLElement}
|
||||||
*/
|
*/
|
||||||
function selectItemBuilder(pictureGetter) {
|
export function selectItemBuilder(pictureGetter?: (item: RemoteResult) => string) {
|
||||||
return (item) => {
|
return (item: RemoteResult) => {
|
||||||
const picture = typeof pictureGetter === "function" ? pictureGetter(item) : null;
|
const picture = typeof pictureGetter === "function" ? pictureGetter(item) : null;
|
||||||
const imgHtml = picture
|
const imgHtml = picture
|
||||||
? `<img
|
? `<img
|
@ -12,7 +12,6 @@
|
|||||||
<link rel="stylesheet" href="{{ static('core/header.scss') }}">
|
<link rel="stylesheet" href="{{ static('core/header.scss') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/navbar.scss') }}">
|
<link rel="stylesheet" href="{{ static('core/navbar.scss') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/pagination.scss') }}">
|
<link rel="stylesheet" href="{{ static('core/pagination.scss') }}">
|
||||||
<link rel="stylesheet" href="{{ static('vendored/select2/select2.min.css') }}">
|
|
||||||
|
|
||||||
{% block jquery_css %}
|
{% block jquery_css %}
|
||||||
{# Thile file is quite heavy (around 250kb), so declaring it in a block allows easy removal #}
|
{# Thile file is quite heavy (around 250kb), so declaring it in a block allows easy removal #}
|
||||||
@ -26,8 +25,6 @@
|
|||||||
<script src="{{ static('webpack/jquery-index.js') }}"></script>
|
<script src="{{ static('webpack/jquery-index.js') }}"></script>
|
||||||
<!-- Put here to always have access 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 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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
41
package-lock.json
generated
41
package-lock.json
generated
@ -21,13 +21,17 @@
|
|||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"jquery-ui": "^1.14.0",
|
"jquery-ui": "^1.14.0",
|
||||||
"jquery.shorten": "^1.0.0",
|
"jquery.shorten": "^1.0.0",
|
||||||
"native-file-system-adapter": "^3.0.1"
|
"native-file-system-adapter": "^3.0.1",
|
||||||
|
"select2": "^4.1.0-rc.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
"@babel/preset-env": "^7.25.4",
|
"@babel/preset-env": "^7.25.4",
|
||||||
"@biomejs/biome": "1.9.3",
|
"@biomejs/biome": "1.9.3",
|
||||||
"@hey-api/openapi-ts": "^0.53.8",
|
"@hey-api/openapi-ts": "^0.53.8",
|
||||||
|
"@types/alpinejs": "^3.13.10",
|
||||||
|
"@types/jquery": "^3.5.31",
|
||||||
|
"@types/select2": "^4.0.63",
|
||||||
"babel-loader": "^9.2.1",
|
"babel-loader": "^9.2.1",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||||
@ -2184,6 +2188,12 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/alpinejs": {
|
||||||
|
"version": "3.13.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.10.tgz",
|
||||||
|
"integrity": "sha512-ah53tF6mWuuwerpDE7EHwbZErNDJQlsLISPqJhYj2RZ9nuTYbRknSkqebUd3igkhLIZKkPa7IiXjSn9qsU9O2w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/codemirror": {
|
"node_modules/@types/codemirror": {
|
||||||
"version": "5.60.15",
|
"version": "5.60.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz",
|
||||||
@ -2221,6 +2231,15 @@
|
|||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jquery": {
|
||||||
|
"version": "3.5.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz",
|
||||||
|
"integrity": "sha512-rf/iB+cPJ/YZfMwr+FVuQbm7IaWC4y3FVYfVDxRGqmUCFjjPII0HWaP0vTPJGp6m4o13AXySCcMbWfrWtBFAKw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/sizzle": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@ -2241,6 +2260,21 @@
|
|||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/select2": {
|
||||||
|
"version": "4.0.63",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/select2/-/select2-4.0.63.tgz",
|
||||||
|
"integrity": "sha512-/DXUfPSj3iVTGlRYRYPCFKKSogAGP/j+Z0fIMXbBiBtmmZj0WH7vnfNuckafq9C43KnqPPQW2TI/Rj/vTSGnQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jquery": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sizzle": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/tern": {
|
"node_modules/@types/tern": {
|
||||||
"version": "0.23.9",
|
"version": "0.23.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||||
@ -5655,6 +5689,11 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/select2": {
|
||||||
|
"version": "4.1.0-rc.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz",
|
||||||
|
"integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A=="
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.6.3",
|
"version": "7.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||||
|
@ -23,6 +23,9 @@
|
|||||||
"@babel/preset-env": "^7.25.4",
|
"@babel/preset-env": "^7.25.4",
|
||||||
"@biomejs/biome": "1.9.3",
|
"@biomejs/biome": "1.9.3",
|
||||||
"@hey-api/openapi-ts": "^0.53.8",
|
"@hey-api/openapi-ts": "^0.53.8",
|
||||||
|
"@types/alpinejs": "^3.13.10",
|
||||||
|
"@types/jquery": "^3.5.31",
|
||||||
|
"@types/select2": "^4.0.63",
|
||||||
"babel-loader": "^9.2.1",
|
"babel-loader": "^9.2.1",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||||
@ -48,6 +51,7 @@
|
|||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"jquery-ui": "^1.14.0",
|
"jquery-ui": "^1.14.0",
|
||||||
"jquery.shorten": "^1.0.0",
|
"jquery.shorten": "^1.0.0",
|
||||||
"native-file-system-adapter": "^3.0.1"
|
"native-file-system-adapter": "^3.0.1",
|
||||||
|
"select2": "^4.1.0-rc.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,302 +0,0 @@
|
|||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
};
|
|
324
sas/static/webpack/sas/viewer-index.ts
Normal file
324
sas/static/webpack/sas/viewer-index.ts
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import { makeUrl, paginated } from "#core:utils/api";
|
||||||
|
import { History } from "#core:utils/history";
|
||||||
|
import {
|
||||||
|
type AjaxResponse,
|
||||||
|
type RemoteResult,
|
||||||
|
remoteDataSource,
|
||||||
|
sithSelect2,
|
||||||
|
} from "#core:utils/select2";
|
||||||
|
import {
|
||||||
|
type IdentifiedUserSchema,
|
||||||
|
type PictureSchema,
|
||||||
|
type PicturesFetchIdentificationsResponse,
|
||||||
|
type PicturesFetchPicturesData,
|
||||||
|
type UserProfileSchema,
|
||||||
|
picturesDeletePicture,
|
||||||
|
picturesFetchIdentifications,
|
||||||
|
picturesFetchPictures,
|
||||||
|
picturesIdentifyUsers,
|
||||||
|
picturesModeratePicture,
|
||||||
|
userSearchUsers,
|
||||||
|
usersidentifiedDeleteRelation,
|
||||||
|
} from "#openapi";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container for a picture with the users identified on it
|
||||||
|
* able to prefetch its data.
|
||||||
|
*/
|
||||||
|
class PictureWithIdentifications {
|
||||||
|
identifications: PicturesFetchIdentificationsResponse | null = null;
|
||||||
|
imageLoading = false;
|
||||||
|
identificationsLoading = false;
|
||||||
|
id: number;
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
compressed_url: string;
|
||||||
|
|
||||||
|
constructor(picture: PictureSchema) {
|
||||||
|
Object.assign(this, picture);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromPicture(picture: PictureSchema) {
|
||||||
|
return new PictureWithIdentifications(picture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If not already done, fetch the users identified on this picture and
|
||||||
|
* populate the identifications field
|
||||||
|
*/
|
||||||
|
async loadIdentifications(options?: { forceReload: boolean }) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ViewerConfig {
|
||||||
|
/** Id of the user to get the pictures from */
|
||||||
|
userId: number;
|
||||||
|
/** Url of the current album */
|
||||||
|
albumUrl: string;
|
||||||
|
/** Id of the album to displlay */
|
||||||
|
albumId: number;
|
||||||
|
/** id of the first picture to load on the page */
|
||||||
|
firstPictureId: number;
|
||||||
|
/** if the user is sas admin */
|
||||||
|
userIsSasAdmin: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load user picture page with a nice download bar
|
||||||
|
**/
|
||||||
|
(window as unknown as { loadViewer: (config: ViewerConfig) => undefined }).loadViewer =
|
||||||
|
(config: ViewerConfig) => {
|
||||||
|
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 },
|
||||||
|
} as PicturesFetchPicturesData)
|
||||||
|
).map(PictureWithIdentifications.fromPicture);
|
||||||
|
this.selector = sithSelect2({
|
||||||
|
element: $(this.$refs.search) as unknown as HTMLElement,
|
||||||
|
dataSource: remoteDataSource(await makeUrl(userSearchUsers), {
|
||||||
|
excluded: () => [
|
||||||
|
...(this.currentPicture.identifications || []).map(
|
||||||
|
(i: IdentifiedUserSchema) => i.user.id,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
resultConverter: (obj: AjaxResponse) => {
|
||||||
|
return { ...obj, text: (obj as UserProfileSchema).display_name };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
pictureGetter: (user: RemoteResult) => user.profile_pict,
|
||||||
|
});
|
||||||
|
this.currentPicture = this.pictures.find(
|
||||||
|
(i: PictureSchema) => i.id === config.firstPictureId,
|
||||||
|
);
|
||||||
|
this.$watch(
|
||||||
|
"currentPicture",
|
||||||
|
(current: PictureSchema, previous: PictureSchema) => {
|
||||||
|
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: PictureSchema) => 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 = {
|
||||||
|
data: { sasPictureId: this.currentPicture.id },
|
||||||
|
unused: "",
|
||||||
|
url: `/sas/picture/${this.currentPicture.id}/`,
|
||||||
|
};
|
||||||
|
if (this.pushstate === History.Replace) {
|
||||||
|
window.history.replaceState(
|
||||||
|
updateArgs.data,
|
||||||
|
updateArgs.unused,
|
||||||
|
updateArgs.url,
|
||||||
|
);
|
||||||
|
this.pushstate = History.Push;
|
||||||
|
} else {
|
||||||
|
window.history.pushState(
|
||||||
|
updateArgs.data,
|
||||||
|
updateArgs.unused,
|
||||||
|
updateArgs.url,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.error as { detail: string }).detail}`;
|
||||||
|
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.error as { detail: string }).detail}`;
|
||||||
|
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: string) => 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: IdentifiedUserSchema) {
|
||||||
|
return config.userIsSasAdmin || identification.user.id === config.userId;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Untag a user from the current picture
|
||||||
|
* @param {PictureIdentification} identification
|
||||||
|
*/
|
||||||
|
async removeIdentification(identification: IdentifiedUserSchema) {
|
||||||
|
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: IdentifiedUserSchema) => i.id !== identification.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
{%- block additional_css -%}
|
{%- block additional_css -%}
|
||||||
<link rel="stylesheet" href="{{ static('sas/css/picture.scss') }}">
|
<link rel="stylesheet" href="{{ static('sas/css/picture.scss') }}">
|
||||||
|
<link rel="stylesheet" href="{{ static('webpack/sas/viewer-index.css') }}" defer>
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{%- block additional_js -%}
|
{%- block additional_js -%}
|
||||||
<script defer src="{{ static("webpack/sas/viewer-index.js") }}"></script>
|
<script defer src="{{ static("webpack/sas/viewer-index.ts") }}"></script>
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
|
@ -755,5 +755,4 @@ SITH_FRONT_DEP_VERSIONS = {
|
|||||||
"https://github.com/vasturiano/three-spritetext": "1.6.5",
|
"https://github.com/vasturiano/three-spritetext": "1.6.5",
|
||||||
"https://github.com/vasturiano/3d-force-graph/": "1.70.19",
|
"https://github.com/vasturiano/3d-force-graph/": "1.70.19",
|
||||||
"https://github.com/vasturiano/d3-force-3d": "3.0.3",
|
"https://github.com/vasturiano/d3-force-3d": "3.0.3",
|
||||||
"https://github.com/select2/select2/": "4.0.13",
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
"target": "es5",
|
"target": "es5",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
"types": ["jquery", "alpinejs"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"#openapi": ["./staticfiles/generated/openapi/index.ts"],
|
"#openapi": ["./staticfiles/generated/openapi/index.ts"],
|
||||||
"#core:*": ["./core/static/webpack/*"],
|
"#core:*": ["./core/static/webpack/*"],
|
||||||
|
Loading…
Reference in New Issue
Block a user