Skip to content
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
2 changes: 2 additions & 0 deletions assets/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"name": "Name",
"search": "Search",
"filter": "Filter",
"elapsed_time": "Elapsed Time",
"phases": {
"1": "Phase 1 (5.0 - T14)",
"2": "Phase 2 (5.1)",
Expand Down Expand Up @@ -2331,6 +2332,7 @@
"breakpoints_implemented": "The following breakpoints have been implemented for this spec:",
"post_cap_ep": "Post cap EP",
"reforge_optimization_failed": "Reforge optimization failed. Please try again, or report the issue if it persists.",
"reforge_optimization_cancelled": "Reforge optimization cancelled.",
"use_custom": "Use custom EP Weights",
"enable_modification": "This will enable modification of the default EP weights and setting custom stat caps.",
"modify_in_editor": "Ep weights can be modified in the Stat Weights editor.",
Expand Down
2 changes: 2 additions & 0 deletions assets/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"name": "Nom",
"search": "Rechercher",
"filter": "Filtrer",
"elapsed_time": "Temps écoulé",
"phases": {
"1": "Phase 1 (5.0 - T14)",
"2": "Phase 2 (5.1)",
Expand Down Expand Up @@ -2331,6 +2332,7 @@
"breakpoints_implemented": "Les breakpoints suivants ont été implémentés pour cette spé :",
"post_cap_ep": "PE post-cap",
"reforge_optimization_failed": "L'optimisation de la retouche a échoué. Veuillez réessayer ou signaler le problème s'il persiste.",
"reforge_optimization_cancelled": "Optimisation de la retouche annulée.",
"use_custom": "Utiliser des poids PE personnalisés",
"enable_modification": "Ceci permettra la modification des poids PE par défaut et la définition de caps de statistiques personnalisés.",
"modify_in_editor": "Les poids PE peuvent être modifiés dans l'éditeur de Poids des Statistiques.",
Expand Down
6 changes: 6 additions & 0 deletions schemas/translation.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@
"filter": {
"type": "string"
},
"elapsed_time": {
"type": "string"
},
"phases": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -9748,6 +9751,9 @@
"reforge_optimization_failed": {
"type": "string"
},
"reforge_optimization_cancelled": {
"type": "string"
},
"use_custom": {
"type": "string"
},
Expand Down
29 changes: 19 additions & 10 deletions ui/core/components/base_modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type BaseModalConfig = {
title?: string | null;
// Should the modal be disposed on close?
disposeOnClose?: boolean;
// User should not be able to close the modal
preventClose?: boolean;
};

const DEFAULT_CONFIG = {
Expand All @@ -46,7 +48,7 @@ export class BaseModal extends Component {
readonly body: HTMLElement;
readonly footer: HTMLElement | undefined;

constructor(parent: HTMLElement, cssClass: string, config: BaseModalConfig = { disposeOnClose: true }) {
constructor(parent: HTMLElement, cssClass: string, config: BaseModalConfig = { disposeOnClose: true, preventClose: false }) {
super(parent, 'modal');
this.modalConfig = { ...DEFAULT_CONFIG, ...config };

Expand All @@ -58,18 +60,22 @@ export class BaseModal extends Component {
const modalSizeKlass = this.modalConfig.size && this.modalConfig.size != 'md' ? `modal-${this.modalConfig.size}` : '';

this.rootElem.classList.add('fade');
if (this.modalConfig.preventClose) this.rootElem.classList.add('modal-static');

this.rootElem.appendChild(
<div className={`modal-dialog ${cssClass} ${modalSizeKlass} ${this.modalConfig.scrollContents ? 'modal-overflow-scroll' : ''}`} ref={dialogRef}>
<div className="modal-content">
<div className={`modal-header ${this.modalConfig.header || this.modalConfig.title ? '' : 'p-0 border-0'}`} ref={headerRef}>
{this.modalConfig.title && <h5 className="modal-title">{this.modalConfig.title}</h5>}
<button
type="button"
className={`btn-close ${this.modalConfig.closeButton?.fixed ? 'position-fixed' : ''}`}
onclick={() => this.close()}
attributes={{ 'aria-label': 'Close' }}>
<i className="fas fa-times fa-2xl"></i>
</button>
{!this.modalConfig.preventClose && (
<button
type="button"
className={`btn-close ${this.modalConfig.closeButton?.fixed ? 'position-fixed' : ''}`}
onclick={() => this.close()}
attributes={{ 'aria-label': 'Close' }}>
<i className="fas fa-times fa-2xl"></i>
</button>
)}
</div>
<div className="modal-body" ref={bodyRef} />
{this.modalConfig.footer && <div className="modal-footer" ref={footerRef} />}
Expand All @@ -82,7 +88,10 @@ export class BaseModal extends Component {
this.body = bodyRef.value!;
this.footer = footerRef.value!;

this.modal = new Modal(this.rootElem);
this.modal = new Modal(this.rootElem, {
backdrop: this.modalConfig.preventClose ? 'static' : true,
keyboard: !this.modalConfig.preventClose,
});

if (this.modalConfig.disposeOnClose) {
this.rootElem.addEventListener(
Expand Down Expand Up @@ -169,7 +178,7 @@ export class BaseModal extends Component {
}

private closeModalOnEscKey(event: KeyboardEvent) {
if (event.key == 'Escape') {
if (!this.modalConfig.preventClose && event.key == 'Escape') {
this.close();
}
}
Expand Down
139 changes: 139 additions & 0 deletions ui/core/components/progress_tracker_modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import clsx from 'clsx';
import { BaseModal } from './base_modal.js';
import { Component } from './component.js';
import { ref } from 'tsx-vanilla';
import i18n from '../../i18n/config.js';

export interface ProgressTrackerModalState {
stage: 'initializing' | 'complete' | 'error' | string;
message?: string;
}

interface ProgressTrackerModalOptions {
id: string;
onCancel?: () => void;
onComplete?: () => void;
title: string;
warning?: string | Element;
initializingMessage?: string;
}

export class ProgressTrackerModal extends Component {
private progressState: ProgressTrackerModalState;

readonly id: string;
private modal: BaseModal;
private startTime: number = 0;
private updateInterval: number | null = null;

private messageElement: HTMLElement | null = null;
private elapsedTimeElement: HTMLElement | null = null;
private contentElement: HTMLElement | null = null;

constructor(parent: HTMLElement, options: ProgressTrackerModalOptions) {
super(null, undefined, parent);
this.id = options.id;
this.progressState = {
stage: 'initializing',
message: options.initializingMessage,
};

this.modal = new BaseModal(this.rootElem, clsx('progress-tracker-modal', options.id), {
title: options.title,
disposeOnClose: false,
preventClose: true,
size: 'md',
});
this.modal.rootElem.id = this.id;

const messageRef = ref<HTMLDivElement>();
const contentRef = ref<HTMLDivElement>();
const elapsedRef = ref<HTMLSpanElement>();

this.modal.body.replaceChildren(
<div className="progress-tracker-modal-modal">
<div className="progress-tracker-modal-overlay"></div>
<div className="progress-tracker-modal-content" ref={contentRef}>
{options.warning && <div className="progress-tracker-modal-warning">{options.warning}</div>}
<div className="progress-tracker-modal-time-display">
<strong>{i18n.t('common.elapsed_time')}:</strong>{' '}
<span className="time-elapsed" ref={elapsedRef}>
0s
</span>
</div>
<div
className={clsx('progress-tracker-modal-message', !this.progressState.message && 'd-none')}
dataset={{
stage: this.progressState.stage,
}}
ref={messageRef}>
{this.progressState.message}
</div>
{options.onCancel && (
<button
className="btn btn-outline-cancel progress-tracker-modal-cancel-btn"
onclick={() => {
options.onCancel?.();
this.hide();
}}>
<i className="fa fa-ban me-1"></i>
{i18n.t('sidebar.results.reference.cancel')}
</button>
)}
</div>
</div>,
);

this.elapsedTimeElement = elapsedRef.value!;
this.messageElement = messageRef.value!;
this.contentElement = contentRef.value!;
}

show(): void {
this.modal.open();
this.startTime = Date.now();
this.updateInterval = window.setInterval(() => this.updateTimeDisplay(), 100);
}

hide(): void {
if (this.updateInterval) {
clearInterval(this.updateInterval);
}

// Ensure we give the modal enough time to finish opening
// To solve a Bootstrap Modal bug where it will not close properly
setTimeout(() => this.modal.close(), Math.max(0, 650 - (Date.now() - this.startTime)));
}

updateProgress(state: Partial<ProgressTrackerModalState>): void {
this.progressState = { ...this.progressState, ...state };
this.render();
}

private render(): void {
const { stage, message } = this.progressState;

// Update data-stage attribute for CSS styling
if (this.contentElement) this.contentElement.dataset.stage = stage;

if (!this.messageElement) return;

this.messageElement.classList[message ? 'remove' : 'add']('d-none');
this.messageElement.textContent = message || '';
}

private updateTimeDisplay(): void {
if (!this.startTime || !this.elapsedTimeElement) return;

const elapsed = (Date.now() - this.startTime) / 1000;

// Format time nicely
if (elapsed < 60) {
this.elapsedTimeElement.textContent = `${elapsed.toFixed(1)}s`;
} else {
const minutes = Math.floor(elapsed / 60);
const seconds = Math.floor(elapsed % 60);
this.elapsedTimeElement.textContent = `${minutes}m ${seconds}s`;
}
}
}
Loading