mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-22 06:03:20 +00:00
Add helper function to export ts functions to html
This commit is contained in:
parent
3b1d06a71d
commit
b6e1c3bc88
@ -4,3 +4,17 @@ declare global {
|
|||||||
const Alpine: AlpineType;
|
const Alpine: AlpineType;
|
||||||
const gettext: (text: string) => string;
|
const gettext: (text: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to export typescript functions to regular html and jinja files
|
||||||
|
* Without it, you either have to use the any keyword and suppress warnings or do a
|
||||||
|
* very painful type conversion workaround which is only here to please the linter
|
||||||
|
*
|
||||||
|
* This is only useful if you're using typescript, this is equivalent to doing
|
||||||
|
* window.yourFunction = yourFunction
|
||||||
|
**/
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
|
||||||
|
export function exportToHtml(name: string, func: any) {
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
|
||||||
|
(window as any)[name] = func;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { makeUrl, paginated } from "#core:utils/api";
|
import { makeUrl, paginated } from "#core:utils/api";
|
||||||
|
import { exportToHtml } from "#core:utils/globals";
|
||||||
import { History } from "#core:utils/history";
|
import { History } from "#core:utils/history";
|
||||||
import {
|
import {
|
||||||
type AjaxResponse,
|
type AjaxResponse,
|
||||||
@ -97,228 +98,223 @@ interface ViewerConfig {
|
|||||||
/**
|
/**
|
||||||
* Load user picture page with a nice download bar
|
* Load user picture page with a nice download bar
|
||||||
**/
|
**/
|
||||||
(window as unknown as { loadViewer: (config: ViewerConfig) => undefined }).loadViewer =
|
exportToHtml("loadViewer", (config: ViewerConfig) => {
|
||||||
(config: ViewerConfig) => {
|
document.addEventListener("alpine:init", () => {
|
||||||
document.addEventListener("alpine:init", () => {
|
Alpine.data("picture_viewer", () => ({
|
||||||
Alpine.data("picture_viewer", () => ({
|
/**
|
||||||
/**
|
* All the pictures that can be displayed on this picture viewer
|
||||||
* All the pictures that can be displayed on this picture viewer
|
* @type PictureWithIdentifications[]
|
||||||
* @type PictureWithIdentifications[]
|
**/
|
||||||
**/
|
pictures: [],
|
||||||
pictures: [],
|
/**
|
||||||
/**
|
* The currently displayed picture
|
||||||
* The currently displayed picture
|
* Default dummy data are pre-loaded to avoid javascript error
|
||||||
* Default dummy data are pre-loaded to avoid javascript error
|
* when loading the page at the beginning
|
||||||
* when loading the page at the beginning
|
* @type PictureWithIdentifications
|
||||||
* @type PictureWithIdentifications
|
**/
|
||||||
**/
|
currentPicture: {
|
||||||
currentPicture: {
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
is_moderated: true,
|
||||||
is_moderated: true,
|
id: null,
|
||||||
id: null,
|
name: "",
|
||||||
name: "",
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
display_name: "",
|
||||||
display_name: "",
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
compressed_url: "",
|
||||||
compressed_url: "",
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
profile_url: "",
|
||||||
profile_url: "",
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
full_size_url: "",
|
||||||
full_size_url: "",
|
owner: "",
|
||||||
owner: "",
|
date: new Date(),
|
||||||
date: new Date(),
|
identifications: [],
|
||||||
identifications: [],
|
},
|
||||||
},
|
/**
|
||||||
/**
|
* The picture which will be displayed next if the user press the "next" button
|
||||||
* The picture which will be displayed next if the user press the "next" button
|
* @type ?PictureWithIdentifications
|
||||||
* @type ?PictureWithIdentifications
|
**/
|
||||||
**/
|
nextPicture: null,
|
||||||
nextPicture: null,
|
/**
|
||||||
/**
|
* The picture which will be displayed next if the user press the "previous" button
|
||||||
* The picture which will be displayed next if the user press the "previous" button
|
* @type ?PictureWithIdentifications
|
||||||
* @type ?PictureWithIdentifications
|
**/
|
||||||
**/
|
previousPicture: null,
|
||||||
previousPicture: null,
|
/**
|
||||||
/**
|
* The select2 component used to identify users
|
||||||
* The select2 component used to identify users
|
**/
|
||||||
**/
|
selector: undefined,
|
||||||
selector: undefined,
|
/**
|
||||||
/**
|
* true if the page is in a loading state, else false
|
||||||
* true if the page is in a loading state, else false
|
**/
|
||||||
**/
|
/**
|
||||||
/**
|
* Error message when a moderation operation fails
|
||||||
* Error message when a moderation operation fails
|
* @type string
|
||||||
* @type string
|
**/
|
||||||
**/
|
moderationError: "",
|
||||||
moderationError: "",
|
/**
|
||||||
/**
|
* Method of pushing new url to the browser history
|
||||||
* Method of pushing new url to the browser history
|
* Used by popstate event and always reset to it's default value when used
|
||||||
* Used by popstate event and always reset to it's default value when used
|
* @type History
|
||||||
* @type History
|
**/
|
||||||
**/
|
pushstate: History.Push,
|
||||||
pushstate: History.Push,
|
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
this.pictures = (
|
this.pictures = (
|
||||||
await paginated(picturesFetchPictures, {
|
await paginated(picturesFetchPictures, {
|
||||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
query: { album_id: config.albumId },
|
query: { album_id: config.albumId },
|
||||||
} as PicturesFetchPicturesData)
|
} as PicturesFetchPicturesData)
|
||||||
).map(PictureWithIdentifications.fromPicture);
|
).map(PictureWithIdentifications.fromPicture);
|
||||||
this.selector = sithSelect2({
|
this.selector = sithSelect2({
|
||||||
element: $(this.$refs.search) as unknown as HTMLElement,
|
element: $(this.$refs.search) as unknown as HTMLElement,
|
||||||
dataSource: remoteDataSource(await makeUrl(userSearchUsers), {
|
dataSource: remoteDataSource(await makeUrl(userSearchUsers), {
|
||||||
excluded: () => [
|
excluded: () => [
|
||||||
...(this.currentPicture.identifications || []).map(
|
...(this.currentPicture.identifications || []).map(
|
||||||
(i: IdentifiedUserSchema) => i.user.id,
|
(i: IdentifiedUserSchema) => i.user.id,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
resultConverter: (obj: AjaxResponse) => {
|
resultConverter: (obj: AjaxResponse) => {
|
||||||
return { ...obj, text: (obj as UserProfileSchema).display_name };
|
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) => {
|
pictureGetter: (user: RemoteResult) => user.profile_pict,
|
||||||
if (!event.state || event.state.sasPictureId === undefined) {
|
});
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
this.pushstate = History.Replace;
|
this.updatePicture();
|
||||||
this.currentPicture = this.pictures.find(
|
},
|
||||||
(i: PictureSchema) => i.id === Number.parseInt(event.state.sasPictureId),
|
);
|
||||||
);
|
window.addEventListener("popstate", async (event) => {
|
||||||
});
|
if (!event.state || event.state.sasPictureId === undefined) {
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
this.currentPicture.is_moderated = true;
|
this.pushstate = History.Replace;
|
||||||
this.currentPicture.askedForRemoval = false;
|
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();
|
||||||
|
},
|
||||||
|
|
||||||
async deletePicture() {
|
/**
|
||||||
const res = await picturesDeletePicture({
|
* 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
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
path: { picture_id: this.currentPicture.id },
|
picture_id: this.currentPicture.id,
|
||||||
});
|
},
|
||||||
if (res.error) {
|
body: this.selector.val().map((i: string) => Number.parseInt(i)),
|
||||||
this.moderationError = `${gettext("Couldn't delete picture")} : ${(res.error as { detail: string }).detail}`;
|
});
|
||||||
return;
|
// refresh the identified users list
|
||||||
}
|
await this.currentPicture.loadIdentifications({ forceReload: true });
|
||||||
this.pictures.splice(this.pictures.indexOf(this.currentPicture), 1);
|
this.selector.empty().trigger("change");
|
||||||
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.
|
* Check if an identification can be removed by the currently logged user
|
||||||
*/
|
* @param {PictureIdentification} identification
|
||||||
async submitIdentification() {
|
* @return {boolean}
|
||||||
await picturesIdentifyUsers({
|
*/
|
||||||
path: {
|
canBeRemoved(identification: IdentifiedUserSchema) {
|
||||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
return config.userIsSasAdmin || identification.user.id === config.userId;
|
||||||
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
|
* Untag a user from the current picture
|
||||||
* @param {PictureIdentification} identification
|
* @param {PictureIdentification} identification
|
||||||
* @return {boolean}
|
*/
|
||||||
*/
|
async removeIdentification(identification: IdentifiedUserSchema) {
|
||||||
canBeRemoved(identification: IdentifiedUserSchema) {
|
const res = await usersidentifiedDeleteRelation({
|
||||||
return config.userIsSasAdmin || identification.user.id === config.userId;
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
},
|
path: { relation_id: identification.id },
|
||||||
|
});
|
||||||
/**
|
if (!res.error && Array.isArray(this.currentPicture.identifications)) {
|
||||||
* Untag a user from the current picture
|
this.currentPicture.identifications =
|
||||||
* @param {PictureIdentification} identification
|
this.currentPicture.identifications.filter(
|
||||||
*/
|
(i: IdentifiedUserSchema) => i.id !== identification.id,
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user