2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 14:50:56 +00:00

refactor: use typed injections

- use contextual shortcuts
- type Sidebar.vue, App.vue, ListView.vue
- improve types for search.ts
This commit is contained in:
18alantom 2023-03-21 15:03:13 +05:30 committed by Alan
parent 1259420098
commit 06f981163d
10 changed files with 415 additions and 307 deletions

View File

@ -36,13 +36,12 @@
</div>
</div>
</template>
<script>
<script lang="ts">
import { ConfigKeys } from 'fyo/core/types';
import { RTL_LANGUAGES } from 'fyo/utils/consts';
import { ModelNameEnum } from 'models/types';
import { systemLanguageRef } from 'src/utils/refs';
import { computed } from 'vue';
import { defineComponent, provide, ref, Ref } from 'vue';
import WindowsTitleBar from './components/WindowsTitleBar.vue';
import { handleErrorWithDialog } from './errorHandling';
import { fyo } from './initFyo';
@ -50,8 +49,10 @@ import DatabaseSelector from './pages/DatabaseSelector.vue';
import Desk from './pages/Desk.vue';
import SetupWizard from './pages/SetupWizard/SetupWizard.vue';
import setupInstance from './setup/setupInstance';
import { SetupWizardOptions } from './setup/types';
import './styles/index.css';
import { initializeInstance } from './utils/initialization';
import * as injectionKeys from './utils/injectionKeys';
import { checkForUpdates } from './utils/ipcCalls';
import { updateConfigFiles } from './utils/misc';
import { updatePrintTemplates } from './utils/printTemplates';
@ -60,26 +61,38 @@ import { setGlobalShortcuts } from './utils/shortcuts';
import { routeTo } from './utils/ui';
import { Shortcuts, useKeys } from './utils/vueUtils';
export default {
enum Screen {
Desk = 'Desk',
DatabaseSelector = 'DatabaseSelector',
SetupWizard = 'SetupWizard',
}
export default defineComponent({
name: 'App',
setup() {
return { keys: useKeys() };
const keys = useKeys();
const searcher: Ref<null | Search> = ref(null);
const shortcuts = new Shortcuts(keys);
const languageDirection = ref(
getLanguageDirection(systemLanguageRef.value)
);
provide(injectionKeys.keysKey, keys);
provide(injectionKeys.searcherKey, searcher);
provide(injectionKeys.shortcutsKey, shortcuts);
provide(injectionKeys.languageDirectionKey, languageDirection);
return { keys, searcher, shortcuts, languageDirection };
},
data() {
return {
activeScreen: null,
dbPath: '',
companyName: '',
searcher: null,
shortcuts: null,
};
},
provide() {
return {
languageDirection: computed(() => this.languageDirection),
searcher: computed(() => this.searcher),
shortcuts: computed(() => this.shortcuts),
keys: computed(() => this.keys),
} as {
activeScreen: null | Screen;
dbPath: string;
companyName: string;
};
},
components: {
@ -89,66 +102,77 @@ export default {
WindowsTitleBar,
},
async mounted() {
const shortcuts = new Shortcuts(this.keys);
this.shortcuts = shortcuts;
const lastSelectedFilePath = fyo.config.get(
ConfigKeys.LastSelectedFilePath,
null
);
if (!lastSelectedFilePath) {
return (this.activeScreen = 'DatabaseSelector');
}
try {
await this.fileSelected(lastSelectedFilePath, false);
} catch (err) {
await handleErrorWithDialog(err, undefined, true, true);
await this.showDbSelector();
}
setGlobalShortcuts(shortcuts);
setGlobalShortcuts(this.shortcuts as Shortcuts);
this.setInitialScreen();
},
watch: {
language(value) {
this.languageDirection = getLanguageDirection(value);
},
},
computed: {
language() {
language(): string {
return systemLanguageRef.value;
},
languageDirection() {
return RTL_LANGUAGES.includes(this.language) ? 'rtl' : 'ltr';
},
},
methods: {
async setDesk(filePath) {
this.activeScreen = 'Desk';
await this.setDeskRoute();
await fyo.telemetry.start(true);
await checkForUpdates(false);
this.dbPath = filePath;
this.companyName = await fyo.getValue(
ModelNameEnum.AccountingSettings,
'companyName'
async setInitialScreen(): Promise<void> {
const lastSelectedFilePath = fyo.config.get(
ConfigKeys.LastSelectedFilePath,
null
);
await this.setSearcher();
updateConfigFiles(fyo);
if (
typeof lastSelectedFilePath !== 'string' ||
!lastSelectedFilePath.length
) {
this.activeScreen = Screen.DatabaseSelector;
return;
}
await this.fileSelected(lastSelectedFilePath, false);
},
async setSearcher() {
async setSearcher(): Promise<void> {
this.searcher = new Search(fyo);
await this.searcher.initializeKeywords();
},
async fileSelected(filePath, isNew) {
async setDesk(filePath: string): Promise<void> {
this.activeScreen = Screen.Desk;
await this.setDeskRoute();
await fyo.telemetry.start(true);
await checkForUpdates();
this.dbPath = filePath;
this.companyName = (await fyo.getValue(
ModelNameEnum.AccountingSettings,
'companyName'
)) as string;
await this.setSearcher();
updateConfigFiles(fyo);
},
async fileSelected(filePath: string, isNew?: boolean): Promise<void> {
fyo.config.set(ConfigKeys.LastSelectedFilePath, filePath);
if (isNew) {
this.activeScreen = 'SetupWizard';
this.activeScreen = Screen.SetupWizard;
return;
}
await this.showSetupWizardOrDesk(filePath);
try {
await this.showSetupWizardOrDesk(filePath);
} catch (error) {
await handleErrorWithDialog(error, undefined, true, true);
await this.showDbSelector();
}
},
async setupComplete(setupWizardOptions) {
async setupComplete(setupWizardOptions: SetupWizardOptions): Promise<void> {
const filePath = fyo.config.get(ConfigKeys.LastSelectedFilePath);
if (typeof filePath !== 'string') {
return;
}
await setupInstance(filePath, setupWizardOptions, fyo);
await this.setDesk(filePath);
},
async showSetupWizardOrDesk(filePath) {
async showSetupWizardOrDesk(filePath: string): Promise<void> {
const countryCode = await fyo.db.connectToDatabase(filePath);
const setupComplete = await fyo.getValue(
ModelNameEnum.AccountingSettings,
@ -156,7 +180,7 @@ export default {
);
if (!setupComplete) {
this.activeScreen = 'SetupWizard';
this.activeScreen = Screen.SetupWizard;
return;
}
@ -164,7 +188,7 @@ export default {
await updatePrintTemplates(fyo);
await this.setDesk(filePath);
},
async setDeskRoute() {
async setDeskRoute(): Promise<void> {
const { onboardingComplete } = await fyo.doc.getDoc('GetStarted');
const { hideGetStarted } = await fyo.doc.getDoc('SystemSettings');
@ -174,15 +198,19 @@ export default {
routeTo('/get-started');
}
},
async showDbSelector() {
async showDbSelector(): Promise<void> {
fyo.config.set('lastSelectedFilePath', null);
fyo.telemetry.stop();
await fyo.purgeCache();
this.activeScreen = 'DatabaseSelector';
this.activeScreen = Screen.DatabaseSelector;
this.dbPath = '';
this.searcher = null;
this.companyName = '';
},
},
};
});
function getLanguageDirection(language: string): 'rtl' | 'ltr' {
return RTL_LANGUAGES.includes(language) ? 'rtl' : 'ltr';
}
</script>

View File

@ -15,4 +15,7 @@
<feather-icon name="chevron-left" class="w-4 h-4" />
</a>
</template>
<script></script>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({});
</script>

View File

@ -14,8 +14,10 @@
<slot></slot>
</button>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Button',
props: {
type: {
@ -53,7 +55,7 @@ export default {
};
},
},
};
});
</script>
<style scoped>
button:focus {

View File

@ -30,14 +30,14 @@
</div>
</div>
</template>
<script>
<script lang="ts">
import { languageDirectionKey } from 'src/utils/injectionKeys';
import { showSidebar } from 'src/utils/refs';
import { Transition } from 'vue';
import { defineComponent, inject, Transition } from 'vue';
import BackLink from './BackLink.vue';
import SearchBar from './SearchBar.vue';
export default {
inject: ['languageDirection'],
export default defineComponent({
props: {
title: { type: String, default: '' },
backLink: { type: Boolean, default: true },
@ -45,16 +45,16 @@ export default {
border: { type: Boolean, default: true },
searchborder: { type: Boolean, default: true },
},
components: { SearchBar, BackLink, Transition },
components: { BackLink, SearchBar, Transition },
setup() {
return { showSidebar };
return { showSidebar, languageDirection: inject(languageDirectionKey) };
},
computed: {
showBorder() {
return !!this.$slots.default && this.searchborder;
},
},
};
});
</script>
<style scoped>
.w-tl {

View File

@ -26,8 +26,6 @@
spellcheck="false"
:placeholder="t`Type to search...`"
v-model="inputValue"
@focus="search"
@input="search"
@keydown.up="up"
@keydown.down="down"
@keydown.enter="() => select()"
@ -50,7 +48,7 @@
<div :style="`max-height: ${49 * 6 - 1}px`" class="overflow-auto">
<div
v-for="(si, i) in suggestions"
:key="`${i}-${si.key}`"
:key="`${i}-${si.label}`"
ref="suggestions"
class="hover:bg-gray-50 cursor-pointer"
:class="idx === i ? 'border-blue-500 bg-gray-50 border-s-4' : ''"
@ -97,7 +95,7 @@
:key="g"
class="border px-1 py-0.5 rounded-lg"
:class="getGroupFilterButtonClass(g)"
@click="searcher.set(g, !searcher.filters.groupFilters[g])"
@click="searcher!.set(g, !searcher!.filters.groupFilters[g])"
>
{{ groupLabelMap[g] }}
</button>
@ -115,11 +113,11 @@
<!-- Group Skip Filters -->
<div class="flex gap-1 text-gray-800">
<button
v-for="s in ['skipTables', 'skipTransactions']"
v-for="s in ['skipTables', 'skipTransactions'] as const"
:key="s"
class="border px-1 py-0.5 rounded-lg"
:class="{ 'bg-gray-200': searcher.filters[s] }"
@click="searcher.set(s, !searcher.filters[s])"
:class="{ 'bg-gray-200': searcher?.filters[s] }"
@click="searcher?.set(s, !searcher?.filters[s])"
>
{{
s === 'skipTables' ? t`Skip Child Tables` : t`Skip Transactions`
@ -141,12 +139,12 @@
whitespace-nowrap
"
:class="{
'bg-blue-100': searcher.filters.schemaFilters[sf.value],
'bg-blue-100': searcher?.filters.schemaFilters[sf.value],
}"
@click="
searcher.set(
searcher?.set(
sf.value,
!searcher.filters.schemaFilters[sf.value]
!searcher?.filters.schemaFilters[sf.value]
)
"
>
@ -180,12 +178,12 @@
>
<template
v-for="c in allowedLimits.filter(
(c) => c < searcher.numSearches || c === -1
(c) => c < (searcher?.numSearches ?? 0) || c === -1
)"
:key="c + '-count'"
>
<button
@click="limit = parseInt(c)"
@click="limit = Number(c)"
class="w-9"
:class="limit === c ? 'bg-gray-100' : ''"
>
@ -198,17 +196,32 @@
</div>
</Modal>
</template>
<script>
<script lang="ts">
import { fyo } from 'src/initFyo';
import { getBgTextColorClass } from 'src/utils/colors';
import { searcherKey, shortcutsKey } from 'src/utils/injectionKeys';
import { openLink } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc';
import { getGroupLabelMap, searchGroups } from 'src/utils/search';
import { nextTick } from 'vue';
import {
getGroupLabelMap,
SearchGroup,
searchGroups,
SearchItems,
} from 'src/utils/search';
import { defineComponent, inject, nextTick } from 'vue';
import Button from './Button.vue';
import Modal from './Modal.vue';
export default {
type SchemaFilters = { value: string; label: string; index: number }[];
export default defineComponent({
setup() {
return {
searcher: inject(searcherKey),
shortcuts: inject(shortcutsKey),
};
},
data() {
return {
idx: 0,
@ -220,10 +233,10 @@ export default {
allowedLimits: [50, 100, 500, -1],
};
},
inject: ['searcher', 'shortcuts'],
components: { Modal, Button },
async mounted() {
if (fyo.store.isDevelopment) {
// @ts-ignore
window.search = this;
}
@ -234,15 +247,15 @@ export default {
this.openModal = false;
},
deactivated() {
this.deleteShortcuts();
this.shortcuts!.delete(this);
},
methods: {
openDocs() {
openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
},
getShortcuts() {
const ifOpen = (cb) => () => this.openModal && cb();
const ifClose = (cb) => () => !this.openModal && cb();
const ifOpen = (cb: Function) => () => this.openModal && cb();
const ifClose = (cb: Function) => () => !this.openModal && cb();
const shortcuts = [
{
@ -256,12 +269,16 @@ export default {
shortcut: `Digit${Number(i) + 1}`,
callback: ifOpen(() => {
const group = searchGroups[i];
if (!this.searcher) {
return;
}
const value = this.searcher.filters.groupFilters[group];
if (typeof value !== 'boolean') {
return;
}
this.searcher.set(group, !value);
this.searcher!.set(group, !value);
}),
});
}
@ -270,15 +287,10 @@ export default {
},
setShortcuts() {
for (const { shortcut, callback } of this.getShortcuts()) {
this.shortcuts.pmod.set([shortcut], callback);
this.shortcuts!.pmod.set(this, [shortcut], callback);
}
},
deleteShortcuts() {
for (const { shortcut } of this.getShortcuts()) {
this.shortcuts.pmod.delete([shortcut]);
}
},
modKeyText(key) {
modKeyText(key: string): string {
key = key.toUpperCase();
if (this.platform === 'Mac') {
return `${key}`;
@ -286,41 +298,48 @@ export default {
return `Ctrl ${key}`;
},
open() {
open(): void {
this.openModal = true;
this.searcher?.updateKeywords();
nextTick(() => {
this.$refs.input.focus();
(this.$refs.input as HTMLInputElement).focus();
});
},
close() {
close(): void {
this.openModal = false;
this.reset();
},
reset() {
reset(): void {
this.inputValue = '';
},
up() {
up(): void {
this.idx = Math.max(this.idx - 1, 0);
this.scrollToHighlighted();
},
down() {
down(): void {
this.idx = Math.max(
Math.min(this.idx + 1, this.suggestions.length - 1),
0
);
this.scrollToHighlighted();
},
select(idx) {
select(idx?: number): void {
this.idx = idx ?? this.idx;
this.suggestions[this.idx]?.action();
this.suggestions[this.idx]?.action?.();
this.close();
},
scrollToHighlighted() {
const ref = this.$refs.suggestions[this.idx];
ref.scrollIntoView({ block: 'nearest' });
scrollToHighlighted(): void {
const suggestionRefs = this.$refs.suggestions;
if (!Array.isArray(suggestionRefs)) {
return;
}
const el = suggestionRefs[this.idx];
if (el instanceof HTMLElement) {
el.scrollIntoView({ block: 'nearest' });
}
},
getGroupFilterButtonClass(g) {
getGroupFilterButtonClass(g: SearchGroup): string {
if (!this.searcher) {
return '';
}
@ -335,27 +354,34 @@ export default {
},
},
computed: {
groupLabelMap() {
groupLabelMap(): Record<SearchGroup, string> {
return getGroupLabelMap();
},
schemaFilters() {
const schemaNames = Object.keys(this.searcher?.searchables) ?? [];
return schemaNames
.map((sn) => {
const schema = fyo.schemaMap[sn];
const value = sn;
const label = schema.label;
schemaFilters(): SchemaFilters {
const searchables = this.searcher?.searchables ?? {};
const schemaNames = Object.keys(searchables);
const filters = schemaNames
.map((value) => {
const schema = fyo.schemaMap[value];
if (!schema) {
return;
}
let index = 1;
if (schema.isSubmittable) {
index = 0;
} else if (schema.isChild) {
index = 2;
}
return { value, label, index };
return { value, label: schema.label, index };
})
.sort((a, b) => a.index - b.index);
.filter(Boolean) as SchemaFilters;
return filters.sort((a, b) => a.index - b.index);
},
groupColorMap() {
groupColorMap(): Record<SearchGroup, string> {
return {
Docs: 'blue',
Create: 'green',
@ -364,13 +390,13 @@ export default {
Page: 'orange',
};
},
groupColorClassMap() {
groupColorClassMap(): Record<SearchGroup, string> {
return searchGroups.reduce((map, g) => {
map[g] = getBgTextColorClass(this.groupColorMap[g]);
return map;
}, {});
}, {} as Record<SearchGroup, string>);
},
suggestions() {
suggestions(): SearchItems {
if (!this.searcher) {
return [];
}
@ -383,7 +409,7 @@ export default {
return suggestions.slice(0, this.limit);
},
},
};
});
</script>
<style scoped>
input[type='search']::-webkit-search-decoration,

View File

@ -35,7 +35,7 @@
? 'bg-gray-100 border-s-4 border-blue-500'
: ''
"
@click="onGroupClick(group)"
@click="routeToSidebarItem(group)"
>
<Icon
class="flex-shrink-0"
@ -72,7 +72,7 @@
? 'bg-gray-100 text-blue-600 border-s-4 border-blue-500'
: ''
"
@click="onItemClick(item)"
@click="routeToSidebarItem(item)"
>
<p :style="isItemActive(item) ? 'margin-left: -4px' : ''">
{{ item.label }}
@ -175,29 +175,40 @@
</Modal>
</div>
</template>
<script>
import Button from 'src/components/Button.vue';
<script lang="ts">
import { reportIssue } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { languageDirectionKey, shortcutsKey } from 'src/utils/injectionKeys';
import { openLink } from 'src/utils/ipcCalls';
import { docsPathRef } from 'src/utils/refs';
import { getSidebarConfig } from 'src/utils/sidebarConfig';
import { SidebarConfig, SidebarItem, SidebarRoot } from 'src/utils/types';
import { routeTo, toggleSidebar } from 'src/utils/ui';
import { defineComponent, inject } from 'vue';
import router from '../router';
import Icon from './Icon.vue';
import Modal from './Modal.vue';
import ShortcutsHelper from './ShortcutsHelper.vue';
export default {
components: [Button],
inject: ['languageDirection', 'shortcuts'],
export default defineComponent({
emits: ['change-db-file'],
setup() {
return {
languageDirection: inject(languageDirectionKey),
shortcuts: inject(shortcutsKey),
};
},
data() {
return {
companyName: '',
groups: [],
viewShortcuts: false,
activeGroup: null,
} as {
companyName: string;
groups: SidebarConfig;
viewShortcuts: boolean;
activeGroup: null | SidebarRoot;
};
},
computed: {
@ -212,7 +223,7 @@ export default {
},
async mounted() {
const { companyName } = await fyo.doc.getDoc('AccountingSettings');
this.companyName = companyName;
this.companyName = companyName as string;
this.groups = await getSidebarConfig();
this.setActiveGroup();
@ -220,16 +231,15 @@ export default {
this.setActiveGroup();
});
this.shortcuts.shift.set(['KeyH'], () => {
this.shortcuts?.shift.set(this, ['KeyH'], () => {
if (document.body === document.activeElement) {
this.toggleSidebar();
}
});
this.shortcuts.set(['F1'], () => this.openDocumentation());
this.shortcuts?.set(this, ['F1'], () => this.openDocumentation());
},
unmounted() {
this.shortcuts.alt.delete(['KeyH']);
this.shortcuts.delete(['F1']);
this.shortcuts?.delete(this);
},
methods: {
routeTo,
@ -241,31 +251,30 @@ export default {
setActiveGroup() {
const { fullPath } = this.$router.currentRoute.value;
const fallBackGroup = this.activeGroup;
this.activeGroup = this.groups.find((g) => {
if (fullPath.startsWith(g.route) && g.route !== '/') {
return true;
}
if (g.route === fullPath) {
return true;
}
if (g.items) {
let activeItem = g.items.filter(
({ route }) => route === fullPath || fullPath.startsWith(route)
);
if (activeItem.length) {
this.activeGroup =
this.groups.find((g) => {
if (fullPath.startsWith(g.route) && g.route !== '/') {
return true;
}
}
});
if (!this.activeGroup) {
this.activeGroup = fallBackGroup || this.groups[0];
}
if (g.route === fullPath) {
return true;
}
if (g.items) {
let activeItem = g.items.filter(
({ route }) => route === fullPath || fullPath.startsWith(route)
);
if (activeItem.length) {
return true;
}
}
}) ??
fallBackGroup ??
this.groups[0];
},
isItemActive(item) {
isItemActive(item: SidebarItem) {
const { path: currentRoute, params } = this.$route;
const routeMatch = currentRoute === item.route;
@ -279,28 +288,13 @@ export default {
return isMatch;
},
isGroupActive(group) {
isGroupActive(group: SidebarRoot) {
return this.activeGroup && group.label === this.activeGroup.label;
},
onGroupClick(group) {
if (group.action) {
group.action();
}
if (group.route) {
routeTo(this.getPath(group));
}
routeToSidebarItem(item: SidebarItem | SidebarRoot) {
routeTo(this.getPath(item));
},
onItemClick(item) {
if (item.action) {
item.action();
}
if (item.route) {
routeTo(this.getPath(item));
}
},
getPath(item) {
getPath(item: SidebarItem | SidebarRoot) {
const { route: path, filters } = item;
if (!filters) {
return path;
@ -309,5 +303,5 @@ export default {
return { path, query: { filters: JSON.stringify(filters) } };
},
},
};
});
</script>

View File

@ -41,7 +41,9 @@
</Modal>
</div>
</template>
<script>
<script lang="ts">
import { Doc } from 'fyo/model/doc';
import { Field } from 'schemas/types';
import Button from 'src/components/Button.vue';
import ExportWizard from 'src/components/ExportWizard.vue';
import FilterDropdown from 'src/components/FilterDropdown.vue';
@ -49,22 +51,34 @@ import Modal from 'src/components/Modal.vue';
import PageHeader from 'src/components/PageHeader.vue';
import { fyo } from 'src/initFyo';
import { getRouteData } from 'src/utils/filters';
import { shortcutsKey } from 'src/utils/injectionKeys';
import {
docsPathMap,
getCreateFiltersFromListViewFilters,
} from 'src/utils/misc';
import { docsPathRef } from 'src/utils/refs';
import { openQuickEdit, routeTo } from 'src/utils/ui';
import { Shortcuts } from 'src/utils/vueUtils';
import { QueryFilter } from 'utils/db/types';
import { defineComponent, inject, ref } from 'vue';
import { RouteLocationRaw } from 'vue-router';
import List from './List.vue';
export default {
export default defineComponent({
name: 'ListView',
props: {
schemaName: String,
schemaName: { type: String, required: true },
filters: Object,
pageTitle: { type: String, default: '' },
},
setup() {
return {
shortcuts: inject(shortcutsKey),
list: ref<InstanceType<typeof List> | null>(null),
filterDropdown: ref<InstanceType<typeof FilterDropdown> | null>(null),
makeNewDocButton: ref<InstanceType<typeof Button> | null>(null),
exportButton: ref<InstanceType<typeof Button> | null>(null),
};
},
components: {
PageHeader,
List,
@ -78,41 +92,52 @@ export default {
listConfig: undefined,
openExportModal: false,
listFilters: {},
} as {
listConfig: undefined | ReturnType<typeof getListConfig>;
openExportModal: boolean;
listFilters: QueryFilter;
};
},
inject: { shortcutManager: { from: 'shortcuts' } },
async activated() {
if (typeof this.filters === 'object') {
this.$refs.filterDropdown.setFilter(this.filters, true);
this.filterDropdown?.setFilter(this.filters, true);
}
this.listConfig = getListConfig(this.schemaName);
docsPathRef.value = docsPathMap[this.schemaName] ?? docsPathMap.Entries;
docsPathRef.value =
docsPathMap[this.schemaName] ?? docsPathMap.Entries ?? '';
if (this.fyo.store.isDevelopment) {
// @ts-ignore
window.lv = this;
}
this.shortcuts.pmod.set(['KeyN'], () =>
this.$refs.makeNewDocButton.$el.click()
);
this.shortcuts.pmod.set(['KeyE'], () =>
this.$refs.exportButton.$el.click()
);
this.setShortcuts();
},
deactivated() {
docsPathRef.value = '';
this.shortcuts.pmod.delete(['KeyN']);
this.shortcuts.pmod.delete(['KeyE']);
this.shortcuts?.delete(this);
},
methods: {
updatedData(listFilters) {
setShortcuts() {
if (!this.shortcuts) {
return;
}
this.shortcuts.pmod.set(this, ['KeyN'], () =>
this.makeNewDocButton?.$el.click()
);
this.shortcuts.pmod.set(this, ['KeyE'], () =>
this.exportButton?.$el.click()
);
},
updatedData(listFilters: QueryFilter) {
this.listFilters = listFilters;
},
async openDoc(name) {
async openDoc(name: string) {
const doc = await this.fyo.doc.getDoc(this.schemaName, name);
if (this.listConfig.formRoute) {
if (this.listConfig?.formRoute) {
return await routeTo(this.listConfig.formRoute(name));
}
@ -138,13 +163,21 @@ export default {
this.$router.replace(path);
});
},
applyFilter(filters) {
this.$refs.list.updateData(filters);
applyFilter(filters: QueryFilter) {
this.list?.updateData(filters);
},
getFormPath(doc) {
getFormPath(doc: Doc) {
const { label, routeFilter } = getRouteData({ doc });
let path = {
path: `/list/${this.schemaName}/${label}`,
const currentPath = this.$router.currentRoute.value.path;
// Maintain filters
let path = `/list/${this.schemaName}/${label}`;
if (currentPath.startsWith(path)) {
path = currentPath;
}
let route: RouteLocationRaw = {
path,
query: {
edit: 1,
schemaName: this.schemaName,
@ -153,47 +186,31 @@ export default {
},
};
if (this.listConfig.formRoute) {
path = this.listConfig.formRoute(doc.name);
if (this.listConfig?.formRoute) {
route = this.listConfig.formRoute(doc.name!);
}
if (typeof path === 'object') {
return path;
}
// Maintain filter if present
const currentPath = this.$router.currentRoute.value.path;
if (currentPath.slice(0, path?.path?.length ?? 0) === path.path) {
path.path = currentPath;
}
return path;
return route;
},
},
computed: {
title() {
return this.pageTitle || fyo.schemaMap[this.schemaName].label;
},
fields() {
return fyo.schemaMap[this.schemaName].fields;
},
canCreate() {
return fyo.schemaMap[this.schemaName].create !== false;
},
shortcuts() {
// @ts-ignore
const shortcutManager = this.shortcutManager;
if (shortcutManager instanceof Shortcuts) {
return shortcutManager;
title(): string {
if (this.pageTitle) {
return this.pageTitle;
}
// no-op (hopefully)
throw Error('Shortcuts Not Found');
return fyo.schemaMap[this.schemaName]?.label ?? this.schemaName;
},
fields(): Field[] {
return fyo.schemaMap[this.schemaName]?.fields ?? [];
},
canCreate(): boolean {
return fyo.schemaMap[this.schemaName]?.create !== false;
},
},
};
});
function getListConfig(schemaName) {
function getListConfig(schemaName: string) {
const listConfig = fyo.models[schemaName]?.getListViewSettings?.(fyo);
if (listConfig?.columns === undefined) {
return {

View File

@ -219,6 +219,7 @@ import HorizontalResizer from 'src/components/HorizontalResizer.vue';
import PageHeader from 'src/components/PageHeader.vue';
import ShortcutKeys from 'src/components/ShortcutKeys.vue';
import { handleErrorWithDialog } from 'src/errorHandling';
import { shortcutsKey } from 'src/utils/injectionKeys';
import { getSavePath } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc';
import {
@ -238,12 +239,12 @@ import {
showMessageDialog,
showToast,
} from 'src/utils/ui';
import { Shortcuts } from 'src/utils/vueUtils';
import { getMapFromList } from 'utils/index';
import { computed, defineComponent } from 'vue';
import PrintContainer from './PrintContainer.vue';
import TemplateBuilderHint from './TemplateBuilderHint.vue';
import TemplateEditor from './TemplateEditor.vue';
import { inject } from 'vue';
export default defineComponent({
props: { name: String },
@ -258,7 +259,11 @@ export default defineComponent({
TemplateBuilderHint,
ShortcutKeys,
},
inject: { shortcutManager: { from: 'shortcuts' } },
setup() {
return {
shortcuts: inject(shortcutsKey),
};
},
provide() {
return { doc: computed(() => this.doc) };
},
@ -304,21 +309,28 @@ export default defineComponent({
},
async activated(): Promise<void> {
docsPathRef.value = docsPathMap.PrintTemplate ?? '';
this.shortcuts.ctrl.set(['Enter'], this.setTemplate);
this.shortcuts.ctrl.set(['KeyE'], this.toggleEditMode);
this.shortcuts.ctrl.set(['KeyH'], this.toggleShowHints);
this.shortcuts.ctrl.set(['Equal'], () => this.setScale(this.scale + 0.1));
this.shortcuts.ctrl.set(['Minus'], () => this.setScale(this.scale - 0.1));
this.setShortcuts;
},
deactivated(): void {
docsPathRef.value = '';
this.shortcuts.ctrl.delete(['Enter']);
this.shortcuts.ctrl.delete(['KeyE']);
this.shortcuts.ctrl.delete(['KeyH']);
this.shortcuts.ctrl.delete(['Equal']);
this.shortcuts.ctrl.delete(['Minus']);
this.shortcuts?.delete(this);
},
methods: {
setShortcuts() {
if (!this.shortcuts) {
return;
}
this.shortcuts.ctrl.set(this, ['Enter'], this.setTemplate);
this.shortcuts.ctrl.set(this, ['KeyE'], this.toggleEditMode);
this.shortcuts.ctrl.set(this, ['KeyH'], this.toggleShowHints);
this.shortcuts.ctrl.set(this, ['Equal'], () =>
this.setScale(this.scale + 0.1)
);
this.shortcuts.ctrl.set(this, ['Minus'], () =>
this.setScale(this.scale - 0.1)
);
},
async initialize() {
await this.setDoc();
if (this.doc?.type) {
@ -580,17 +592,6 @@ export default defineComponent({
return null;
},
shortcuts(): Shortcuts {
// @ts-ignore
const shortcutManager = this.shortcutManager;
if (shortcutManager instanceof Shortcuts) {
return shortcutManager;
}
// no-op (hopefully)
throw Error('Shortcuts Not Found');
},
maxWidth() {
return window.innerWidth - 12 * 16 - 100;
},

View File

@ -0,0 +1,17 @@
import { InjectionKey, Ref } from 'vue';
import { Search } from './search';
import type { Shortcuts, useKeys } from './vueUtils';
export const languageDirectionKey = Symbol('languageDirection') as InjectionKey<
Ref<'ltr' | 'rtl'>
>;
export const keysKey = Symbol('keys') as InjectionKey<
ReturnType<typeof useKeys>
>;
export const searcherKey = Symbol('searcher') as InjectionKey<
Ref<null | Search>
>;
export const shortcutsKey = Symbol('shortcuts') as InjectionKey<Shortcuts>;

View File

@ -11,29 +11,29 @@ import { RouteLocationRaw } from 'vue-router';
import { fuzzyMatch } from '.';
import { getFormRoute, routeTo } from './ui';
export const searchGroups = ['Docs', 'List', 'Create', 'Report', 'Page'];
enum SearchGroupEnum {
'List' = 'List',
'Report' = 'Report',
'Create' = 'Create',
'Page' = 'Page',
'Docs' = 'Docs',
}
export const searchGroups = [
'Docs',
'List',
'Create',
'Report',
'Page',
] as const;
type SearchGroup = keyof typeof SearchGroupEnum;
export type SearchGroup = typeof searchGroups[number];
interface SearchItem {
label: string;
group: SearchGroup;
group: Exclude<SearchGroup, 'Docs'>;
route?: string;
action?: () => void;
}
interface DocSearchItem extends SearchItem {
interface DocSearchItem extends Omit<SearchItem, 'group'> {
group: 'Docs';
schemaLabel: string;
more: string[];
}
type SearchItems = (DocSearchItem | SearchItem)[];
export type SearchItems = (DocSearchItem | SearchItem)[];
interface Searchable {
needsUpdate: boolean;
@ -604,10 +604,7 @@ export class Search {
return;
}
const subArray = this._getSubSortedArray(
keywords,
input
) as DocSearchItem[];
const subArray = this._getSubSortedArray(keywords, input);
array.push(...subArray);
}
@ -615,7 +612,7 @@ export class Search {
const filtered = this._nonDocSearchList.filter(
(si) => this.filters.groupFilters[si.group]
);
const subArray = this._getSubSortedArray(filtered, input) as SearchItem[];
const subArray = this._getSubSortedArray(filtered, input);
array.push(...subArray);
}
@ -627,38 +624,68 @@ export class Search {
[];
for (const item of items) {
const isSearchItem = !!(item as SearchItem).group;
if (!input && isSearchItem) {
subArray.push({ item: item as SearchItem, distance: 0 });
const subArrayItem = this._getSubArrayItem(item, input);
if (!subArrayItem) {
continue;
}
if (!input) {
continue;
}
const values = this._getValueList(item).filter(Boolean);
const { isMatch, distance } = this._getMatchAndDistance(input, values);
if (!isMatch) {
continue;
}
if (isSearchItem) {
subArray.push({ item: item as SearchItem, distance });
} else {
subArray.push({
item: this._getDocSearchItemFromKeyword(item as Keyword),
distance,
});
}
subArray.push(subArrayItem);
}
subArray.sort((a, b) => a.distance - b.distance);
return subArray.map(({ item }) => item);
}
_getSubArrayItem(item: SearchItem | Keyword, input?: string) {
if (isSearchItem(item)) {
return this._getSubArrayItemFromSearchItem(item, input);
}
if (!input) {
return null;
}
return this._getSubArrayItemFromKeyword(item, input);
}
_getSubArrayItemFromSearchItem(item: SearchItem, input?: string) {
if (!input) {
return { item, distance: 0 };
}
const values = this._getValueListFromSearchItem(item).filter(Boolean);
const { isMatch, distance } = this._getMatchAndDistance(input, values);
if (!isMatch) {
return null;
}
return { item, distance };
}
_getValueListFromSearchItem({ label, group }: SearchItem): string[] {
return [label, group];
}
_getSubArrayItemFromKeyword(item: Keyword, input: string) {
const values = this._getValueListFromKeyword(item).filter(Boolean);
const { isMatch, distance } = this._getMatchAndDistance(input, values);
if (!isMatch) {
return null;
}
return {
item: this._getDocSearchItemFromKeyword(item),
distance,
};
}
_getValueListFromKeyword({ values, meta }: Keyword): string[] {
const schemaLabel = meta.schemaName as string;
return [values, schemaLabel].flat();
}
_getMatchAndDistance(input: string, values: string[]) {
/**
* All the parts should match with something.
@ -693,17 +720,6 @@ export class Search {
return { isMatch, distance };
}
_getValueList(item: SearchItem | Keyword): string[] {
const { label, group } = item as SearchItem;
if (group && group !== 'Docs') {
return [label, group];
}
const { values, meta } = item as Keyword;
const schemaLabel = meta.schemaName as string;
return [values, schemaLabel].flat();
}
_getDocSearchItemFromKeyword(keyword: Keyword): DocSearchItem {
const schemaName = keyword.meta.schemaName as string;
const schemaLabel = this.fyo.schemaMap[schemaName]?.label!;
@ -874,3 +890,7 @@ export class Search {
}
}
}
function isSearchItem(item: SearchItem | Keyword): item is SearchItem {
return !!(item as SearchItem).group;
}