mirror of
https://github.com/frappe/books.git
synced 2026-08-24 02:24:17 -05:00
Merge pull request #1353 from Gadha2311/journal-entry
feat: add Recent Used Options to Quick Search Command Bar
This commit is contained in:
@@ -1271,13 +1271,16 @@ export abstract class Invoice extends Transactional {
|
||||
|
||||
let accountField: AccountFieldEnum = AccountFieldEnum.Account;
|
||||
let paymentType: PaymentTypeEnum = PaymentTypeEnum.Receive;
|
||||
let referenceType: 'SalesInvoice' | 'PurchaseInvoice';
|
||||
|
||||
if (this.isSales && this.isReturn) {
|
||||
accountField = AccountFieldEnum.PaymentAccount;
|
||||
paymentType = PaymentTypeEnum.Pay;
|
||||
}
|
||||
|
||||
if (!this.isSales) {
|
||||
if (this.isSales) {
|
||||
referenceType = 'SalesInvoice';
|
||||
if (this.isReturn) {
|
||||
accountField = AccountFieldEnum.PaymentAccount;
|
||||
paymentType = PaymentTypeEnum.Pay;
|
||||
}
|
||||
} else {
|
||||
referenceType = 'PurchaseInvoice';
|
||||
accountField = AccountFieldEnum.PaymentAccount;
|
||||
paymentType = PaymentTypeEnum.Pay;
|
||||
|
||||
@@ -1296,6 +1299,7 @@ export abstract class Invoice extends Transactional {
|
||||
paymentType,
|
||||
amount: paymentAmount,
|
||||
[accountField]: this.account,
|
||||
referenceType,
|
||||
for: [
|
||||
{
|
||||
referenceType: this.schemaName,
|
||||
|
||||
@@ -252,6 +252,7 @@ import { docsPathMap } from 'src/utils/misc';
|
||||
import {
|
||||
SearchGroup,
|
||||
SearchItems,
|
||||
SearchItem,
|
||||
getGroupLabelMap,
|
||||
searchGroups,
|
||||
} from 'src/utils/search';
|
||||
@@ -317,6 +318,7 @@ export default defineComponent({
|
||||
List: 'teal',
|
||||
Report: 'yellow',
|
||||
Page: 'orange',
|
||||
Recent: 'purple',
|
||||
};
|
||||
},
|
||||
groupColorClassMap(): Record<SearchGroup, string> {
|
||||
@@ -422,7 +424,13 @@ export default defineComponent({
|
||||
},
|
||||
select(idx?: number): void {
|
||||
this.idx = idx ?? this.idx;
|
||||
this.suggestions[this.idx]?.action?.();
|
||||
const selectedItem = this.suggestions[this.idx];
|
||||
|
||||
if (selectedItem?.action) {
|
||||
this.searcher?.addToRecent(selectedItem);
|
||||
selectedItem.action();
|
||||
}
|
||||
|
||||
this.close();
|
||||
},
|
||||
scrollToHighlighted(): void {
|
||||
|
||||
+123
-18
@@ -10,21 +10,19 @@ import { safeParseFloat } from 'utils/index';
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
import { fuzzyMatch } from '.';
|
||||
import { getFormRoute, routeTo } from './ui';
|
||||
import { searchGroups } from '../../utils/types';
|
||||
import type { SearchGroup, SearchItem } from '../../utils/types';
|
||||
|
||||
export const searchGroups = [
|
||||
'Docs',
|
||||
'List',
|
||||
'Create',
|
||||
'Report',
|
||||
'Page',
|
||||
] as const;
|
||||
export { searchGroups };
|
||||
export type { SearchGroup, SearchItem };
|
||||
|
||||
export type SearchGroup = typeof searchGroups[number];
|
||||
interface SearchItem {
|
||||
interface StoredRecentItem {
|
||||
label: string;
|
||||
group: Exclude<SearchGroup, 'Docs'>;
|
||||
group: string;
|
||||
route?: string;
|
||||
action?: () => void | Promise<void>;
|
||||
schemaName?: string;
|
||||
reportName?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface DocSearchItem extends Omit<SearchItem, 'group'> {
|
||||
@@ -33,7 +31,11 @@ interface DocSearchItem extends Omit<SearchItem, 'group'> {
|
||||
more: string[];
|
||||
}
|
||||
|
||||
export type SearchItems = (DocSearchItem | SearchItem)[];
|
||||
interface RecentSearchItem extends Omit<SearchItem, 'group'> {
|
||||
group: 'Recent';
|
||||
}
|
||||
|
||||
export type SearchItems = (DocSearchItem | SearchItem | RecentSearchItem)[];
|
||||
|
||||
interface Searchable {
|
||||
needsUpdate: boolean;
|
||||
@@ -69,6 +71,7 @@ export function getGroupLabelMap() {
|
||||
Report: t`Report`,
|
||||
Docs: t`Docs`,
|
||||
Page: t`Page`,
|
||||
Recent: t`Recent`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,7 +198,7 @@ function getListViewList(fyo: Fyo): SearchItem[] {
|
||||
ModelNameEnum.PrintTemplate,
|
||||
];
|
||||
|
||||
if (fyo.doc.singles.AccountingSecuttings?.enableInventory) {
|
||||
if (fyo.doc.singles.AccountingSettings?.enableInventory) {
|
||||
schemaNames.push(
|
||||
ModelNameEnum.StockMovement,
|
||||
ModelNameEnum.Shipment,
|
||||
@@ -350,6 +353,7 @@ export class Search {
|
||||
|
||||
_obsSet = false;
|
||||
numSearches = 0;
|
||||
recentKey = 'searchRecents';
|
||||
searchables: Record<string, Searchable>;
|
||||
keywords: Record<string, Keyword[]>;
|
||||
priorityMap: Record<string, number> = {
|
||||
@@ -371,6 +375,7 @@ export class Search {
|
||||
Create: true,
|
||||
Page: true,
|
||||
Docs: true,
|
||||
Recent: true,
|
||||
},
|
||||
schemaFilters: {},
|
||||
skipTables: false,
|
||||
@@ -384,6 +389,9 @@ export class Search {
|
||||
_nonDocSearchList: SearchItem[];
|
||||
_groupLabelMap?: Record<SearchGroup, string>;
|
||||
|
||||
maxRecentItems = 10;
|
||||
recentExpiryDays = 30;
|
||||
|
||||
constructor(fyo: Fyo) {
|
||||
this.fyo = fyo;
|
||||
this.keywords = {};
|
||||
@@ -396,6 +404,89 @@ export class Search {
|
||||
* `skipT*` filters and the `schemaFilters`.
|
||||
*/
|
||||
|
||||
private _loadAndCleanRecentItems(): StoredRecentItem[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.recentKey);
|
||||
return raw ? (JSON.parse(raw) as StoredRecentItem[]) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private _saveRecentItems(items: StoredRecentItem[]) {
|
||||
localStorage.setItem(this.recentKey, JSON.stringify(items));
|
||||
}
|
||||
|
||||
addToRecent(item: SearchItems[number]) {
|
||||
const recents = this._loadAndCleanRecentItems();
|
||||
|
||||
const recentItem: StoredRecentItem = {
|
||||
label: item.label,
|
||||
group: item.group,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
if ('route' in item && item.route) {
|
||||
recentItem.route = item.route;
|
||||
} else if (item.group === 'Docs') {
|
||||
recentItem.schemaName = item.schemaLabel;
|
||||
}
|
||||
|
||||
const updatedRecents = [
|
||||
recentItem,
|
||||
...recents.filter((r) => r.label !== recentItem.label),
|
||||
].slice(0, this.maxRecentItems);
|
||||
|
||||
this._saveRecentItems(updatedRecents);
|
||||
}
|
||||
|
||||
getRecentItems(searchTerm?: string): RecentSearchItem[] {
|
||||
try {
|
||||
const recents = this._loadAndCleanRecentItems();
|
||||
|
||||
let filtered = recents;
|
||||
if (searchTerm) {
|
||||
const lower = searchTerm.toLowerCase();
|
||||
filtered = recents.filter(
|
||||
(item) =>
|
||||
item.label.toLowerCase().includes(lower) ||
|
||||
item.group.toLowerCase().includes(lower)
|
||||
);
|
||||
}
|
||||
|
||||
const result = filtered.map((item) => ({
|
||||
label: item.label,
|
||||
group: 'Recent' as const,
|
||||
action: () => this._executeRecentAction(item),
|
||||
route: item.route,
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private _executeRecentAction(item: StoredRecentItem) {
|
||||
if (item.route) {
|
||||
void routeTo(item.route);
|
||||
} else if (item.schemaName) {
|
||||
this._openDocList(item.schemaName);
|
||||
} else if (item.reportName) {
|
||||
this._openReport(item.reportName);
|
||||
}
|
||||
}
|
||||
|
||||
private _openDocList(schemaName: string) {
|
||||
const route = `/list/${schemaName}`;
|
||||
void routeTo(route);
|
||||
}
|
||||
|
||||
private _openReport(reportName: string) {
|
||||
const route = `/report/${reportName}`;
|
||||
void routeTo(route);
|
||||
}
|
||||
|
||||
get skipTables() {
|
||||
let value = true;
|
||||
for (const val of Object.values(this.searchables)) {
|
||||
@@ -500,7 +591,7 @@ export class Search {
|
||||
}
|
||||
|
||||
_searchSuggestions(input: string): SearchItems {
|
||||
const matches: { si: SearchItem | DocSearchItem; distance: number }[] = [];
|
||||
const matches: { si: SearchItems[number]; distance: number }[] = [];
|
||||
|
||||
for (const si of this._intermediate.suggestions) {
|
||||
const label = si.label;
|
||||
@@ -571,9 +662,21 @@ export class Search {
|
||||
|
||||
keys.sort((a, b) => safeParseFloat(b) - safeParseFloat(a));
|
||||
const array: SearchItems = [];
|
||||
|
||||
const showRecent =
|
||||
!input ||
|
||||
input.startsWith('#') ||
|
||||
input.toLowerCase().startsWith('recent');
|
||||
if (showRecent && this.filters.groupFilters.Recent) {
|
||||
const recentSearchTerm = input?.replace(/^#|recent/gi, '').trim();
|
||||
const recentItems = this.getRecentItems(recentSearchTerm);
|
||||
if (recentItems.length > 0) {
|
||||
array.push(...recentItems);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const keywords = groupedKeywords[key] ?? [];
|
||||
|
||||
this._pushDocSearchItems(keywords, array, input);
|
||||
if (key === '0') {
|
||||
this._pushNonDocSearchItems(array, input);
|
||||
@@ -609,8 +712,7 @@ export class Search {
|
||||
items: (SearchItem | Keyword)[],
|
||||
input?: string
|
||||
): SearchItems {
|
||||
const subArray: { item: SearchItem | DocSearchItem; distance: number }[] =
|
||||
[];
|
||||
const subArray: { item: SearchItems[number]; distance: number }[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const subArrayItem = this._getSubArrayItem(item, input);
|
||||
@@ -625,7 +727,10 @@ export class Search {
|
||||
return subArray.map(({ item }) => item);
|
||||
}
|
||||
|
||||
_getSubArrayItem(item: SearchItem | Keyword, input?: string) {
|
||||
_getSubArrayItem(
|
||||
item: SearchItem | Keyword,
|
||||
input?: string
|
||||
): { item: SearchItems[number]; distance: number } | null {
|
||||
if (isSearchItem(item)) {
|
||||
return this._getSubArrayItemFromSearchItem(item, input);
|
||||
}
|
||||
|
||||
@@ -192,6 +192,7 @@ export function getActionsForDoc(doc?: Doc): Action[] {
|
||||
const actions: Action[] = [
|
||||
...getActions(doc),
|
||||
getDuplicateAction(doc),
|
||||
getNewAction(doc),
|
||||
getDeleteAction(doc),
|
||||
getCancelAction(doc),
|
||||
];
|
||||
@@ -290,6 +291,21 @@ function getDuplicateAction(doc: Doc): Action {
|
||||
};
|
||||
}
|
||||
|
||||
function getNewAction(doc: Doc): Action {
|
||||
return {
|
||||
label: t`New Entry`,
|
||||
group: t`Create`,
|
||||
async action() {
|
||||
try {
|
||||
const newDoc = fyo.doc.getNewDoc(doc.schemaName);
|
||||
await openEdit(newDoc);
|
||||
} catch (err) {
|
||||
await handleErrorWithDialog(err as Error, doc);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getFieldsGroupedByTabAndSection(
|
||||
schema: Schema,
|
||||
doc: Doc
|
||||
|
||||
@@ -79,3 +79,21 @@ interface ModMap {
|
||||
export interface ConfigFilesWithModified extends ConfigFile {
|
||||
modified: string;
|
||||
}
|
||||
|
||||
export const searchGroups = [
|
||||
'Docs',
|
||||
'List',
|
||||
'Create',
|
||||
'Report',
|
||||
'Page',
|
||||
'Recent',
|
||||
] as const;
|
||||
|
||||
export type SearchGroup = typeof searchGroups[number];
|
||||
|
||||
export interface SearchItem {
|
||||
label: string;
|
||||
group: Exclude<SearchGroup, 'Docs' | 'Recent'>;
|
||||
route?: string;
|
||||
action?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user