Files
cbsnexora/src/app/shared/keyboard-shortcuts-dialog/keyboard-shortcuts-dialog.component.ts
T

331 lines
9.9 KiB
TypeScript

/**
* 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<KeyboardShortcutsDialogComponent>>(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<ShortcutItem> = {};
activeNewInput = false;
editingShortcutId: string | null = null;
editingShortcutData: Partial<ShortcutItem> = {};
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<ShortcutItem>): 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<ShortcutItem>, 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<string>();
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<any> | 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<ShortcutItem>): 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<ShortcutItem>, 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<string>();
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);
}
}