From 88765f3a5998ef7f3e34d18d3a1dd183065e3e6f Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Thu, 25 Sep 2025 10:22:56 +0530 Subject: [PATCH 1/2] feat: add Recent Used Options to Quick Search Command Bar --- models/baseModels/Invoice/Invoice.ts | 16 +-- src/components/SearchBar.vue | 9 +- src/utils/search.ts | 147 ++++++++++++++++++++++++++- src/utils/ui.ts | 16 +++ 4 files changed, 177 insertions(+), 11 deletions(-) diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 4f41c163..096bba0b 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -1274,13 +1274,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: this.outstandingAmount?.abs(), [accountField]: this.account, + referenceType, for: [ { referenceType: this.schemaName, diff --git a/src/components/SearchBar.vue b/src/components/SearchBar.vue index 622ea838..7337545c 100644 --- a/src/components/SearchBar.vue +++ b/src/components/SearchBar.vue @@ -317,6 +317,7 @@ export default defineComponent({ List: 'teal', Report: 'yellow', Page: 'orange', + Recent: 'purple', }; }, groupColorClassMap(): Record { @@ -422,7 +423,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 { diff --git a/src/utils/search.ts b/src/utils/search.ts index 4c01369e..9b943781 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -17,23 +17,37 @@ export const searchGroups = [ 'Create', 'Report', 'Page', + 'Recent', ] as const; export type SearchGroup = typeof searchGroups[number]; interface SearchItem { label: string; - group: Exclude; + group: Exclude; route?: string; action?: () => void | Promise; } +interface StoredRecentItem { + label: string; + group: string; + route?: string; + schemaName?: string; + reportName?: string; + timestamp: number; +} + interface DocSearchItem extends Omit { group: 'Docs'; schemaLabel: string; more: string[]; } -export type SearchItems = (DocSearchItem | SearchItem)[]; +interface RecentSearchItem extends Omit { + group: 'Recent'; +} + +export type SearchItems = (DocSearchItem | SearchItem | RecentSearchItem)[]; interface Searchable { needsUpdate: boolean; @@ -69,6 +83,7 @@ export function getGroupLabelMap() { Report: t`Report`, Docs: t`Docs`, Page: t`Page`, + Recent: t`Recent`, }; } @@ -195,7 +210,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 +365,7 @@ export class Search { _obsSet = false; numSearches = 0; + recentKey = 'searchRecents'; searchables: Record; keywords: Record; priorityMap: Record = { @@ -371,6 +387,7 @@ export class Search { Create: true, Page: true, Docs: true, + Recent: true, }, schemaFilters: {}, skipTables: false, @@ -384,6 +401,9 @@ export class Search { _nonDocSearchList: SearchItem[]; _groupLabelMap?: Record; + maxRecentItems = 10; + recentExpiryDays = 30; + constructor(fyo: Fyo) { this.fyo = fyo; this.keywords = {}; @@ -396,6 +416,113 @@ export class Search { * `skipT*` filters and the `schemaFilters`. */ + debugRecentItems() { + const recents = this._loadAndCleanRecentItems(); + return recents; + } + + private _loadAndCleanRecentItems(): StoredRecentItem[] { + try { + const raw = localStorage.getItem(this.recentKey); + if (!raw) { + return []; + } + + const parsed: StoredRecentItem[] = JSON.parse( + raw ?? '[]' + ) as StoredRecentItem[]; + + return parsed; + } catch (error) { + return []; + } + } + + private _saveRecentItems(items: StoredRecentItem[]) { + try { + localStorage.setItem(this.recentKey, JSON.stringify(items)); + } catch (error) {} + } + + addToRecent(item: SearchItem | DocSearchItem) { + try { + 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') { + const docItem = item; + recentItem.schemaName = docItem.schemaLabel; + } + + const updatedRecents = [ + recentItem, + ...recents.filter((r) => r.label !== recentItem.label), + ].slice(0, this.maxRecentItems); + + this._saveRecentItems(updatedRecents); + } catch (error) {} + } + + 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); + } + + clearRecentItems() { + try { + localStorage.removeItem(this.recentKey); + } catch (error) {} + } + get skipTables() { let value = true; for (const val of Object.values(this.searchables)) { @@ -571,9 +698,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); diff --git a/src/utils/ui.ts b/src/utils/ui.ts index a00fffc1..daabff6c 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -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 From 545ee3b7b1d6402f6ca5689928954b72cd2f089b Mon Sep 17 00:00:00 2001 From: Gadha2311 Date: Fri, 10 Oct 2025 12:09:01 +0530 Subject: [PATCH 2/2] fix: export SearchItem and type suggestions to resolve type errors --- src/components/SearchBar.vue | 1 + src/utils/search.ts | 92 ++++++++++++------------------------ utils/types.ts | 18 +++++++ 3 files changed, 48 insertions(+), 63 deletions(-) diff --git a/src/components/SearchBar.vue b/src/components/SearchBar.vue index 7337545c..bebe7334 100644 --- a/src/components/SearchBar.vue +++ b/src/components/SearchBar.vue @@ -252,6 +252,7 @@ import { docsPathMap } from 'src/utils/misc'; import { SearchGroup, SearchItems, + SearchItem, getGroupLabelMap, searchGroups, } from 'src/utils/search'; diff --git a/src/utils/search.ts b/src/utils/search.ts index 9b943781..c9a89784 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -10,23 +10,11 @@ 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', - 'Recent', -] as const; - -export type SearchGroup = typeof searchGroups[number]; -interface SearchItem { - label: string; - group: Exclude; - route?: string; - action?: () => void | Promise; -} +export { searchGroups }; +export type { SearchGroup, SearchItem }; interface StoredRecentItem { label: string; @@ -416,58 +404,40 @@ export class Search { * `skipT*` filters and the `schemaFilters`. */ - debugRecentItems() { - const recents = this._loadAndCleanRecentItems(); - return recents; - } - private _loadAndCleanRecentItems(): StoredRecentItem[] { try { const raw = localStorage.getItem(this.recentKey); - if (!raw) { - return []; - } - - const parsed: StoredRecentItem[] = JSON.parse( - raw ?? '[]' - ) as StoredRecentItem[]; - - return parsed; + return raw ? (JSON.parse(raw) as StoredRecentItem[]) : []; } catch (error) { return []; } } private _saveRecentItems(items: StoredRecentItem[]) { - try { - localStorage.setItem(this.recentKey, JSON.stringify(items)); - } catch (error) {} + localStorage.setItem(this.recentKey, JSON.stringify(items)); } - addToRecent(item: SearchItem | DocSearchItem) { - try { - const recents = this._loadAndCleanRecentItems(); + addToRecent(item: SearchItems[number]) { + const recents = this._loadAndCleanRecentItems(); - const recentItem: StoredRecentItem = { - label: item.label, - group: item.group, - timestamp: Date.now(), - }; + 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') { - const docItem = item; - recentItem.schemaName = docItem.schemaLabel; - } + 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); + const updatedRecents = [ + recentItem, + ...recents.filter((r) => r.label !== recentItem.label), + ].slice(0, this.maxRecentItems); - this._saveRecentItems(updatedRecents); - } catch (error) {} + this._saveRecentItems(updatedRecents); } getRecentItems(searchTerm?: string): RecentSearchItem[] { @@ -517,12 +487,6 @@ export class Search { void routeTo(route); } - clearRecentItems() { - try { - localStorage.removeItem(this.recentKey); - } catch (error) {} - } - get skipTables() { let value = true; for (const val of Object.values(this.searchables)) { @@ -627,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; @@ -748,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); @@ -764,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); } diff --git a/utils/types.ts b/utils/types.ts index c3b4f6dd..34fbde0b 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -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; + route?: string; + action?: () => void | Promise; +}