2
0
mirror of https://github.com/frappe/books.git synced 2024-11-10 07:40:55 +00:00

refactor: add context to shortcuts

- remove ref based doc shortcuts
This commit is contained in:
18alantom 2023-03-21 13:22:49 +05:30 committed by Alan
parent 9bce0f1ae8
commit 1259420098
11 changed files with 150 additions and 173 deletions

View File

@ -96,8 +96,8 @@ export async function handleError(
export async function handleErrorWithDialog(
error: unknown,
doc?: Doc,
reportError?: false,
dontThrow?: false
reportError?: boolean,
dontThrow?: boolean
) {
if (!(error instanceof Error)) {
return;

View File

@ -119,10 +119,9 @@ import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue';
import { handleErrorWithDialog } from 'src/errorHandling';
import { getErrorMessage } from 'src/utils';
import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import { docsPathRef } from 'src/utils/refs';
import { ActionGroup, UIGroupedFields } from 'src/utils/types';
import {
commonDocSubmit,
@ -173,7 +172,6 @@ export default defineComponent({
}
await this.setDoc();
focusedDocsRef.add(this.docOrNull);
this.updateGroupedFields();
if (this.groupedFields) {
this.activeTab = [...this.groupedFields.keys()][0];
@ -182,13 +180,9 @@ export default defineComponent({
},
activated(): void {
docsPathRef.value = docsPathMap[this.schemaName] ?? '';
focusedDocsRef.add(this.docOrNull);
},
deactivated(): void {
docsPathRef.value = '';
if (this.docOrNull) {
focusedDocsRef.delete(this.doc);
}
},
computed: {
hasDoc(): boolean {

View File

@ -314,10 +314,10 @@ import StatusBadge from 'src/components/StatusBadge.vue';
import LinkedEntryWidget from 'src/components/Widgets/LinkedEntryWidget.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import { docsPathRef } from 'src/utils/refs';
import {
commonDocSync,
commonDocSubmit,
commonDocSync,
getGroupedActionsForDoc,
routeTo,
} from 'src/utils/ui';
@ -458,16 +458,13 @@ export default {
},
activated() {
docsPathRef.value = docsPathMap[this.schemaName];
focusedDocsRef.add(this.doc);
},
deactivated() {
docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
},
async mounted() {
try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) {
if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`);

View File

@ -114,13 +114,12 @@ import StatusBadge from 'src/components/StatusBadge.vue';
import TwoColumnForm from 'src/components/TwoColumnForm.vue';
import { fyo } from 'src/initFyo';
import { getQuickEditWidget } from 'src/utils/quickEditWidgets';
import { focusedDocsRef } from 'src/utils/refs';
import {
commonDocSubmit,
commonDocSync,
focusOrSelectFormControl,
getActionsForDoc,
openQuickEdit,
commonDocSubmit,
commonDocSync,
focusOrSelectFormControl,
getActionsForDoc,
openQuickEdit
} from 'src/utils/ui';
export default {
@ -170,21 +169,11 @@ export default {
await this.fetchFieldsAndDoc();
focusOrSelectFormControl(this.doc, this.$refs.titleControl, false);
focusedDocsRef.add(this.doc);
if (fyo.store.isDevelopment) {
window.qef = this;
}
},
activated() {
focusedDocsRef.add(this.doc);
},
deactivated() {
focusedDocsRef.delete(this.doc);
},
unmounted() {
focusedDocsRef.delete(this.doc);
},
computed: {
isChild() {
return !!this?.doc?.schema?.isChild;

View File

@ -149,7 +149,15 @@
</div>
<div
v-if="templateChanged"
class="flex gap-2 p-2 text-sm text-gray-600 items-center mt-auto border-t"
class="
flex
gap-2
p-2
text-sm text-gray-600
items-center
mt-auto
border-t
"
>
<ShortcutKeys :keys="applyChangesShortcut" :simple="true" />
{{ t` to apply changes` }}
@ -218,7 +226,7 @@ import {
getPrintTemplatePropHints,
getPrintTemplatePropValues,
} from 'src/utils/printTemplates';
import { docsPathRef, focusedDocsRef, showSidebar } from 'src/utils/refs';
import { docsPathRef, showSidebar } from 'src/utils/refs';
import { PrintValues } from 'src/utils/types';
import {
focusOrSelectFormControl,
@ -289,7 +297,6 @@ export default defineComponent({
},
async mounted() {
await this.initialize();
focusedDocsRef.add(this.doc);
if (this.fyo.store.isDevelopment) {
// @ts-ignore
window.tb = this;
@ -305,9 +312,6 @@ export default defineComponent({
},
deactivated(): void {
docsPathRef.value = '';
if (this.doc instanceof Doc) {
focusedDocsRef.delete(this.doc);
}
this.shortcuts.ctrl.delete(['Enter']);
this.shortcuts.ctrl.delete(['KeyE']);
this.shortcuts.ctrl.delete(['KeyH']);

View File

@ -163,45 +163,3 @@ export function getCreateFiltersFromListViewFilters(filters: QueryFilter) {
return createFilters;
}
export class FocusedDocContextSet {
set: Doc[];
constructor() {
this.set = [];
}
add(doc: unknown) {
if (!(doc instanceof Doc)) {
return;
}
const index = this.findIndex(doc);
if (index !== -1) {
this.delete(index);
}
return this.set.push(doc);
}
delete(index: Doc | number) {
if (typeof index !== 'number') {
index = this.findIndex(index);
}
if (index === -1) {
return;
}
this.set = this.set.filter((_, i) => i !== index);
}
last() {
return this.set.at(-1);
}
findIndex(doc: Doc) {
return this.set.findIndex(
(d) => d.name === doc.name && d.schemaName === doc.schemaName
);
}
}

View File

@ -1,9 +1,5 @@
import { reactive, ref } from 'vue';
import { FocusedDocContextSet } from './misc';
import { ref } from 'vue';
export const showSidebar = ref(true);
export const docsPathRef = ref<string>('');
export const systemLanguageRef = ref<string>('');
export const focusedDocsRef = reactive<FocusedDocContextSet>(
new FocusedDocContextSet()
);

View File

@ -1,9 +1,4 @@
import { t } from 'fyo';
import type { Doc } from 'fyo/model/doc';
import { fyo } from 'src/initFyo';
import router from 'src/router';
import { focusedDocsRef } from './refs';
import { showMessageDialog } from './ui';
import { Shortcuts } from './vueUtils';
export function setGlobalShortcuts(shortcuts: Shortcuts) {
@ -11,68 +6,12 @@ export function setGlobalShortcuts(shortcuts: Shortcuts) {
* PMod : if macOS then Meta () else Ctrl, both Left and Right
*
* Backspace : Go to the previous page
* PMod + S : Save or Submit focused doc if possible
* PMod + Backspace : Cancel or Delete focused doc if possible
*/
shortcuts.set(['Backspace'], async () => {
shortcuts.set(window, ['Backspace'], async () => {
if (document.body !== document.activeElement) {
return;
}
router.back();
});
shortcuts.pmod.set(['KeyS'], async () => {
const doc = focusedDocsRef.last();
if (!doc) {
return;
}
if (doc.canSave) {
await showDocStateChangeMessageDialog(doc, 'sync');
} else if (doc.canSubmit) {
await showDocStateChangeMessageDialog(doc, 'submit');
}
});
shortcuts.pmod.set(['Backspace'], async () => {
const doc = focusedDocsRef.last();
if (!doc) {
return;
}
if (doc.canCancel) {
await showDocStateChangeMessageDialog(doc, 'cancel');
} else if (doc.canDelete) {
await showDocStateChangeMessageDialog(doc, 'delete');
}
});
}
async function showDocStateChangeMessageDialog(
doc: Doc,
state: 'sync' | 'submit' | 'cancel' | 'delete'
) {
const label = fyo.schemaMap[doc.schemaName]?.label ?? t`Doc`;
const name = doc.name ?? '';
const message =
{ sync: t`Save`, submit: t`Submit`, cancel: t`Cancel`, delete: t`Delete` }[
state
] + ` ${label} ${name}`;
await showMessageDialog({
message,
buttons: [
{
label: t`Yes`,
async action() {
await doc[state]();
},
},
{
label: t`No`,
action() {},
},
],
});
}

View File

@ -1,7 +1,7 @@
import { t } from 'fyo';
import { routeFilters } from 'src/utils/filters';
import { fyo } from '../initFyo';
import { SidebarConfig, SidebarRoot } from './types';
import { SidebarConfig, SidebarItem, SidebarRoot } from './types';
export async function getSidebarConfig(): Promise<SidebarConfig> {
const sideBar = await getCompleteSidebar();
@ -95,7 +95,7 @@ async function getInventorySidebar(): Promise<SidebarRoot[]> {
label: t`Stock Balance`,
name: 'stock-balance',
route: '/report/StockBalance',
}
},
],
},
];
@ -182,7 +182,7 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
schemaName: 'Item',
filters: routeFilters.SalesItems,
},
],
] as SidebarItem[],
},
{
label: t`Purchases`,
@ -217,7 +217,7 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
schemaName: 'Item',
filters: routeFilters.PurchaseItems,
},
],
] as SidebarItem[],
},
{
label: t`Common`,
@ -245,7 +245,7 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
schemaName: 'Item',
filters: { for: 'Both' },
},
],
] as SidebarItem[],
},
await getReportSidebar(),
await getInventorySidebar(),
@ -282,7 +282,7 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
name: 'settings',
route: '/settings',
},
],
] as SidebarItem[],
},
].flat();
}

View File

@ -59,6 +59,7 @@ export interface SidebarItem {
route: string;
schemaName?: string;
hidden?: () => boolean;
filters?: QueryFilter;
}
export interface ExportField {

View File

@ -14,17 +14,26 @@ interface Keys extends ModMap {
pressed: Set<string>;
}
type Context = unknown;
type ShortcutFunction = () => void;
type ShortcutConfig = {
callback: ShortcutFunction;
propagate: boolean;
};
type ShortcutMap = Map<Context, Map<string, ShortcutConfig>>;
const mods: Readonly<Mod[]> = ['alt', 'ctrl', 'meta', 'repeat', 'shift'];
export class Shortcuts {
keys: Keys;
isMac: boolean;
shortcuts: Map<string, ShortcutFunction>;
contextStack: Context[];
shortcuts: ShortcutMap;
modMap: Partial<Record<Mod, boolean>>;
constructor(keys?: Keys) {
this.contextStack = [];
this.modMap = {};
this.keys = keys ?? useKeys();
this.shortcuts = new Map();
@ -37,37 +46,107 @@ export class Shortcuts {
#trigger(keys: Keys) {
const key = this.getKey(Array.from(keys.pressed), keys);
this.shortcuts.get(key)?.();
for (const context of this.contextStack.reverse()) {
const obj = this.shortcuts.get(context)?.get(key);
if (!obj) {
continue;
}
obj.callback();
if (!obj.propagate) {
break;
}
}
}
has(shortcut: string[]) {
const key = this.getKey(shortcut);
return this.shortcuts.has(key);
}
set(
shortcut: string[],
callback: ShortcutFunction,
removeIfSet: boolean = true
) {
const key = this.getKey(shortcut);
if (removeIfSet) {
this.shortcuts.delete(key);
/**
* Check if a context is present or if a shortcut
* is present in a context.
*
*
* @param context context in which the shortcut is to be checked
* @param shortcut shortcut that is to be checked
* @returns
*/
has(context: Context, shortcut?: string[]) {
if (!shortcut) {
return this.shortcuts.has(context);
}
if (this.shortcuts.has(key)) {
const contextualShortcuts = this.shortcuts.get(context);
if (!contextualShortcuts) {
return false;
}
const key = this.getKey(shortcut);
return contextualShortcuts.has(key);
}
/**
* Assign a function to a shortcut in a given context.
*
* @param context context object to which the shortcut belongs
* @param shortcut keyboard event codes used as shortcut chord
* @param callback function to be called when the shortcut is pressed
* @param propagate whether to check and executs shortcuts in parent contexts
* @param removeIfSet whether to delete the set shortcut
*/
set(
context: Context,
shortcut: string[],
callback: ShortcutFunction,
propagate: boolean = false,
removeIfSet: boolean = true
) {
if (!this.shortcuts.has(context)) {
this.shortcuts.set(context, new Map());
}
const contextualShortcuts = this.shortcuts.get(context)!;
const key = this.getKey(shortcut);
if (removeIfSet) {
contextualShortcuts.delete(key);
}
if (contextualShortcuts.has(key)) {
throw new Error(`Shortcut ${key} already exists.`);
}
this.shortcuts.set(key, callback);
this.#pushContext(context);
contextualShortcuts.set(key, { callback, propagate });
}
delete(shortcut: string[]) {
/**
* Either delete a single shortcut or all the shortcuts in
* a given context.
*
* @param context context from which the shortcut is to be removed
* @param shortcut shortcut that is to be deleted
* @returns boolean indicating success
*/
delete(context: Context, shortcut?: string[]): boolean {
if (!shortcut) {
this.#removeContext(context);
return this.shortcuts.delete(context);
}
const contextualShortcuts = this.shortcuts.get(context);
if (!contextualShortcuts) {
return false;
}
const key = this.getKey(shortcut);
this.shortcuts.delete(key);
return contextualShortcuts.delete(key);
}
/**
* Converts shortcuts list and modMap to a string to be
* used as a shortcut key.
*
* @param shortcut array of shortcut keys
* @param modMap boolean map of mod keys to be used
* @returns string to be used as the shortcut Map key
*/
getKey(shortcut: string[], modMap?: Partial<ModMap>): string {
const _modMap = modMap || this.modMap;
this.modMap = {};
@ -89,6 +168,26 @@ export class Shortcuts {
return '';
}
#pushContext(context: Context) {
this.#removeContext(context);
this.contextStack.push(context);
}
#removeContext(context: Context) {
const index = this.contextStack.indexOf(context);
if (index === -1) {
return;
}
this.contextStack = [
this.contextStack.slice(0, index),
this.contextStack.slice(index + 1),
].flat();
}
/**
* Shortcut Modifiers
*/
get alt() {
this.modMap['alt'] = true;
return this;
@ -117,9 +216,9 @@ export class Shortcuts {
get pmod() {
if (this.isMac) {
return this.meta;
} else {
return this.ctrl;
}
return this.ctrl;
}
}