/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { Component, OnInit, inject, ViewChild } from '@angular/core'; import { MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialog, MatDialogClose, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { CdkScrollable } from '@angular/cdk/scrolling'; import { MatDivider } from '@angular/material/divider'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { KeyboardShortcutService, ShortcutItem } from '../../core/services/keyboard-shortcut.service'; import { FormsModule } from '@angular/forms'; import { NgClass } from '@angular/common'; import { MatButton } from '@angular/material/button'; import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; import { ShortcutUsageDialogComponent } from '../shortcut-usage-dialog/shortcut-usage-dialog.component'; import { MatIcon } from '@angular/material/icon'; import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatInput } from '@angular/material/input'; import { MatSelect } from '@angular/material/select'; import { MatOption } from '@angular/material/core'; @Component({ selector: 'mifosx-keyboard-shortcuts-dialog', templateUrl: './keyboard-shortcuts-dialog.component.html', styleUrls: ['./keyboard-shortcuts-dialog.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, MatDialogTitle, CdkScrollable, MatDialogContent, MatDialogActions, MatDialogClose, MatButton, FormsModule, MatIcon, MatFormField, MatLabel, MatInput, MatSelect, MatOption ] }) export class KeyboardShortcutsDialogComponent implements OnInit { dialogRef = inject>(MatDialogRef); private keyboardShortcutService = inject(KeyboardShortcutService); public data = inject(MAT_DIALOG_DATA, { optional: true }); shortcuts: ShortcutItem[] = []; validationError: string | null = null; activeInputItem: ShortcutItem | null = null; displayedColumns: string[] = [ 'title', 'keyCombination' ]; isAddingNew = false; newShortcut: Partial = {}; activeNewInput = false; editingShortcutId: string | null = null; editingShortcutData: Partial = {}; activeEditInput = false; ngOnInit() { this.dialogRef.updateSize('800px'); this.keyboardShortcutService.activeShortcuts$.subscribe((shortcuts) => { // Deep copy to allow editing without affecting the global state immediately this.shortcuts = JSON.parse(JSON.stringify(shortcuts)); }); if (this.data && this.data.prefill) { this.startAddShortcut(); this.newShortcut = { ...this.newShortcut, ...this.data.prefill }; } } formatShortcut(item: Partial): string { if (!item || (!item.key && !item.ctrlKey && !item.shiftKey && !item.altKey)) return ''; const parts = []; if (item.ctrlKey) parts.push('Ctrl'); if (item.shiftKey) parts.push('Shift'); if (item.altKey) parts.push('Alt'); if (item.key) parts.push(item.key.toUpperCase()); return parts.join(' + '); } onKeyDown(event: KeyboardEvent, item: Partial, isNewOrEdit: boolean = false) { event.preventDefault(); event.stopPropagation(); // Ignore if only modifier keys are pressed if ([ 'Control', 'Shift', 'Alt', 'Meta' ].includes(event.key)) { return; } item.ctrlKey = event.ctrlKey; item.shiftKey = event.shiftKey; item.altKey = event.altKey; item.key = event.key.toLowerCase(); if (!isNewOrEdit) { this.activeInputItem = null; this.validateShortcuts(); } else { this.activeNewInput = false; this.activeEditInput = false; } } validateShortcuts() { this.validationError = null; const seen = new Set(); for (const item of this.shortcuts) { if (!item.key) { this.validationError = `Invalid shortcut for ${item.title}: Key is required`; return; } const keyStr = `${!!item.ctrlKey}-${!!item.shiftKey}-${!!item.altKey}-${item.key.toLowerCase()}`; if (seen.has(keyStr)) { this.validationError = `Duplicate shortcut detected: ${this.formatShortcut(item)}`; return; } seen.add(keyStr); } } startAddShortcut() { this.isAddingNew = true; this.newShortcut = { title: '', route: '', key: '', ctrlKey: false, shiftKey: false, altKey: false, action: 'navigate', custom: true, id: 'custom_' + Date.now() }; this.validationError = null; } cancelAddShortcut() { this.isAddingNew = false; this.validationError = null; } private dialog = inject(MatDialog); @ViewChild('shortcutDetailsTemplate') shortcutDetailsTemplate: any; private detailsDialogRef: MatDialogRef | null = null; showShortcutDetails(item: ShortcutItem) { this.detailsDialogRef = this.dialog.open(this.shortcutDetailsTemplate, { data: item, width: '400px' }); } openUsagePopup() { this.dialog.open(ShortcutUsageDialogComponent, { width: '600px' }); } getShortcutDescription(item: Partial): string { if (item.custom) { if (item.action === 'navigate') { return `Navigates to the ${item.title} page.`; } return `Executes the ${item.action} action.`; } else { switch (item.id) { case 'logout': return 'Logs you out of the application securely.'; case 'help': return 'Opens the user manual and help documentation.'; case 'runReport': return 'Executes the currently selected report.'; case 'cancel': return 'Cancels the current operation or form.'; case 'submit': return 'Submits the current form or dialog.'; default: return `Navigates to the predefined ${item.title} feature.`; } } } saveNewShortcut() { if (this.newShortcut.action === 'navigate' && this.newShortcut.route) { this.newShortcut.route = this.keyboardShortcutService.normalizeRouteStr(this.newShortcut.route); } if (this.validateSingleShortcut(this.newShortcut, null)) { this.shortcuts.push(this.newShortcut as ShortcutItem); this.isAddingNew = false; this.validateShortcuts(); if (!this.validationError) { this.keyboardShortcutService.updateShortcuts(this.shortcuts); } } } startEditShortcut(item: ShortcutItem) { if (this.detailsDialogRef) { this.detailsDialogRef.close(); this.detailsDialogRef = null; } this.editingShortcutId = item.id || null; this.editingShortcutData = { ...item }; this.validationError = null; } cancelEditShortcut() { this.editingShortcutId = null; this.validationError = null; } saveEditShortcut() { if (this.editingShortcutData.action === 'navigate' && this.editingShortcutData.route) { this.editingShortcutData.route = this.keyboardShortcutService.normalizeRouteStr(this.editingShortcutData.route); } if (this.validateSingleShortcut(this.editingShortcutData, this.editingShortcutId)) { const index = this.shortcuts.findIndex((s) => s.id === this.editingShortcutId); if (index > -1) { this.shortcuts[index] = { ...this.editingShortcutData } as ShortcutItem; } this.editingShortcutId = null; this.validateShortcuts(); if (!this.validationError) { this.keyboardShortcutService.updateShortcuts(this.shortcuts); } } } deleteShortcut(item: ShortcutItem) { if (this.detailsDialogRef) { this.detailsDialogRef.close(); this.detailsDialogRef = null; } this.shortcuts = this.shortcuts.filter((s) => (s.id ? s.id !== item.id : s.title !== item.title)); this.validateShortcuts(); if (!this.validationError) { this.keyboardShortcutService.updateShortcuts(this.shortcuts); } } validateSingleShortcut(item: Partial, excludeId: string | null): boolean { if (!item.title || item.title.trim() === '') { this.validationError = 'Shortcut Name is required'; return false; } if (!item.key) { this.validationError = 'Key combination is required'; return false; } if (item.action === 'navigate' && (!item.route || item.route.trim() === '')) { this.validationError = 'URL / Route is required'; return false; } const keyStr = `${!!item.ctrlKey}-${!!item.shiftKey}-${!!item.altKey}-${item.key.toLowerCase()}`; const seen = new Set(); for (const s of this.shortcuts) { if (excludeId && s.id === excludeId) continue; seen.add(`${!!s.ctrlKey}-${!!s.shiftKey}-${!!s.altKey}-${s.key.toLowerCase()}`); } if (seen.has(keyStr)) { this.validationError = `Duplicate shortcut detected: ${this.formatShortcut(item)}`; return false; } this.validationError = null; return true; } save() { this.validateShortcuts(); if (!this.validationError) { this.keyboardShortcutService.updateShortcuts(this.shortcuts); this.dialogRef.close(true); } } resetToDefaults() { const dialogRef = this.dialog.open(ConfirmationDialogComponent, { data: { heading: 'Reset Keyboard Shortcuts', dialogContext: 'Are you sure you want to reset all keyboard shortcuts to their default configuration? This will remove all custom shortcuts and restore default keys.', type: 'Warn' } }); dialogRef.afterClosed().subscribe((confirm: boolean) => { if (confirm) { this.keyboardShortcutService.resetToDefaults(); this.dialogRef.close(true); } }); } cancel() { this.dialogRef.close(false); } }