Skip to content

Commit

Permalink
Merge pull request #986 from Quetzacoalt91/backup-options-page
Browse files Browse the repository at this point in the history
[NEW-UI] Backup options page
  • Loading branch information
Quetzacoalt91 authored Nov 25, 2024
2 parents d5ca3d1 + b16960a commit 2eadd96
Show file tree
Hide file tree
Showing 39 changed files with 813 additions and 207 deletions.
3 changes: 2 additions & 1 deletion _dev/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export default [
sourceType: 'module',
globals: {
...globals.browser,
...globals.node
...globals.node,
...globals.jquery
},
parser: tseslintParser,
parserOptions: {
Expand Down
297 changes: 173 additions & 124 deletions _dev/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions _dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"devDependencies": {
"@eslint/js": "^9.9.1",
"@prestashopcorp/stylelint-config": "^1.0.0",
"@types/bootstrap": "^3.4.0",
"@types/jest": "^29.5.13",
"@types/jquery": "^3.5.32",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"eslint": ">=9.16.0 || <=9.14.0",
Expand Down
6 changes: 6 additions & 0 deletions _dev/src/scss/components/_modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ $e: ".modal";
}

// Custom modals
&__spacer {
display: flex;
flex-direction: column;
gap: 1.5rem;
}

&__no-backup {
margin-block: 0;

Expand Down
4 changes: 3 additions & 1 deletion _dev/src/ts/autoUpgrade.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import ModalContainer from './components/ModalContainer';
import RouteHandler from './routing/RouteHandler';
import ScriptHandler from './routing/ScriptHandler';

export const routeHandler = new RouteHandler();

export const modalContainer = new ModalContainer();
export const scriptHandler = new ScriptHandler();

export default { routeHandler, scriptHandler };
export default { routeHandler, scriptHandler, modalContainer };
58 changes: 58 additions & 0 deletions _dev/src/ts/components/ModalContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import DomLifecycle from '../types/DomLifecycle';
import Hydration from '../utils/Hydration';

export default class ModalContainer implements DomLifecycle {
public static readonly cancelEvent = 'cancel';
public static readonly okEvent = 'ok';

public static readonly containerId = 'ua_modal';

public mount(): void {
this.modalContainer.addEventListener(Hydration.hydrationEventName, this.#displayModal);
this.modalContainer.addEventListener('click', this.#onClick);
this.modalContainer.addEventListener(ModalContainer.cancelEvent, this.#closeModal);
this.modalContainer.addEventListener(ModalContainer.okEvent, this.#closeModal);
}

public beforeDestroy(): void {
this.modalContainer.removeEventListener(Hydration.hydrationEventName, this.#displayModal);
this.modalContainer.removeEventListener('click', this.#onClick);
this.modalContainer.removeEventListener(ModalContainer.cancelEvent, this.#closeModal);
this.modalContainer.removeEventListener(ModalContainer.okEvent, this.#closeModal);
}

public get modalContainer(): HTMLElement {
const container = document.getElementById(ModalContainer.containerId);

if (!container) {
throw new Error('Cannot find modal container to initialize.');
}
return container;
}

#displayModal(): void {
$(
document.getElementById(ModalContainer.containerId)?.getElementsByClassName('modal') || []
).modal('show');
}

#onClick(ev: Event): void {
const target = ev.target ? (ev.target as HTMLElement) : null;
const modal = target?.closest('.modal');

if (modal) {
if (target?.closest("[data-dismiss='modal']")) {
modal.dispatchEvent(new Event(ModalContainer.cancelEvent, { bubbles: true }));
} else if (target?.closest(".modal-footer button:not([data-dismiss='modal'])")) {
modal.dispatchEvent(new Event(ModalContainer.okEvent, { bubbles: true }));
}
}
}

#closeModal(ev: Event): void {
const modal = ev.target;
if (modal) {
$(modal).modal('hide');
}
}
}
72 changes: 72 additions & 0 deletions _dev/src/ts/modals/StartUpdateModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import DomLifecycle from '../types/DomLifecycle';
import api from '../api/RequestHandler';

export default class StartUpdateModal implements DomLifecycle {
protected readonly formId = 'form-confirm-update';
protected readonly confirmCheckboxId = 'modal-start-update-own-backup';

public mount(): void {
this.#form.addEventListener('submit', this.#onSubmit);
this.#form.addEventListener('change', this.#onChange);

this.#updateSubmitButtonStatus(
document.getElementById('modal-start-update-own-backup') as HTMLInputElement | undefined
);
}

public beforeDestroy(): void {
this.#form.removeEventListener('submit', this.#onSubmit);
this.#form.removeEventListener('change', this.#onChange);
}

get #form(): HTMLFormElement {
const form = document.forms.namedItem('form-confirm-update');
if (!form) {
throw new Error('Form not found');
}

// We implement the same way to check from the other scripts, even though there is only one value.
// This will ease any potential refacto.
['routeToSubmit'].forEach((data) => {
if (!form.dataset[data]) {
throw new Error(`Missing data ${data} from form dataset.`);
}
});

return form;
}

get #submitButton(): HTMLButtonElement {
const submitButton = Array.from(this.#form.elements).find(
(element) => element instanceof HTMLButtonElement && element.type === 'submit'
) as HTMLButtonElement | null;

if (!submitButton) {
throw new Error(`No submit button found for form ${this.#form.id}`);
}

return submitButton;
}

readonly #onChange = async (ev: Event) => {
const optionInput = ev.target as HTMLInputElement;

if (optionInput.id === this.confirmCheckboxId) {
this.#updateSubmitButtonStatus(optionInput);
}
};

readonly #onSubmit = async (event: Event) => {
event.preventDefault();

await api.post(this.#form.dataset.routeToSubmit!, new FormData(this.#form));
};

#updateSubmitButtonStatus(input?: HTMLInputElement): void {
if (!input || input.checked) {
this.#submitButton.removeAttribute('disabled');
} else {
this.#submitButton.setAttribute('disabled', 'true');
}
}
}
2 changes: 1 addition & 1 deletion _dev/src/ts/pages/HomePage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PageAbstract from './PageAbstract';
import api from '../api/RequestHandler';
import PageAbstract from './PageAbstract';

export default class HomePage extends PageAbstract {
constructor() {
Expand Down
4 changes: 3 additions & 1 deletion _dev/src/ts/pages/PageAbstract.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import DomLifecycle from '../types/DomLifecycle';

/**
* @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 {
export default abstract class PageAbstract implements DomLifecycle {
/**
* @abstract
* @description Method to initialize and mount the page component. Should be implemented by subclasses to set up event listeners, render content, etc.
Expand Down
2 changes: 1 addition & 1 deletion _dev/src/ts/pages/UpdatePage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PageAbstract from './PageAbstract';
import Stepper from '../utils/Stepper';
import PageAbstract from './PageAbstract';

export default class UpdatePage extends PageAbstract {
protected stepCode = 'version-choice';
Expand Down
69 changes: 65 additions & 4 deletions _dev/src/ts/pages/UpdatePageBackup.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,74 @@
import api from '../api/RequestHandler';
import { modalContainer } from '../autoUpgrade';
import ModalContainer from '../components/ModalContainer';
import UpdatePage from './UpdatePage';

export default class UpdatePageBackup extends UpdatePage {
protected stepCode = 'backup';

constructor() {
super();
}

public mount() {
this.initStepper();
this.#form.addEventListener('submit', this.#onFormSubmit);
this.#form.addEventListener('change', this.#onInputChange);

document.getElementById('ua_container')?.addEventListener('click', this.#onClick);
modalContainer.modalContainer.addEventListener(ModalContainer.okEvent, this.#onModalOk);
}

public beforeDestroy(): void {
this.#form.removeEventListener('submit', this.#onFormSubmit);
this.#form.removeEventListener('change', this.#onInputChange);

document.getElementById('ua_container')?.removeEventListener('click', this.#onClick);
modalContainer.modalContainer.removeEventListener(ModalContainer.okEvent, this.#onModalOk);
}

get #form(): HTMLFormElement {
const form = document.forms.namedItem('update-backup-page-form');
if (!form) {
throw new Error('Form not found');
}

['routeToSave', 'routeToSubmitBackup', 'routeToSubmitUpdate', 'routeToConfirmBackup'].forEach(
(data) => {
if (!form.dataset[data]) {
throw new Error(`Missing data ${data} from form dataset.`);
}
}
);

return form;
}

readonly #onClick = async (ev: Event) => {
if ((ev.target as HTMLElement).id === 'update-backup-page-skip-btn') {
const formData = new FormData();
// TODO: Value currently hardcoded until management of backups is implemented
formData.append('backupDone', JSON.stringify(false));
await api.post(this.#form.dataset.routeToSubmitUpdate!, formData);
}
};

readonly #onModalOk = async (ev: Event) => {
// We handle the backup confirmation modal as it is really basic
if ((ev.target as HTMLElement).id === 'modal-confirm-backup') {
api.post(this.#form.dataset.routeToConfirmBackup!);
}
// The update confirmation modal gets its logic in a dedicated script
};

readonly #onInputChange = async (ev: Event) => {
const optionInput = ev.target as HTMLInputElement;

const data = new FormData(this.#form);
optionInput.setAttribute('disabled', 'true');
await api.post(this.#form.dataset.routeToSave!, data);
optionInput.removeAttribute('disabled');
};

readonly #onFormSubmit = async (event: Event) => {
event.preventDefault();

await api.post(this.#form.dataset.routeToSubmitBackup!, new FormData(this.#form));
};
}
20 changes: 10 additions & 10 deletions _dev/src/ts/pages/UpdatePageUpdateOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ export default class UpdatePageUpdateOptions extends UpdatePage {

public mount() {
this.initStepper();
this.form.addEventListener('submit', this.onSubmit);
this.form.addEventListener('change', this.onChange);
this.#form.addEventListener('submit', this.#onSubmit);
this.#form.addEventListener('change', this.#onChange);
}

public beforeDestroy() {
try {
this.form.removeEventListener('submit', this.onSubmit);
this.form.removeEventListener('change', this.onChange);
this.#form.removeEventListener('submit', this.#onSubmit);
this.#form.removeEventListener('change', this.#onChange);
} catch {
// Do Nothing, page is likely removed from the DOM already
}
}

private get form(): HTMLFormElement {
get #form(): HTMLFormElement {
const form = document.forms.namedItem('update-options-page-form');
if (!form) {
throw new Error('Form not found');
Expand All @@ -34,18 +34,18 @@ export default class UpdatePageUpdateOptions extends UpdatePage {
return form;
}

private readonly onChange = async (ev: Event) => {
readonly #onChange = async (ev: Event) => {
const optionInput = ev.target as HTMLInputElement;

const data = new FormData(this.form);
const data = new FormData(this.#form);
optionInput.setAttribute('disabled', 'true');
await api.post(this.form.dataset.routeToSave!, data);
await api.post(this.#form.dataset.routeToSave!, data);
optionInput.removeAttribute('disabled');
};

private readonly onSubmit = async (event: Event) => {
readonly #onSubmit = async (event: Event) => {
event.preventDefault();

await api.post(this.form.dataset.routeToSubmit!, new FormData(this.form));
await api.post(this.#form.dataset.routeToSubmit!, new FormData(this.#form));
};
}
Loading

0 comments on commit 2eadd96

Please sign in to comment.