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:
parent
74f4513a7e
commit
8cef2ae60f
@ -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
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -27,6 +27,7 @@ export default {
|
||||
emits: ['open', 'close'],
|
||||
props: {
|
||||
showPopup: {
|
||||
type: [Boolean, null],
|
||||
default: null,
|
||||
},
|
||||
right: Boolean,
|
||||
|
@ -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`,
|
||||
|
@ -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');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user