Implement keyboard shortcuts and workflow changes
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="ks-header">
|
||||
<h1 mat-dialog-title class="ks-title">Create Shortcut</h1>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-content>
|
||||
@if (validationError) {
|
||||
<div class="alert error margin-b-10" style="color: #ef4444; margin-bottom: 15px; font-size: 13px">
|
||||
{{ validationError }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="layout-column gap-10px" style="padding-top: 10px">
|
||||
<mat-form-field class="ks-field" style="width: 100%">
|
||||
<mat-label>Shortcut Name</mat-label>
|
||||
<input matInput [(ngModel)]="newShortcut.title" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="ks-field" style="width: 100%">
|
||||
<mat-label>Key Combination</mat-label>
|
||||
<input
|
||||
matInput
|
||||
readonly
|
||||
[value]="formatShortcut(newShortcut)"
|
||||
(keydown)="onKeyDown($event)"
|
||||
placeholder="Click and press keys..."
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="ks-field" style="width: 100%">
|
||||
<mat-label>Route</mat-label>
|
||||
<input matInput [ngModel]="newShortcut.route" disabled="true" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="ks-field" style="width: 100%">
|
||||
<mat-label>Action</mat-label>
|
||||
<input matInput [ngModel]="'Navigate'" disabled="true" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions class="ks-actions">
|
||||
<div style="flex: 1"></div>
|
||||
<div class="ks-actions__right">
|
||||
<button mat-stroked-button class="ks-actions__cancel" (click)="cancel()">Cancel</button>
|
||||
<button mat-flat-button class="ks-actions__confirm ks-actions__confirm--basic" (click)="save()">Save</button>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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, inject } from '@angular/core';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions
|
||||
} from '@angular/material/dialog';
|
||||
import { KeyboardShortcutService, ShortcutItem } from '../../core/services/keyboard-shortcut.service';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatFormField, MatLabel, MatError } from '@angular/material/form-field';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-inline-create-shortcut-dialog',
|
||||
templateUrl: './inline-create-shortcut-dialog.component.html',
|
||||
styleUrls: ['../keyboard-shortcuts-dialog/keyboard-shortcuts-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
FormsModule,
|
||||
MatFormField,
|
||||
MatLabel,
|
||||
MatInput,
|
||||
MatError
|
||||
]
|
||||
})
|
||||
export class InlineCreateShortcutDialogComponent {
|
||||
dialogRef = inject<MatDialogRef<InlineCreateShortcutDialogComponent>>(MatDialogRef);
|
||||
data = inject(MAT_DIALOG_DATA);
|
||||
private keyboardShortcutService = inject(KeyboardShortcutService);
|
||||
|
||||
newShortcut: Partial<ShortcutItem> = {
|
||||
title: this.data.pageName,
|
||||
route: this.data.route,
|
||||
action: 'navigate',
|
||||
key: '',
|
||||
ctrlKey: false,
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
custom: true,
|
||||
id: 'custom_' + Date.now()
|
||||
};
|
||||
|
||||
validationError: string | null = null;
|
||||
|
||||
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) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Ignore if only modifier keys are pressed
|
||||
if ([
|
||||
'Control',
|
||||
'Shift',
|
||||
'Alt',
|
||||
'Meta'
|
||||
].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.newShortcut.ctrlKey = event.ctrlKey;
|
||||
this.newShortcut.shiftKey = event.shiftKey;
|
||||
this.newShortcut.altKey = event.altKey;
|
||||
this.newShortcut.key = event.key.toLowerCase();
|
||||
this.validationError = null;
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.newShortcut.title || this.newShortcut.title.trim() === '') {
|
||||
this.validationError = 'Shortcut Name is required';
|
||||
return;
|
||||
}
|
||||
if (!this.newShortcut.key) {
|
||||
this.validationError = 'Key combination is required';
|
||||
return;
|
||||
}
|
||||
|
||||
const keyStr = `${!!this.newShortcut.ctrlKey}-${!!this.newShortcut.shiftKey}-${!!this.newShortcut.altKey}-${this.newShortcut.key.toLowerCase()}`;
|
||||
|
||||
this.keyboardShortcutService.activeShortcuts$.pipe(take(1)).subscribe((shortcuts) => {
|
||||
// Check for duplicates
|
||||
const seen = new Set<string>();
|
||||
for (const s of shortcuts) {
|
||||
seen.add(`${!!s.ctrlKey}-${!!s.shiftKey}-${!!s.altKey}-${s.key.toLowerCase()}`);
|
||||
}
|
||||
|
||||
if (seen.has(keyStr)) {
|
||||
this.validationError = `Duplicate shortcut detected: ${this.formatShortcut(this.newShortcut)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add and save
|
||||
if (this.newShortcut.route) {
|
||||
this.newShortcut.route = this.keyboardShortcutService.normalizeRouteStr(this.newShortcut.route);
|
||||
}
|
||||
shortcuts.push(this.newShortcut as ShortcutItem);
|
||||
this.keyboardShortcutService.updateShortcuts(shortcuts);
|
||||
this.dialogRef.close(true);
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<h1 mat-dialog-title>Shortcut Usage</h1>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<table mat-table [dataSource]="usageData" class="mat-elevation-z1" style="width: 100%; margin-top: 10px">
|
||||
<!-- Page Column -->
|
||||
<ng-container matColumnDef="page">
|
||||
<th mat-header-cell *matHeaderCellDef>Page</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div>{{ item.pageName }}</div>
|
||||
<div style="font-size: 11px; color: #888">{{ item.route }}</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Visits Column -->
|
||||
<ng-container matColumnDef="visits">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center">Visits</th>
|
||||
<td mat-cell *matCellDef="let item" style="text-align: center">
|
||||
<span style="font-weight: 500">{{ item.visits }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status / Action Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: right">Status / Action</th>
|
||||
<td mat-cell *matCellDef="let item" style="text-align: right">
|
||||
@if (item.hasShortcut) {
|
||||
<span style="color: #4caf50; font-size: 13px; font-weight: 500">✓ Shortcut exists</span>
|
||||
} @else if (item.visits >= 5) {
|
||||
<span style="color: #ff9800; font-size: 12px; margin-right: 10px; font-weight: 500">Frequently Used</span>
|
||||
<button mat-button color="primary" (click)="createShortcut(item)">Create Shortcut</button>
|
||||
} @else {
|
||||
<span style="color: #aaa; font-size: 12px">Not enough visits</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
|
||||
@if (usageData.length === 0) {
|
||||
<div style="padding: 20px; text-align: center; color: #888; border: 1px solid #eee; border-top: none">
|
||||
No usage data collected yet.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>Close</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
// Usage dialog styling
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import {
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialog
|
||||
} from '@angular/material/dialog';
|
||||
import { ShortcutSuggestionService } from '../../core/services/shortcut-suggestion.service';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import {
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { InlineCreateShortcutDialogComponent } from './inline-create-shortcut-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-shortcut-usage-dialog',
|
||||
templateUrl: './shortcut-usage-dialog.component.html',
|
||||
styleUrls: ['./shortcut-usage-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatButton,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow
|
||||
]
|
||||
})
|
||||
export class ShortcutUsageDialogComponent implements OnInit {
|
||||
dialogRef = inject<MatDialogRef<ShortcutUsageDialogComponent>>(MatDialogRef);
|
||||
private shortcutSuggestionService = inject(ShortcutSuggestionService);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
usageData: { route: string; pageName: string; visits: number; hasShortcut: boolean }[] = [];
|
||||
displayedColumns: string[] = [
|
||||
'page',
|
||||
'visits',
|
||||
'status'
|
||||
];
|
||||
|
||||
ngOnInit() {
|
||||
this.refreshData();
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.usageData = this.shortcutSuggestionService.getUsageData();
|
||||
}
|
||||
|
||||
createShortcut(item: { pageName: string; route: string }) {
|
||||
const inlineDialogRef = this.dialog.open(InlineCreateShortcutDialogComponent, {
|
||||
data: item,
|
||||
width: '400px',
|
||||
disableClose: true
|
||||
});
|
||||
|
||||
inlineDialogRef.afterClosed().subscribe((saved) => {
|
||||
if (saved) {
|
||||
this.shortcutSuggestionService.markAsDismissed(item.route);
|
||||
this.refreshData(); // update the table so the checkmark appears!
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user