Merge pull request #1353 from Gadha2311/journal-entry

feat: add Recent Used Options to Quick Search Command Bar
This commit is contained in:
Able k Saju
2025-10-13 14:19:03 +05:30
committed by GitHub
5 changed files with 176 additions and 25 deletions
+10 -6
View File
@@ -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,
+9 -1
View File
@@ -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
View File
@@ -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);
}
+16
View File
@@ -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
+18
View File
@@ -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>;
}