Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW-UI] optimise frontend #984

Merged
merged 6 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 35 additions & 10 deletions _dev/src/ts/api/RequestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,55 @@ import Hydration from '../utils/Hydration';
export class RequestHandler {
private currentRequestAbortController: AbortController | null = null;

public post(route: string, data = new FormData(), fromPopState?: boolean) {
/**
* @public
* @param {string} route - Target route for the POST request.
* @param {FormData}[data=new FormData()] - Form data to send with the request by default we send FormData with admin dir required by backend.
* @param {boolean} [fromPopState] - Indicates if the request originated from a popstate event need by hydration.
* @returns {Promise<void>}
* @description Sends a POST request to the specified route with optional data and pop state indicator. Cancels any ongoing request before initiating a new one.
*/
public async post(
route: string,
data: FormData = new FormData(),
fromPopState?: boolean
): Promise<void> {
// Cancel any previous request if it exists
if (this.currentRequestAbortController) {
this.currentRequestAbortController.abort();
}

// Create a new AbortController for the current request (used to cancel previous request)
this.currentRequestAbortController = new AbortController();
const { signal } = this.currentRequestAbortController;

// Append admin dir required by backend
data.append('dir', window.AutoUpgradeVariables.admin_dir);

baseApi
.post('', data, {
params: { route: route },
try {
const response = await baseApi.post('', data, {
params: { route },
signal
})
.then((response) => {
const data = response.data as ApiResponse;
this.handleResponse(data, fromPopState);
});

const responseData = response.data as ApiResponse;
await this.#handleResponse(responseData, fromPopState);
} catch (error) {
// TODO: catch errors
console.error(error);
}
Quetzacoalt91 marked this conversation as resolved.
Show resolved Hide resolved
}

private handleResponse(response: ApiResponse, fromPopState?: boolean) {
/**
* @private
* @param {ApiResponse} response - The response data from the API.
* @param {boolean} [fromPopState] - Indicates if the request originated from a popstate event need by hydration.
* @returns {Promise<void>}
* @description Handles the API response by checking for next route or hydration data.
*/
async #handleResponse(response: ApiResponse, fromPopState?: boolean): Promise<void> {
if ('next_route' in response) {
this.post(response.next_route);
await this.post(response.next_route);
}
if ('hydration' in response) {
new Hydration().hydrate(response, fromPopState);
Expand Down
4 changes: 2 additions & 2 deletions _dev/src/ts/pages/HomePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ export default class HomePage extends PageAbstract {
}
};

private handleSubmit = (event: Event) => {
private handleSubmit = async (event: Event) => {
event.preventDefault();
const routeToSubmit = this.form?.dataset.routeToSubmit;

if (routeToSubmit) {
const formData = new FormData(this.form);
api.post(routeToSubmit, formData);
await api.post(routeToSubmit, formData);
}
};

Expand Down
15 changes: 15 additions & 0 deletions _dev/src/ts/pages/PageAbstract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
/**
* @abstract
* @description Base abstract class defining the structure for page components, requiring implementation of lifecycle methods for mounting and destruction.
*/
export default abstract class PageAbstract {
/**
* @abstract
* @description Method to initialize and mount the page component. Should be implemented by subclasses to set up event listeners, render content, etc.
* @returns {void}
*/
abstract mount(): void;

/**
* @abstract
* @description Method to clean up and perform necessary teardown operations before the page component is destroyed. Should be implemented by subclasses to remove event listeners, clear timers, etc.
* @returns {void}
*/
abstract beforeDestroy(): void;
}
8 changes: 4 additions & 4 deletions _dev/src/ts/pages/UpdatePageVersionChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export default class UpdatePageVersionChoice extends UpdatePage {
});
};

private sendForm = (routeToSend: string) => {
private sendForm = async (routeToSend: string) => {
const formData = new FormData(this.form!);
api.post(routeToSend, formData);
await api.post(routeToSend, formData);
};

private addListenerToCheckRequirementsAgainButtons = () => {
Expand All @@ -61,7 +61,7 @@ export default class UpdatePageVersionChoice extends UpdatePage {
}
};

private saveForm = () => {
private saveForm = async () => {
const routeToSave = this.form!.dataset.routeToSave;

if (!routeToSave) {
Expand All @@ -82,7 +82,7 @@ export default class UpdatePageVersionChoice extends UpdatePage {
currentInputCheck.removeAttribute('data-requirements-are-ok');
this.toggleNextButton();
currentInputCheck.classList.add(this.radioLoadingClass);
this.sendForm(routeToSave);
await this.sendForm(routeToSave);
}
};

Expand Down
52 changes: 43 additions & 9 deletions _dev/src/ts/routing/RouteHandler.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,72 @@
import api from '../api/RequestHandler';

export default class RouteHandler {
/**
* @constructor
* @description Initializes the RouteHandler by setting the current route to 'home-page'
* if no route is present. Sets up an event listener for the 'popstate' event
* to handle route changes.
*/
constructor() {
if (!this.getCurrentRoute()) {
this.setNewRoute('home-page');
}
window.addEventListener('popstate', () => this.handleRouteChange());
window.addEventListener('popstate', () => this.#handleRouteChange());
}

private getCurrentUrl(): URL {
/**
* @private
* @returns {URL} The current URL object.
* @description Retrieves the current URL of the window.
*/
#getCurrentUrl(): URL {
return new URL(window.location.href);
}

private getQueryParams(): URLSearchParams {
return this.getCurrentUrl().searchParams;
/**
* @private
* @returns {URLSearchParams} The URLSearchParams object containing the query parameters.
* @description Retrieves the query parameters from the current URL.
*/
#getQueryParams(): URLSearchParams {
return this.#getCurrentUrl().searchParams;
}

/**
* @public
* @returns {string | null} The current route name, or null if not found.
* @description Gets the current route from the query parameters.
*/
public getCurrentRoute(): string | null {
return this.getQueryParams().get('route');
return this.#getQueryParams().get('route');
}

/**
* @public
* @param {string} newRoute - The new route name to set.
* @description Sets a new route by updating the query parameters and pushing the new URL
* to the browser's history.
*/
public setNewRoute(newRoute: string): void {
const queryParams = this.getQueryParams();
const queryParams = this.#getQueryParams();
queryParams.set('route', newRoute);

const newUrl = `${this.getCurrentUrl().pathname}?${queryParams.toString()}`;
const newUrl = `${this.#getCurrentUrl().pathname}?${queryParams.toString()}`;

window.history.pushState(null, '', newUrl);
}

private handleRouteChange() {
/**
* @private
* @async
* @returns {Promise<void>} A promise that resolves when the request is complete.
* @description Handles changes to the route by sending a POST request to the new route.
* If the new route is not null, it makes a request using the api module.
*/
async #handleRouteChange() {
const newRoute = this.getCurrentRoute();
if (newRoute !== null) {
api.post(newRoute, new FormData(), true);
await api.post(newRoute, new FormData(), true);
}
}
}
62 changes: 45 additions & 17 deletions _dev/src/ts/routing/ScriptHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,14 @@ import { RoutesMatching } from '../types/scriptHandlerTypes';
import { routeHandler } from '../autoUpgrade';

export default class ScriptHandler {
constructor() {
const currentRoute = routeHandler.getCurrentRoute();

if (currentRoute) {
this.loadScript(currentRoute);
}
}

private currentScript: PageAbstract | undefined;
#currentScript: PageAbstract | undefined;

private routesMatching: RoutesMatching = {
/**
* @private
* @type {RoutesMatching}
* @description Maps route names to their corresponding page classes.
*/
readonly #routesMatching: RoutesMatching = {
'home-page': HomePage,
'update-page-version-choice': UpdatePageVersionChoice,
'update-page-update-options': UpdatePageUpdateOptions,
Expand All @@ -28,16 +25,47 @@ export default class ScriptHandler {
'update-page-post-update': UpdatePagePostUpdate
};

private loadScript(routeName: string) {
if (this.routesMatching[routeName]) {
const pageClass = this.routesMatching[routeName];
this.currentScript = new pageClass();
this.currentScript.mount();
/**
* @constructor
* @description Initializes the `ScriptHandler` by loading the page script associated with the current route.
*/
constructor() {
const currentRoute = routeHandler.getCurrentRoute();

if (currentRoute) {
this.#loadScript(currentRoute);
}
}

/**
* @private
* @param {string} routeName - The name of the route to load his associated script.
* @returns void
* @description Loads and mounts the page script associated with the specified route name.
*/
#loadScript(routeName: string) {
const pageClass = this.#routesMatching[routeName];
if (pageClass) {
try {
this.#currentScript = new pageClass();
this.#currentScript.mount();
} catch (error) {
console.error(`Failed to load script for route ${routeName}:`, error);
}
} else {
console.debug(`No matching page Class found for route: ${routeName}`);
}
}

/**
* @public
* @param {string} newRoute - The name of the route to load his associated script.
* @returns void
* @description Updates the currently loaded route script by destroying the current
* page instance and loading a new one based on the provided route name.
*/
public updateRouteScript(newRoute: string) {
this.currentScript?.beforeDestroy();
this.loadScript(newRoute);
this.#currentScript?.beforeDestroy();
this.#loadScript(newRoute);
}
}
22 changes: 20 additions & 2 deletions _dev/src/ts/utils/Hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,27 @@ import { ApiResponseHydration } from '../types/apiTypes';
import { routeHandler, scriptHandler } from '../autoUpgrade';

export default class Hydration {
public static hydrationEventName = 'hydrate';
public hydrationEvent = new Event(Hydration.hydrationEventName);
/**
* @public
* @static
* @type {string}
* @description The name of the hydration event.
*/
public static hydrationEventName: string = 'hydrate';

/**
* @public
* @type {Event}
* @description The hydration event instance.
*/
public hydrationEvent: Event = new Event(Hydration.hydrationEventName);

/**
* @public
* @param {ApiResponseHydration} data - The data containing new content and routing information.
* @param {boolean} [fromPopState=false] - Indicates if the hydration is triggered from a popstate event.
* @description Hydrates the specified element with new content and updates the route if necessary.
*/
public hydrate(data: ApiResponseHydration, fromPopState?: boolean) {
const elementToUpdate = document.getElementById(data.parent_to_update);

Expand Down
10 changes: 10 additions & 0 deletions _dev/src/ts/utils/Stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export default class Stepper {
private doneClass = `${this.baseClass}--done`;
private normalClass = `${this.baseClass}--normal`;

/**
* @constructor
* @throws Will throw an error if the stepper or its steps are not found in the DOM.
* @description Initializes the Stepper by finding the parent element in the DOM and setting up the steps.
*/
constructor() {
const stepper = document.getElementById(
window.AutoUpgradeVariables.stepper_parent_id
Expand Down Expand Up @@ -39,6 +44,11 @@ export default class Stepper {
});
}

/**
* @public
* @param {string} currentStep - The code of the current step to be set.
* @description Sets the current step in the stepper and updates the classes for each step accordingly.
*/
public setCurrentStep = (currentStep: string) => {
let isBeforeCurrentStep = true;

Expand Down
Loading