2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 14:48:25 +00:00

refactor: Dropdown.vue (add types, simplify)

- fix dropdown keyboard item selection
- add new entry shortcut to ListView
- rename doc to entry in a few places
This commit is contained in:
18alantom 2023-03-20 11:10:07 +05:30 committed by Alan
parent 74f4513a7e
commit 8cef2ae60f
5 changed files with 134 additions and 125 deletions

View File

@ -72,7 +72,6 @@ export type SelectOption = { value: string; label: string };
export interface OptionField extends Omit<BaseField, 'fieldtype'> {
fieldtype: OptionFieldType;
options: SelectOption[];
emptyMessage?: string;
allowCustom?: boolean;
}
@ -85,7 +84,6 @@ export interface TargetField extends Omit<BaseField, 'fieldtype'> {
export interface DynamicLinkField extends Omit<BaseField, 'fieldtype'> {
fieldtype: DynamicLinkFieldType;
emptyMessage?: string;
references: string; // Reference to an option field that links to schema
}

View File

@ -21,13 +21,17 @@
{{ t`Loading...` }}
</div>
<div
v-if="!isLoading && dropdownItems.length === 0"
v-else-if="dropdownItems.length === 0"
class="p-2 text-gray-600 italic"
>
{{ getEmptyMessage() }}
</div>
<template v-else>
<div v-for="d in dropdownItems" :key="d.label">
<div
v-for="(d, index) in dropdownItems"
:key="index + d.label"
ref="items"
>
<div
v-if="d.isGroup"
class="
@ -45,7 +49,6 @@
</div>
<a
v-else
ref="items"
class="
block
p-2
@ -55,13 +58,12 @@
cursor-pointer
truncate
"
:class="d.index === highlightedIndex ? 'bg-gray-100' : ''"
@mouseenter="highlightedIndex = d.index"
@mouseleave="highlightedIndex = -1"
:class="index === highlightedIndex ? 'bg-gray-100' : ''"
@mouseenter="highlightedIndex = index"
@mousedown.prevent
@click="selectItem(d)"
>
<component :is="d.component" v-if="d.component" />
<component v-if="d.component" :is="d.component" />
<template v-else>{{ d.label }}</template>
</a>
</div>
@ -71,24 +73,29 @@
</template>
</Popover>
</template>
<script>
import uniq from 'lodash/uniq';
<script lang="ts">
import { Doc } from 'fyo/model/doc';
import { Field } from 'schemas/types';
import { fyo } from 'src/initFyo';
import { nextTick } from 'vue';
import { defineComponent, PropType } from 'vue';
import Popover from './Popover.vue';
export default {
type DropdownItem = {
label: string;
value?: string;
action?: Function;
group?: string;
component?: { template: string };
isGroup?: boolean;
};
export default defineComponent({
name: 'Dropdown',
props: {
items: {
type: Array,
type: Array as PropType<DropdownItem[]>,
default: () => [],
},
groups: {
type: Array,
default: null,
},
right: {
type: Boolean,
default: false,
@ -98,9 +105,11 @@ export default {
default: false,
},
df: {
type: Object as PropType<Field | null>,
default: null,
},
doc: {
type: Object as PropType<Doc | null>,
default: null,
},
},
@ -113,92 +122,53 @@ export default {
highlightedIndex: -1,
};
},
computed: {
sortedGroups() {
if (Array.isArray(this.groups)) {
return this.groups;
}
let groupNames = uniq(
this.items
.map((d) => d.group)
.filter(Boolean)
.sort()
);
if (groupNames.length > 0) {
return groupNames;
}
return null;
watch: {
highlightedIndex() {
this.scrollToHighlighted();
},
dropdownItems() {
if (this.sortedGroups) {
let itemsByGroup = {};
const maxed = Math.max(this.highlightedIndex, -1);
this.highlightedIndex = Math.min(maxed, this.dropdownItems.length - 1);
},
},
computed: {
dropdownItems(): DropdownItem[] {
const groupedItems = getGroupedItems(this.items ?? []);
const groupNames = Object.keys(groupedItems).filter(Boolean).sort();
for (let item of this.items) {
let group = item.group || '';
itemsByGroup[group] = itemsByGroup[group] || [];
itemsByGroup[group].push(item);
}
const items: DropdownItem[] = groupedItems[''] ?? [];
for (let group of groupNames) {
items.push({
label: group,
isGroup: true,
});
let items = [];
let i = 0;
const noGroupItems = itemsByGroup[''];
if (noGroupItems?.length) {
items = items.concat(
noGroupItems.map((d) => {
d.index = i++;
return d;
})
);
}
for (let group of this.sortedGroups) {
let groupItems = itemsByGroup[group];
groupItems = groupItems.map((d) => {
d.index = i++;
return d;
});
items = items.concat(
{
label: group,
isGroup: true,
},
groupItems
);
}
return items;
const grouped = groupedItems[group] ?? [];
items.push(...grouped);
}
return this.items.map((d, i) => {
d.index = i;
return d;
});
return items;
},
},
methods: {
getEmptyMessage() {
if (this.df === null) {
getEmptyMessage(): string {
const { schemaName, fieldname } = this.df ?? {};
if (!schemaName || !fieldname || !this.doc) {
return this.t`Empty`;
}
if (this.df.emptyMessage) {
return this.df.emptyMessage;
}
const { schemaName, fieldname } = this.df;
const emptyMessage = fyo.models[schemaName]?.emptyMessages[fieldname]?.(
this.doc
);
if (emptyMessage === undefined || emptyMessage.length === 0) {
if (!emptyMessage) {
return this.t`Empty`;
}
return emptyMessage;
},
async selectItem(d) {
if (!d?.action) {
async selectItem(d?: DropdownItem): Promise<void> {
if (!d || !d?.action) {
return;
}
@ -208,51 +178,61 @@ export default {
await d.action();
},
toggleDropdown(flag) {
if (flag == null) {
this.isShown = !this.isShown;
} else {
this.isShown = Boolean(flag);
toggleDropdown(flag?: boolean): void {
if (typeof flag !== 'boolean') {
flag = !this.isShown;
}
this.isShown = flag;
},
async selectHighlightedItem() {
if (![-1, this.items.length].includes(this.highlightedIndex)) {
// valid selection
let item = this.items[this.highlightedIndex];
await this.selectItem(item);
} else if (this.items.length === 1) {
await this.selectItem(this.items[0]);
async selectHighlightedItem(): Promise<void> {
let item = this.items[this.highlightedIndex];
if (!item && this.dropdownItems.length === 1) {
item = this.dropdownItems[0];
}
return await this.selectItem(item);
},
highlightItemUp(e) {
highlightItemUp(e?: Event): void {
e?.preventDefault();
this.highlightedIndex -= 1;
if (this.highlightedIndex < 0) {
this.highlightedIndex = 0;
}
nextTick(() => {
this.scrollToHighlighted();
});
this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
},
highlightItemDown(e) {
highlightItemDown(e?: Event): void {
e?.preventDefault();
this.highlightedIndex += 1;
if (this.highlightedIndex >= this.items.length) {
this.highlightedIndex = this.items.length - 1;
this.highlightedIndex = Math.min(
this.dropdownItems.length - 1,
this.highlightedIndex + 1
);
},
scrollToHighlighted(): void {
const elems = this.$refs.items;
if (!Array.isArray(elems)) {
return;
}
nextTick(() => {
this.scrollToHighlighted();
});
},
scrollToHighlighted() {
let highlightedElement = this.$refs.items[this.highlightedIndex];
highlightedElement &&
highlightedElement.scrollIntoView({ block: 'nearest' });
const highlightedElement = elems[this.highlightedIndex];
if (!(highlightedElement instanceof Element)) {
return;
}
highlightedElement.scrollIntoView({ block: 'nearest' });
},
},
};
});
function getGroupedItems(
items: DropdownItem[]
): Record<string, DropdownItem[]> {
const groupedItems: Record<string, DropdownItem[]> = {};
for (let item of items) {
const group = item.group ?? '';
groupedItems[group] ??= [];
groupedItems[group].push(item);
}
return groupedItems;
}
</script>

View File

@ -27,6 +27,7 @@ export default {
emits: ['open', 'close'],
props: {
showPopup: {
type: [Boolean, null],
default: null,
},
right: Boolean,

View File

@ -83,27 +83,38 @@ export default defineComponent({
],
},
{
label: t`Doc`,
description: t`Applicable when a Doc is open in the Form view or Quick Edit view`,
label: t`Entry`,
description: t`Applicable when a entry is open in the Form view or Quick Edit view`,
collapsed: false,
shortcuts: [
{
shortcut: [ShortcutKey.pmod, 'S'],
description: [
t`Save or Submit a doc.`,
t`A doc is submitted only if it is submittable and is in the saved state.`,
t`Save or Submit an entry.`,
t`An entry is submitted only if it is submittable and is in the saved state.`,
].join(' '),
},
{
shortcut: [ShortcutKey.pmod, ShortcutKey.delete],
description: [
t`Cancel or Delete a doc.`,
t`A doc is cancelled only if it is in the submitted state.`,
t`A submittable doc is deleted only if it is in the cancelled state.`,
t`Cancel or Delete an entry.`,
t`An entry is cancelled only if it is in the submitted state.`,
t`A submittable entry is deleted only if it is in the cancelled state.`,
].join(' '),
},
],
},
{
label: t`List View`,
description: t`Applicable when the List View of an entry type is open`,
collapsed: false,
shortcuts: [
{
shortcut: [ShortcutKey.pmod, 'N'],
description: t`Create a new entry of the same type as the List View`,
},
],
},
{
label: t`Quick Search`,
description: t`Applicable when Quick Search is open`,

View File

@ -55,6 +55,7 @@ import {
} from 'src/utils/misc';
import { docsPathRef } from 'src/utils/refs';
import { openQuickEdit, routeTo } from 'src/utils/ui';
import { Shortcuts } from 'src/utils/vueUtils';
import List from './List.vue';
export default {
@ -79,6 +80,7 @@ export default {
listFilters: {},
};
},
inject: { shortcutManager: { from: 'shortcuts' } },
async activated() {
if (typeof this.filters === 'object') {
this.$refs.filterDropdown.setFilter(this.filters, true);
@ -90,9 +92,12 @@ export default {
if (this.fyo.store.isDevelopment) {
window.lv = this;
}
this.shortcuts.pmod.set(['KeyN'], this.makeNewDoc);
},
deactivated() {
docsPathRef.value = '';
this.shortcuts.pmod.delete(['KeyN']);
},
methods: {
updatedData(listFilters) {
@ -113,6 +118,10 @@ export default {
});
},
async makeNewDoc() {
if (!this.canCreate) {
return;
}
const filters = getCreateFiltersFromListViewFilters(this.filters ?? {});
const doc = fyo.doc.getNewDoc(this.schemaName, filters);
const path = this.getFormPath(doc);
@ -165,6 +174,16 @@ export default {
canCreate() {
return fyo.schemaMap[this.schemaName].create !== false;
},
shortcuts() {
// @ts-ignore
const shortcutManager = this.shortcutManager;
if (shortcutManager instanceof Shortcuts) {
return shortcutManager;
}
// no-op (hopefully)
throw Error('Shortcuts Not Found');
},
},
};