Add helper function to export ts functions to html

This commit is contained in:
Antoine Bartuccio 2024-10-13 01:53:23 +02:00 committed by Bartuccio Antoine
parent 3b1d06a71d
commit b6e1c3bc88
2 changed files with 225 additions and 215 deletions

View File

@ -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;
}

View File

@ -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,
);
}
},
}));
});
};