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:
parent
9bce0f1ae8
commit
1259420098
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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}`);
|
||||
|
@ -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;
|
||||
|
@ -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']);
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
);
|
||||
|
@ -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() {},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ export interface SidebarItem {
|
||||
route: string;
|
||||
schemaName?: string;
|
||||
hidden?: () => boolean;
|
||||
filters?: QueryFilter;
|
||||
}
|
||||
|
||||
export interface ExportField {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user