From 1367f0afca1b832cc45c07f9fd4723d2ca0f6b2a Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 2 Apr 2025 13:55:25 -0400 Subject: [PATCH 01/32] add task to ui --- src/CONST.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/SearchUIUtils.ts | 10 ++++++++++ 4 files changed, 13 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index ad28f97b7af6..4229693e5021 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6356,6 +6356,7 @@ const CONST = { DATA_TYPES: { EXPENSE: 'expense', INVOICE: 'invoice', + TASK: 'task', TRIP: 'trip', CHAT: 'chat', }, diff --git a/src/languages/en.ts b/src/languages/en.ts index 44d635f2ef29..b168ba696cbf 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -511,6 +511,7 @@ const translations = { offlinePrompt: "You can't take this action right now.", outstanding: 'Outstanding', chats: 'Chats', + tasks: 'Tasks', unread: 'Unread', sent: 'Sent', links: 'Links', diff --git a/src/languages/es.ts b/src/languages/es.ts index 93a9133fa18a..52cb4513f96e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -504,6 +504,7 @@ const translations = { offlinePrompt: 'No puedes realizar esta acción ahora mismo.', outstanding: 'Pendiente', chats: 'Chats', + tasks: 'Tereas', unread: 'No leído', sent: 'Enviado', links: 'Enlaces', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 6e81cf1e618f..bcf483314139 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -750,6 +750,16 @@ function createTypeMenuItems(allPolicies: OnyxCollection | nul return ROUTES.SEARCH_ROOT.getRoute({query}); }, }, + { + translationPath: 'common.tasks', + type: CONST.SEARCH.DATA_TYPES.TASK, + icon: Expensicons.Task, + getRoute: (policyID?: string) => { + // JACK_TODO: Change the status + const query = buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.TASK, status: CONST.SEARCH.STATUS.CHAT.ALL, policyID}); + return ROUTES.SEARCH_ROOT.getRoute({query}); + }, + }, ]; if (canSendInvoice(allPolicies, email) || hasInvoiceReports()) { From e472331ce8c3cde843cb936b07f6ba060c9a84ed Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 2 Apr 2025 14:40:50 -0400 Subject: [PATCH 02/32] add more filters and task page --- src/CONST.ts | 8 ++++++ .../SearchPageHeader/SearchStatusBar.tsx | 25 ++++++++++++++++++- src/components/Search/types.ts | 2 ++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/pages/Search/AdvancedSearchFilters.tsx | 4 +++ 6 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 4229693e5021..7234a3859054 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6420,6 +6420,11 @@ const CONST = { LINKS: 'links', PINNED: 'pinned', }, + TASK: { + ALL: 'all', + OUTSTANDING: 'outstanding', + COMPLETED: 'completed', + }, }, TABLE_COLUMNS: { RECEIPT: 'receipt', @@ -6475,6 +6480,9 @@ const CONST = { PAID: 'paid', EXPORTED: 'exported', POSTED: 'posted', + TITLE: 'title', + ASSIGNEE: 'assignee', + CREATED_BY: 'createdBy', }, EMPTY_VALUE: 'none', SEARCH_ROUTER_ITEM_TYPE: { diff --git a/src/components/Search/SearchPageHeader/SearchStatusBar.tsx b/src/components/Search/SearchPageHeader/SearchStatusBar.tsx index 4afeb428a803..9563547cadd5 100644 --- a/src/components/Search/SearchPageHeader/SearchStatusBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchStatusBar.tsx @@ -9,7 +9,7 @@ import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import * as Expensicons from '@components/Icon/Expensicons'; import ScrollView from '@components/ScrollView'; import {useSearchContext} from '@components/Search/SearchContext'; -import type {ChatSearchStatus, ExpenseSearchStatus, InvoiceSearchStatus, SearchQueryJSON, TripSearchStatus} from '@components/Search/types'; +import type {ChatSearchStatus, ExpenseSearchStatus, InvoiceSearchStatus, SearchQueryJSON, TaskSearchStatus, TripSearchStatus} from '@components/Search/types'; import SearchStatusSkeleton from '@components/Skeletons/SearchStatusSkeleton'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -152,6 +152,27 @@ const chatOptions: Array<{type: SearchDataTypes; status: ChatSearchStatus; icon: }, ]; +const taskOptions: Array<{type: SearchDataTypes; status: TaskSearchStatus; icon: IconAsset; text: TranslationPaths}> = [ + { + type: CONST.SEARCH.DATA_TYPES.TASK, + status: CONST.SEARCH.STATUS.TASK.ALL, + icon: Expensicons.All, + text: 'common.all', + }, + { + type: CONST.SEARCH.DATA_TYPES.TASK, + status: CONST.SEARCH.STATUS.TASK.OUTSTANDING, + icon: Expensicons.Hourglass, + text: 'common.outstanding', + }, + { + type: CONST.SEARCH.DATA_TYPES.TASK, + status: CONST.SEARCH.STATUS.TASK.COMPLETED, + icon: Expensicons.Checkbox, + text: 'search.filters.completed', + }, +]; + function getOptions(type: SearchDataTypes) { switch (type) { case CONST.SEARCH.DATA_TYPES.INVOICE: @@ -160,6 +181,8 @@ function getOptions(type: SearchDataTypes) { return tripOptions; case CONST.SEARCH.DATA_TYPES.CHAT: return chatOptions; + case CONST.SEARCH.DATA_TYPES.TASK: + return taskOptions; case CONST.SEARCH.DATA_TYPES.EXPENSE: default: return expenseOptions; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 04151ee4c7e3..be4fe330db77 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -57,6 +57,7 @@ type ExpenseSearchStatus = ValueOf; type InvoiceSearchStatus = ValueOf; type TripSearchStatus = ValueOf; type ChatSearchStatus = ValueOf; +type TaskSearchStatus = ValueOf; type SearchStatus = ExpenseSearchStatus | InvoiceSearchStatus | TripSearchStatus | ChatSearchStatus | Array; type SearchGroupBy = ValueOf; type TableColumnSize = ValueOf; @@ -160,6 +161,7 @@ export type { InvoiceSearchStatus, TripSearchStatus, ChatSearchStatus, + TaskSearchStatus, SearchAutocompleteResult, PaymentData, SearchAutocompleteQueryRange, diff --git a/src/languages/en.ts b/src/languages/en.ts index b168ba696cbf..059c4f3c3c2a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5030,6 +5030,7 @@ const translations = { link: 'Link', pinned: 'Pinned', unread: 'Unread', + completed: 'Completed', amount: { lessThan: ({amount}: OptionalParam = {}) => `Less than ${amount ?? ''}`, greaterThan: ({amount}: OptionalParam = {}) => `Greater than ${amount ?? ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 52cb4513f96e..f6079c4734f8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5083,6 +5083,7 @@ const translations = { link: 'Enlace', pinned: 'Fijado', unread: 'No leído', + completed: 'Completado', card: { expensify: 'Expensify', individualCards: 'Tarjetas individuales', diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 964f8efa76aa..cde9ea05e619 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -211,6 +211,10 @@ const typeFiltersKeys: Record, cards: CardList, translate: LocaleContextProps['translate']) { From 4bd881f5c9beb14223d5962ec67b682d8d007561 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 4 Apr 2025 11:13:40 -0400 Subject: [PATCH 03/32] add new fields title, createdBy, assignee to the search autocomplete, and the advanced search filters --- src/components/Search/index.tsx | 1 + .../SelectionList/Search/TaskListItem.tsx | 94 ++++ .../SelectionList/Search/TaskListItemRow.tsx | 485 ++++++++++++++++++ src/libs/SearchUIUtils.ts | 16 + 4 files changed, 596 insertions(+) create mode 100644 src/components/SelectionList/Search/TaskListItem.tsx create mode 100644 src/components/SelectionList/Search/TaskListItemRow.tsx diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 79f687660be9..9617d70c64d6 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -363,6 +363,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS const ListItem = getListItem(type, status, shouldGroupByReports); const sortedData = getSortedSections(type, status, data, sortBy, sortOrder, shouldGroupByReports); + const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const sortedSelectedData = sortedData.map((item) => { const baseKey = isChat diff --git a/src/components/SelectionList/Search/TaskListItem.tsx b/src/components/SelectionList/Search/TaskListItem.tsx new file mode 100644 index 000000000000..ab577519b141 --- /dev/null +++ b/src/components/SelectionList/Search/TaskListItem.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import {useSearchContext} from '@components/Search/SearchContext'; +import BaseListItem from '@components/SelectionList/BaseListItem'; +import type {ListItem, TransactionListItemProps, TransactionListItemType} from '@components/SelectionList/types'; +import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {handleActionButtonPress} from '@libs/actions/Search'; +import variables from '@styles/variables'; +import TransactionListItemRow from './TransactionListItemRow'; + +function TaskListItem({ + item, + isFocused, + showTooltip, + isDisabled, + canSelectMultiple, + onSelectRow, + onCheckboxPress, + onFocus, + onLongPressRow, + shouldSyncFocus, + isLoading, +}: TransactionListItemProps) { + const transactionItem = item as unknown as TransactionListItemType; + const styles = useThemeStyles(); + const theme = useTheme(); + + const {isLargeScreenWidth} = useResponsiveLayout(); + const {currentSearchHash} = useSearchContext(); + + const listItemPressableStyle = [ + styles.selectionListPressableItemWrapper, + styles.pv3, + styles.ph3, + // Removing background style because they are added to the parent OpacityView via animatedHighlightStyle + styles.bgTransparent, + item.isSelected && styles.activeComponentBG, + styles.mh0, + ]; + + const listItemWrapperStyle = [ + styles.flex1, + styles.userSelectNone, + isLargeScreenWidth ? {...styles.flexRow, ...styles.justifyContentBetween, ...styles.alignItemsCenter} : {...styles.flexColumn, ...styles.alignItemsStretch}, + ]; + + const animatedHighlightStyle = useAnimatedHighlightStyle({ + borderRadius: variables.componentBorderRadius, + shouldHighlight: item?.shouldAnimateInHighlight ?? false, + highlightColor: theme.messageHighlightBG, + backgroundColor: theme.highlightBG, + }); + + return ( + + { + handleActionButtonPress(currentSearchHash, transactionItem, () => onSelectRow(item)); + }} + onCheckboxPress={() => onCheckboxPress?.(item)} + isDisabled={!!isDisabled} + canSelectMultiple={!!canSelectMultiple} + isButtonSelected={item.isSelected} + shouldShowTransactionCheckbox={false} + isLoading={isLoading ?? transactionItem.isActionLoading} + /> + + ); +} + +TaskListItem.displayName = 'TaskListItem'; + +export default TaskListItem; diff --git a/src/components/SelectionList/Search/TaskListItemRow.tsx b/src/components/SelectionList/Search/TaskListItemRow.tsx new file mode 100644 index 000000000000..1b5fec14f37a --- /dev/null +++ b/src/components/SelectionList/Search/TaskListItemRow.tsx @@ -0,0 +1,485 @@ +import {Str} from 'expensify-common'; +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import {getButtonRole} from '@components/Button/utils'; +import Checkbox from '@components/Checkbox'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithFeedback} from '@components/Pressable'; +import ReceiptImage from '@components/ReceiptImage'; +import type {TransactionListItemType} from '@components/SelectionList/types'; +import TextWithTooltip from '@components/TextWithTooltip'; +import Tooltip from '@components/Tooltip'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; +import DateUtils from '@libs/DateUtils'; +import {getFileName} from '@libs/fileDownload/FileUtils'; +import Parser from '@libs/Parser'; +import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; +import StringUtils from '@libs/StringUtils'; +import { + getTagForDisplay, + getTaxAmount, + getCreated as getTransactionCreated, + getCurrency as getTransactionCurrency, + getDescription as getTransactionDescription, + hasReceipt, + isExpensifyCardTransaction, + isPending, + isReceiptBeingScanned, +} from '@libs/TransactionUtils'; +import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type {SearchTransactionType} from '@src/types/onyx/SearchResults'; +import ActionCell from './ActionCell'; +import ExpenseItemHeaderNarrow from './ExpenseItemHeaderNarrow'; +import TextWithIconCell from './TextWithIconCell'; +import UserInfoCell from './UserInfoCell'; + +type CellProps = { + // eslint-disable-next-line react/no-unused-prop-types + showTooltip: boolean; + // eslint-disable-next-line react/no-unused-prop-types + isLargeScreenWidth: boolean; +}; + +type TransactionCellProps = { + transactionItem: TransactionListItemType; +} & CellProps; + +type TotalCellProps = { + // eslint-disable-next-line react/no-unused-prop-types + isChildListItem: boolean; +} & TransactionCellProps; + +type TaskListItemRowProps = { + item: TransactionListItemType; + showTooltip: boolean; + onButtonPress: () => void; + onCheckboxPress: () => void; + showItemHeaderOnNarrowLayout?: boolean; + containerStyle?: StyleProp; + isChildListItem?: boolean; + isDisabled: boolean; + canSelectMultiple: boolean; + isButtonSelected?: boolean; + parentAction?: string; + shouldShowTransactionCheckbox?: boolean; + isLoading?: boolean; +}; + +const getTypeIcon = (type?: SearchTransactionType) => { + switch (type) { + case CONST.SEARCH.TRANSACTION_TYPE.CASH: + return Expensicons.Cash; + case CONST.SEARCH.TRANSACTION_TYPE.CARD: + return Expensicons.CreditCard; + case CONST.SEARCH.TRANSACTION_TYPE.DISTANCE: + return Expensicons.Car; + default: + return Expensicons.Cash; + } +}; + +function ReceiptCell({transactionItem}: TransactionCellProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + + const backgroundStyles = transactionItem.isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border); + + let source = transactionItem?.receipt?.source ?? ''; + if (source && typeof source === 'string') { + const filename = getFileName(source); + const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename); + const isReceiptPDF = Str.isPDF(filename); + source = tryResolveUrlFromApiRoot(isReceiptPDF && !receiptURIs.isLocalFile ? receiptURIs.thumbnail ?? '' : receiptURIs.image ?? ''); + } + + return ( + + + + ); +} + +function DateCell({transactionItem, showTooltip, isLargeScreenWidth}: TransactionCellProps) { + const styles = useThemeStyles(); + + const created = getTransactionCreated(transactionItem); + const date = DateUtils.formatWithUTCTimeZone(created, DateUtils.doesDateBelongToAPastYear(created) ? CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT : CONST.DATE.MONTH_DAY_ABBR_FORMAT); + + return ( + + ); +} + +function MerchantCell({transactionItem, showTooltip, isLargeScreenWidth}: TransactionCellProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const description = getTransactionDescription(transactionItem); + let merchantOrDescriptionToDisplay = transactionItem.formattedMerchant; + if (!merchantOrDescriptionToDisplay && !isLargeScreenWidth) { + merchantOrDescriptionToDisplay = Parser.htmlToText(description); + } + let merchant = transactionItem.shouldShowMerchant ? merchantOrDescriptionToDisplay : Parser.htmlToText(description); + + if (hasReceipt(transactionItem) && isReceiptBeingScanned(transactionItem) && transactionItem.shouldShowMerchant) { + merchant = translate('iou.receiptStatusTitle'); + } + const merchantToDisplay = StringUtils.getFirstLine(merchant); + return ( + + ); +} + +function TotalCell({showTooltip, isLargeScreenWidth, transactionItem}: TotalCellProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const currency = getTransactionCurrency(transactionItem); + let amount = convertToDisplayString(transactionItem.formattedTotal, currency); + + if (hasReceipt(transactionItem) && isReceiptBeingScanned(transactionItem)) { + amount = translate('iou.receiptStatusTitle'); + } + + return ( + + ); +} + +function TypeCell({transactionItem, isLargeScreenWidth}: TransactionCellProps) { + const theme = useTheme(); + const {translate} = useLocalize(); + const isPendingExpensifyCardTransaction = isExpensifyCardTransaction(transactionItem) && isPending(transactionItem); + const typeIcon = isPendingExpensifyCardTransaction ? Expensicons.CreditCardHourglass : getTypeIcon(transactionItem.transactionType); + + const tooltipText = isPendingExpensifyCardTransaction ? translate('iou.pending') : ''; + + return ( + + + + + + ); +} + +function CategoryCell({isLargeScreenWidth, showTooltip, transactionItem}: TransactionCellProps) { + const styles = useThemeStyles(); + return ( + + ); +} + +function TagCell({isLargeScreenWidth, showTooltip, transactionItem}: TransactionCellProps) { + const styles = useThemeStyles(); + return isLargeScreenWidth ? ( + + ) : ( + + ); +} + +function TaxCell({transactionItem, showTooltip}: TransactionCellProps) { + const styles = useThemeStyles(); + + const isFromExpenseReport = transactionItem.reportType === CONST.REPORT.TYPE.EXPENSE; + const taxAmount = getTaxAmount(transactionItem, isFromExpenseReport); + const currency = getTransactionCurrency(transactionItem); + + return ( + + ); +} + +function TaskListItemRow({ + item, + showTooltip, + isDisabled, + canSelectMultiple, + onButtonPress, + onCheckboxPress, + showItemHeaderOnNarrowLayout = true, + containerStyle, + isChildListItem = false, + isButtonSelected = false, + parentAction = '', + shouldShowTransactionCheckbox, + isLoading = false, +}: TaskListItemRowProps) { + const styles = useThemeStyles(); + const {isLargeScreenWidth} = useResponsiveLayout(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); + + if (!isLargeScreenWidth) { + return ( + + {showItemHeaderOnNarrowLayout && ( + + )} + + + {canSelectMultiple && !!shouldShowTransactionCheckbox && ( + + + {!!item.isSelected && ( + + )} + + + )} + + + + {!!item.category && ( + + + + )} + + + + + + + + + + + ); + } + + return ( + + {canSelectMultiple && ( + + )} + + + + + + + + + + + + + + + + + + + + {item.shouldShowCategory && ( + + + + )} + {item.shouldShowTag && ( + + + + )} + {item.shouldShowTax && ( + + + + )} + + + + + + + + + + ); +} + +TaskListItemRow.displayName = 'TaskListItemRow'; + +export default TaskListItemRow; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index bcf483314139..c8bb0434eb2c 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -6,6 +6,7 @@ import type {MenuItemWithLink} from '@components/MenuItemList'; import type {SearchColumnType, SearchStatus, SortOrder} from '@components/Search/types'; import ChatListItem from '@components/SelectionList/ChatListItem'; import ReportListItem from '@components/SelectionList/Search/ReportListItem'; +import TaskListItem from '@components/SelectionList/Search/TaskListItem'; import TransactionListItem from '@components/SelectionList/Search/TransactionListItem'; import type {ListItem, ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import * as Expensicons from '@src/components/Icon/Expensicons'; @@ -399,6 +400,14 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr return CONST.SEARCH.ACTION_TYPES.VIEW; } +/** + * @private + * Organizes data into List Sections for display, for the ReportActionListItemType of Search Results. + * + * Do not use directly, use only via `getSections()` facade. + */ +function getTaskSections(data: OnyxTypes.SearchResults['data']): ReportActionListItemType[] {} + /** * @private * Organizes data into List Sections for display, for the ReportActionListItemType of Search Results. @@ -529,6 +538,9 @@ function getListItem(type: SearchDataTypes, status: SearchStatus, shouldGroupByR if (type === CONST.SEARCH.DATA_TYPES.CHAT) { return ChatListItem; } + if (type === CONST.SEARCH.DATA_TYPES.TASK) { + return TaskListItem; + } if (!shouldGroupByReports) { return TransactionListItem; } @@ -542,9 +554,13 @@ function getSections(type: SearchDataTypes, status: SearchStatus, data: OnyxType if (type === CONST.SEARCH.DATA_TYPES.CHAT) { return getReportActionsSections(data); } + if (type === CONST.SEARCH.DATA_TYPES.TASK) { + return getReportActionsSections(data); + } if (!shouldGroupByReports) { return getTransactionsSections(data, metadata); } + return getReportSections(data, metadata); } From 25848cda840cac65b24fb3e63bde2d9e444e5be3 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 4 Apr 2025 11:13:56 -0400 Subject: [PATCH 04/32] Add createdBy, title, assignee to the autocomplete and advanced search filters --- src/CONST.ts | 3 + src/ROUTES.ts | 3 + src/SCREENS.ts | 3 + .../Search/SearchAutocompleteList.tsx | 24 ++ src/languages/en.ts | 3 + src/languages/es.ts | 3 + .../ModalStackNavigators/index.tsx | 3 + .../linkingConfig/RELATIONS/SEARCH_TO_RHP.ts | 3 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/SearchAutocompleteUtils.ts | 2 + src/libs/SearchParser/autocompleteParser.js | 335 +++++++++++------- .../SearchParser/autocompleteParser.peggy | 2 + src/libs/SearchParser/baseRules.peggy | 3 + src/libs/SearchParser/searchParser.js | 326 ++++++++++------- src/libs/SearchParser/searchParser.peggy | 3 + src/libs/SearchQueryUtils.ts | 16 +- src/pages/Search/AdvancedSearchFilters.tsx | 35 +- .../SearchFiltersAssigneePage.tsx | 50 +++ .../SearchFiltersCreatedByPage.tsx | 50 +++ .../SearchFiltersTitlePage.tsx | 85 +++++ src/types/form/SearchAdvancedFiltersForm.ts | 6 + 21 files changed, 700 insertions(+), 261 deletions(-) create mode 100644 src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAssigneePage.tsx create mode 100644 src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCreatedByPage.tsx create mode 100644 src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTitlePage.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 7234a3859054..f25a0d0d6b0a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6518,6 +6518,9 @@ const CONST = { PAID: 'paid', EXPORTED: 'exported', POSTED: 'posted', + TITLE: 'title', + ASSIGNEE: 'assignee', + CREATED_BY: 'created-by', }, DATE_MODIFIERS: { BEFORE: 'Before', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 92c9a3650b5a..f018f397a469 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -68,6 +68,9 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_PAID: 'search/filters/paid', SEARCH_ADVANCED_FILTERS_EXPORTED: 'search/filters/exported', SEARCH_ADVANCED_FILTERS_POSTED: 'search/filters/posted', + SEARCH_ADVANCED_FILTERS_TITLE: 'search/filters/title', + SEARCH_ADVANCED_FILTERS_ASSIGNEE: 'search/filters/assignee', + SEARCH_ADVANCED_FILTERS_CREATED_BY: 'search/filters/createdBy', SEARCH_REPORT: { route: 'search/view/:reportID/:reportActionID?', getRoute: ({ diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 01a83f520507..f81692bb22f4 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -60,6 +60,9 @@ const SCREENS = { ADVANCED_FILTERS_TAG_RHP: 'Search_Advanced_Filters_Tag_RHP', ADVANCED_FILTERS_FROM_RHP: 'Search_Advanced_Filters_From_RHP', ADVANCED_FILTERS_TO_RHP: 'Search_Advanced_Filters_To_RHP', + ADVANCED_FILTERS_TITLE_RHP: 'Search_Advanced_Filters_Title_RHP', + ADVANCED_FILTERS_ASSIGNEE_RHP: 'Search_Advanced_Filters_Assignee_RHP', + ADVANCED_FILTERS_CREATED_BY_RHP: 'Search_Advanced_Filters_Created_By_RHP', SAVED_SEARCH_RENAME_RHP: 'Search_Saved_Search_Rename_RHP', ADVANCED_FILTERS_IN_RHP: 'Search_Advanced_Filters_In_RHP', TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index d0047ee30964..24e83bbeceba 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -321,6 +321,30 @@ function SearchAutocompleteList( mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.TO, })); } + case CONST.SEARCH.SYNTAX_FILTER_KEYS.ASSIGNEE: { + const filteredParticipants = getParticipantsAutocompleteList() + .filter((participant) => participant.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(participant.name.toLowerCase())) + .slice(0, 10); + + return filteredParticipants.map((participant) => ({ + filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.ASSIGNEE, + text: participant.name, + autocompleteID: participant.accountID, + mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.ASSIGNEE, + })); + } + case CONST.SEARCH.SYNTAX_FILTER_KEYS.CREATED_BY: { + const filteredParticipants = getParticipantsAutocompleteList() + .filter((participant) => participant.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(participant.name.toLowerCase())) + .slice(0, 10); + + return filteredParticipants.map((participant) => ({ + filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.CREATED_BY, + text: participant.name, + autocompleteID: participant.accountID, + mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.CREATED_BY, + })); + } case CONST.SEARCH.SYNTAX_FILTER_KEYS.IN: { const filteredChats = searchOptions.recentReports .filter((chat) => chat.text?.toLowerCase()?.includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(chat.text.toLowerCase())) diff --git a/src/languages/en.ts b/src/languages/en.ts index 059c4f3c3c2a..ea46af850072 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -427,6 +427,9 @@ const translations = { websiteExample: 'e.g. https://www.expensify.com', zipCodeExampleFormat: ({zipSampleFormat}: ZipCodeExampleFormatParams) => (zipSampleFormat ? `e.g. ${zipSampleFormat}` : ''), description: 'Description', + title: 'Title', + assignee: 'Assignee', + createdBy: 'Created by', with: 'with', shareCode: 'Share code', share: 'Share', diff --git a/src/languages/es.ts b/src/languages/es.ts index f6079c4734f8..e4b579214162 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -420,6 +420,9 @@ const translations = { websiteExample: 'p. ej. https://www.expensify.com', zipCodeExampleFormat: ({zipSampleFormat}: ZipCodeExampleFormatParams) => (zipSampleFormat ? `p. ej. ${zipSampleFormat}` : ''), description: 'Descripción', + title: 'Título', + assignee: 'Asignado a', + createdBy: 'Creado por', with: 'con', shareCode: 'Compartir código', share: 'Compartir', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8e4dd26974f4..70c174fd0fa2 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -713,6 +713,9 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersFromPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersToPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersInPage').default, + [SCREENS.SEARCH.ADVANCED_FILTERS_TITLE_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersTitlePage').default, + [SCREENS.SEARCH.ADVANCED_FILTERS_ASSIGNEE_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersAssigneePage').default, + [SCREENS.SEARCH.ADVANCED_FILTERS_CREATED_BY_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersCreatedByPage').default, }); const SearchSavedSearchModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts index feee223c233c..320c6f7b5dad 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts @@ -25,6 +25,9 @@ const SEARCH_TO_RHP: string[] = [ SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_TITLE_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_ASSIGNEE_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_CREATED_BY_RHP, SCREENS.SEARCH.SAVED_SEARCH_RENAME_RHP, ]; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 4c2ec248eb81..74848ca8f1c0 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1414,6 +1414,9 @@ const config: LinkingOptions['config'] = { [SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_FROM, [SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_TO, [SCREENS.SEARCH.ADVANCED_FILTERS_IN_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_IN, + [SCREENS.SEARCH.ADVANCED_FILTERS_TITLE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_TITLE, + [SCREENS.SEARCH.ADVANCED_FILTERS_ASSIGNEE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_ASSIGNEE, + [SCREENS.SEARCH.ADVANCED_FILTERS_CREATED_BY_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CREATED_BY, }, }, [SCREENS.RIGHT_MODAL.SEARCH_SAVED_SEARCH]: { diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index 0a1db8b3554c..2c98cc5e535f 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -162,6 +162,8 @@ function filterOutRangesWithCorrectValue( case CONST.SEARCH.SYNTAX_FILTER_KEYS.TO: case CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM: + case CONST.SEARCH.SYNTAX_FILTER_KEYS.ASSIGNEE: + case CONST.SEARCH.SYNTAX_FILTER_KEYS.CREATED_BY: return substitutionMap[`${range.key}:${range.value}`] !== undefined || userLogins.get().includes(range.value); case CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY: diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index 8746845f7446..cf0d3d623f9d 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -218,14 +218,17 @@ function peg$parse(input, options) { var peg$c32 = "groupby"; var peg$c33 = "group-by"; var peg$c34 = "feed"; - var peg$c35 = "!="; - var peg$c36 = ">="; - var peg$c37 = ">"; - var peg$c38 = "<="; - var peg$c39 = "<"; - var peg$c40 = "\u201C"; - var peg$c41 = "\u201D"; - var peg$c42 = "\""; + var peg$c35 = "title"; + var peg$c36 = "assignee"; + var peg$c37 = "createdby"; + var peg$c38 = "!="; + var peg$c39 = ">="; + var peg$c40 = ">"; + var peg$c41 = "<="; + var peg$c42 = "<"; + var peg$c43 = "\u201C"; + var peg$c44 = "\u201D"; + var peg$c45 = "\""; var peg$r0 = /^[:=]/; var peg$r1 = /^[^ ,\t\n\r\xA0]/; @@ -275,30 +278,33 @@ function peg$parse(input, options) { var peg$e33 = peg$literalExpectation("groupBy", true); var peg$e34 = peg$literalExpectation("group-by", true); var peg$e35 = peg$literalExpectation("feed", true); - var peg$e36 = peg$otherExpectation("operator"); - var peg$e37 = peg$classExpectation([":", "="], false, false); - var peg$e38 = peg$literalExpectation("!=", false); - var peg$e39 = peg$literalExpectation(">=", false); - var peg$e40 = peg$literalExpectation(">", false); - var peg$e41 = peg$literalExpectation("<=", false); - var peg$e42 = peg$literalExpectation("<", false); - var peg$e43 = peg$otherExpectation("word"); - var peg$e44 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e45 = peg$otherExpectation("whitespace"); - var peg$e46 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); - var peg$e47 = peg$otherExpectation("quote"); - var peg$e48 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e49 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); - var peg$e50 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); - var peg$e51 = peg$literalExpectation("\u201C", false); - var peg$e52 = peg$literalExpectation("\u201D", false); - var peg$e53 = peg$literalExpectation("\"", false); - var peg$e54 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e55 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e56 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); - var peg$e57 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); - var peg$e58 = peg$anyExpectation(); - var peg$e59 = peg$classExpectation([","], false, false); + var peg$e36 = peg$literalExpectation("title", true); + var peg$e37 = peg$literalExpectation("assignee", true); + var peg$e38 = peg$literalExpectation("createdBy", true); + var peg$e39 = peg$otherExpectation("operator"); + var peg$e40 = peg$classExpectation([":", "="], false, false); + var peg$e41 = peg$literalExpectation("!=", false); + var peg$e42 = peg$literalExpectation(">=", false); + var peg$e43 = peg$literalExpectation(">", false); + var peg$e44 = peg$literalExpectation("<=", false); + var peg$e45 = peg$literalExpectation("<", false); + var peg$e46 = peg$otherExpectation("word"); + var peg$e47 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e48 = peg$otherExpectation("whitespace"); + var peg$e49 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); + var peg$e50 = peg$otherExpectation("quote"); + var peg$e51 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e52 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); + var peg$e53 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); + var peg$e54 = peg$literalExpectation("\u201C", false); + var peg$e55 = peg$literalExpectation("\u201D", false); + var peg$e56 = peg$literalExpectation("\"", false); + var peg$e57 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e58 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e59 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); + var peg$e60 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); + var peg$e61 = peg$anyExpectation(); + var peg$e62 = peg$classExpectation([","], false, false); var peg$f0 = function(ranges) { return { autocomplete, ranges }; }; var peg$f1 = function(filters) { return filters.filter(Boolean).flat(); }; @@ -380,28 +386,31 @@ function peg$parse(input, options) { var peg$f30 = function() { return "posted"; }; var peg$f31 = function() { return "groupBy"; }; var peg$f32 = function() { return "feed"; }; - var peg$f33 = function() { return "eq"; }; - var peg$f34 = function() { return "neq"; }; - var peg$f35 = function() { return "gte"; }; - var peg$f36 = function() { return "gt"; }; - var peg$f37 = function() { return "lte"; }; - var peg$f38 = function() { return "lt"; }; - var peg$f39 = function(o) { + var peg$f33 = function() { return "title"; }; + var peg$f34 = function() { return "assignee"; }; + var peg$f35 = function() { return "createdBy"; }; + var peg$f36 = function() { return "eq"; }; + var peg$f37 = function() { return "neq"; }; + var peg$f38 = function() { return "gte"; }; + var peg$f39 = function() { return "gt"; }; + var peg$f40 = function() { return "lte"; }; + var peg$f41 = function() { return "lt"; }; + var peg$f42 = function(o) { if (nameOperator) { expectingNestedQuote = (o === "eq"); // Use simple parser if no valid operator is found } return o; }; - var peg$f40 = function(chars) { return chars.join("").trim(); }; - var peg$f41 = function() { return "and"; }; - var peg$f42 = function() { return expectingNestedQuote; }; - var peg$f43 = function(start, inner, end) { //handle no-breaking space + var peg$f43 = function(chars) { return chars.join("").trim(); }; + var peg$f44 = function() { return "and"; }; + var peg$f45 = function() { return expectingNestedQuote; }; + var peg$f46 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; - var peg$f44 = function(start) {return "“"}; - var peg$f45 = function(start) {return "”"}; - var peg$f46 = function(start) {return "\""}; - var peg$f47 = function(start, inner, end) { + var peg$f47 = function(start) {return "“"}; + var peg$f48 = function(start) {return "”"}; + var peg$f49 = function(start) {return "\""}; + var peg$f50 = function(start, inner, end) { return [...start, '"', ...inner, '"'].join(""); }; var peg$currPos = options.peg$currPos | 0; @@ -697,17 +706,23 @@ function peg$parse(input, options) { if (s1 === peg$FAILED) { s1 = peg$parsefrom(); if (s1 === peg$FAILED) { - s1 = peg$parseexpenseType(); + s1 = peg$parsecreatedBy(); if (s1 === peg$FAILED) { - s1 = peg$parsetype(); + s1 = peg$parseassignee(); if (s1 === peg$FAILED) { - s1 = peg$parsestatus(); + s1 = peg$parseexpenseType(); if (s1 === peg$FAILED) { - s1 = peg$parsecardID(); + s1 = peg$parsetype(); if (s1 === peg$FAILED) { - s1 = peg$parsefeed(); + s1 = peg$parsestatus(); if (s1 === peg$FAILED) { - s1 = peg$parsegroupBy(); + s1 = peg$parsecardID(); + if (s1 === peg$FAILED) { + s1 = peg$parsefeed(); + if (s1 === peg$FAILED) { + s1 = peg$parsegroupBy(); + } + } } } } @@ -1414,6 +1429,66 @@ function peg$parse(input, options) { return s0; } + function peg$parsetitle() { + var s0, s1; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 5); + if (s1.toLowerCase() === peg$c35) { + peg$currPos += 5; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e36); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f33(); + } + s0 = s1; + + return s0; + } + + function peg$parseassignee() { + var s0, s1; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 8); + if (s1.toLowerCase() === peg$c36) { + peg$currPos += 8; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e37); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f34(); + } + s0 = s1; + + return s0; + } + + function peg$parsecreatedBy() { + var s0, s1; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 9); + if (s1.toLowerCase() === peg$c37) { + peg$currPos += 9; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e38); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f35(); + } + s0 = s1; + + return s0; + } + function peg$parseoperator() { var s0, s1; @@ -1424,81 +1499,81 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e37); } + if (peg$silentFails === 0) { peg$fail(peg$e40); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f33(); + s1 = peg$f36(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c35) { - s1 = peg$c35; + if (input.substr(peg$currPos, 2) === peg$c38) { + s1 = peg$c38; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e38); } + if (peg$silentFails === 0) { peg$fail(peg$e41); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f34(); + s1 = peg$f37(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c36) { - s1 = peg$c36; + if (input.substr(peg$currPos, 2) === peg$c39) { + s1 = peg$c39; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e39); } + if (peg$silentFails === 0) { peg$fail(peg$e42); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f35(); + s1 = peg$f38(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 62) { - s1 = peg$c37; + s1 = peg$c40; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e40); } + if (peg$silentFails === 0) { peg$fail(peg$e43); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f36(); + s1 = peg$f39(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c38) { - s1 = peg$c38; + if (input.substr(peg$currPos, 2) === peg$c41) { + s1 = peg$c41; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e41); } + if (peg$silentFails === 0) { peg$fail(peg$e44); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f37(); + s1 = peg$f40(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 60) { - s1 = peg$c39; + s1 = peg$c42; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e42); } + if (peg$silentFails === 0) { peg$fail(peg$e45); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f38(); + s1 = peg$f41(); } s0 = s1; } @@ -1509,7 +1584,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e36); } + if (peg$silentFails === 0) { peg$fail(peg$e39); } } return s0; @@ -1522,7 +1597,7 @@ function peg$parse(input, options) { s1 = peg$parseoperator(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f39(s1); + s1 = peg$f42(s1); } s0 = s1; @@ -1540,7 +1615,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e44); } + if (peg$silentFails === 0) { peg$fail(peg$e47); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -1550,7 +1625,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e44); } + if (peg$silentFails === 0) { peg$fail(peg$e47); } } } } else { @@ -1558,13 +1633,13 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f40(s1); + s1 = peg$f43(s1); } s0 = s1; peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e43); } + if (peg$silentFails === 0) { peg$fail(peg$e46); } } return s0; @@ -1576,7 +1651,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f41(); + s1 = peg$f44(); s0 = s1; return s0; @@ -1592,7 +1667,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e46); } + if (peg$silentFails === 0) { peg$fail(peg$e49); } } while (s1 !== peg$FAILED) { s0.push(s1); @@ -1601,12 +1676,12 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e46); } + if (peg$silentFails === 0) { peg$fail(peg$e49); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e45); } + if (peg$silentFails === 0) { peg$fail(peg$e48); } return s0; } @@ -1616,7 +1691,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f42(); + s1 = peg$f45(); if (s1) { s1 = undefined; } else { @@ -1653,7 +1728,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e48); } + if (peg$silentFails === 0) { peg$fail(peg$e51); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -1662,7 +1737,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e48); } + if (peg$silentFails === 0) { peg$fail(peg$e51); } } } s2 = input.charAt(peg$currPos); @@ -1670,7 +1745,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e49); } + if (peg$silentFails === 0) { peg$fail(peg$e52); } } if (s2 !== peg$FAILED) { s3 = []; @@ -1679,7 +1754,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e50); } + if (peg$silentFails === 0) { peg$fail(peg$e53); } } while (s4 !== peg$FAILED) { s3.push(s4); @@ -1688,7 +1763,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e50); } + if (peg$silentFails === 0) { peg$fail(peg$e53); } } } s4 = input.charAt(peg$currPos); @@ -1696,7 +1771,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e49); } + if (peg$silentFails === 0) { peg$fail(peg$e52); } } if (s4 !== peg$FAILED) { s5 = []; @@ -1705,7 +1780,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e44); } + if (peg$silentFails === 0) { peg$fail(peg$e47); } } while (s6 !== peg$FAILED) { s5.push(s6); @@ -1714,11 +1789,11 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e44); } + if (peg$silentFails === 0) { peg$fail(peg$e47); } } } peg$savedPos = s0; - s0 = peg$f43(s1, s3, s5); + s0 = peg$f46(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1730,7 +1805,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e47); } + if (peg$silentFails === 0) { peg$fail(peg$e50); } } return s0; @@ -1747,7 +1822,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e48); } + if (peg$silentFails === 0) { peg$fail(peg$e51); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -1756,7 +1831,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e48); } + if (peg$silentFails === 0) { peg$fail(peg$e51); } } } s2 = input.charAt(peg$currPos); @@ -1764,7 +1839,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e49); } + if (peg$silentFails === 0) { peg$fail(peg$e52); } } if (s2 !== peg$FAILED) { s3 = []; @@ -1773,7 +1848,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e50); } + if (peg$silentFails === 0) { peg$fail(peg$e53); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -1789,15 +1864,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c40; + s6 = peg$c43; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e51); } + if (peg$silentFails === 0) { peg$fail(peg$e54); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f44(s1); + s4 = peg$f47(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -1820,15 +1895,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8221) { - s6 = peg$c41; + s6 = peg$c44; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e52); } + if (peg$silentFails === 0) { peg$fail(peg$e55); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f45(s1); + s4 = peg$f48(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -1851,15 +1926,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 34) { - s6 = peg$c42; + s6 = peg$c45; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e53); } + if (peg$silentFails === 0) { peg$fail(peg$e56); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f46(s1); + s4 = peg$f49(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -1878,7 +1953,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e50); } + if (peg$silentFails === 0) { peg$fail(peg$e53); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -1894,15 +1969,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c40; + s6 = peg$c43; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e51); } + if (peg$silentFails === 0) { peg$fail(peg$e54); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f44(s1); + s4 = peg$f47(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -1925,15 +2000,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8221) { - s6 = peg$c41; + s6 = peg$c44; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e52); } + if (peg$silentFails === 0) { peg$fail(peg$e55); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f45(s1); + s4 = peg$f48(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -1956,15 +2031,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 34) { - s6 = peg$c42; + s6 = peg$c45; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e53); } + if (peg$silentFails === 0) { peg$fail(peg$e56); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f46(s1); + s4 = peg$f49(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -1980,7 +2055,7 @@ function peg$parse(input, options) { s4 = peg$parseclosingQuote(); if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f47(s1, s3, s4); + s0 = peg$f50(s1, s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1992,7 +2067,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e47); } + if (peg$silentFails === 0) { peg$fail(peg$e50); } } return s0; @@ -2007,7 +2082,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e49); } + if (peg$silentFails === 0) { peg$fail(peg$e52); } } if (s1 !== peg$FAILED) { s2 = peg$currPos; @@ -2045,7 +2120,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e54); } + if (peg$silentFails === 0) { peg$fail(peg$e57); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -2054,7 +2129,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e54); } + if (peg$silentFails === 0) { peg$fail(peg$e57); } } } s2 = []; @@ -2063,7 +2138,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e55); } + if (peg$silentFails === 0) { peg$fail(peg$e58); } } while (s3 !== peg$FAILED) { s2.push(s3); @@ -2072,7 +2147,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e55); } + if (peg$silentFails === 0) { peg$fail(peg$e58); } } } s3 = []; @@ -2081,7 +2156,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e56); } + if (peg$silentFails === 0) { peg$fail(peg$e59); } } while (s4 !== peg$FAILED) { s3.push(s4); @@ -2090,7 +2165,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e56); } + if (peg$silentFails === 0) { peg$fail(peg$e59); } } } s4 = peg$parseoperator(); @@ -2109,7 +2184,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e57); } + if (peg$silentFails === 0) { peg$fail(peg$e60); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -2118,7 +2193,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e57); } + if (peg$silentFails === 0) { peg$fail(peg$e60); } } } s2 = peg$currPos; @@ -2139,7 +2214,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e58); } + if (peg$silentFails === 0) { peg$fail(peg$e61); } } peg$silentFails--; if (s4 === peg$FAILED) { @@ -2165,7 +2240,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e59); } + if (peg$silentFails === 0) { peg$fail(peg$e62); } } } } diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index 19c877b7a2e5..64075828882f 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -64,6 +64,8 @@ autocompleteKey "key" / to / taxRate / from + / createdBy + / assignee / expenseType / type / status diff --git a/src/libs/SearchParser/baseRules.peggy b/src/libs/SearchParser/baseRules.peggy index 2eef436919dd..48078a778be0 100644 --- a/src/libs/SearchParser/baseRules.peggy +++ b/src/libs/SearchParser/baseRules.peggy @@ -54,6 +54,9 @@ exported = "exported"i { return "exported"; } posted = "posted"i { return "posted"; } groupBy = "groupBy"i / "group-by"i { return "groupBy"; } feed = "feed"i { return "feed"; } +title = "title"i { return "title"; } +assignee = "assignee"i { return "assignee"; } +createdBy = "createdBy"i { return "createdBy"; } operator "operator" = (":" / "=") { return "eq"; } diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 56ee86d609ea..3e88119d1d28 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -219,14 +219,17 @@ function peg$parse(input, options) { var peg$c32 = "groupby"; var peg$c33 = "group-by"; var peg$c34 = "feed"; - var peg$c35 = "!="; - var peg$c36 = ">="; - var peg$c37 = ">"; - var peg$c38 = "<="; - var peg$c39 = "<"; - var peg$c40 = "\u201C"; - var peg$c41 = "\u201D"; - var peg$c42 = "\""; + var peg$c35 = "title"; + var peg$c36 = "assignee"; + var peg$c37 = "createdby"; + var peg$c38 = "!="; + var peg$c39 = ">="; + var peg$c40 = ">"; + var peg$c41 = "<="; + var peg$c42 = "<"; + var peg$c43 = "\u201C"; + var peg$c44 = "\u201D"; + var peg$c45 = "\""; var peg$r0 = /^[^ \t\r\n\xA0]/; var peg$r1 = /^[:=]/; @@ -279,30 +282,33 @@ function peg$parse(input, options) { var peg$e35 = peg$literalExpectation("groupBy", true); var peg$e36 = peg$literalExpectation("group-by", true); var peg$e37 = peg$literalExpectation("feed", true); - var peg$e38 = peg$otherExpectation("operator"); - var peg$e39 = peg$classExpectation([":", "="], false, false); - var peg$e40 = peg$literalExpectation("!=", false); - var peg$e41 = peg$literalExpectation(">=", false); - var peg$e42 = peg$literalExpectation(">", false); - var peg$e43 = peg$literalExpectation("<=", false); - var peg$e44 = peg$literalExpectation("<", false); - var peg$e45 = peg$otherExpectation("word"); - var peg$e46 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e47 = peg$otherExpectation("whitespace"); - var peg$e48 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); - var peg$e49 = peg$otherExpectation("quote"); - var peg$e50 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e51 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); - var peg$e52 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); - var peg$e53 = peg$literalExpectation("\u201C", false); - var peg$e54 = peg$literalExpectation("\u201D", false); - var peg$e55 = peg$literalExpectation("\"", false); - var peg$e56 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e57 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e58 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); - var peg$e59 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); - var peg$e60 = peg$anyExpectation(); - var peg$e61 = peg$classExpectation([","], false, false); + var peg$e38 = peg$literalExpectation("title", true); + var peg$e39 = peg$literalExpectation("assignee", true); + var peg$e40 = peg$literalExpectation("createdBy", true); + var peg$e41 = peg$otherExpectation("operator"); + var peg$e42 = peg$classExpectation([":", "="], false, false); + var peg$e43 = peg$literalExpectation("!=", false); + var peg$e44 = peg$literalExpectation(">=", false); + var peg$e45 = peg$literalExpectation(">", false); + var peg$e46 = peg$literalExpectation("<=", false); + var peg$e47 = peg$literalExpectation("<", false); + var peg$e48 = peg$otherExpectation("word"); + var peg$e49 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e50 = peg$otherExpectation("whitespace"); + var peg$e51 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); + var peg$e52 = peg$otherExpectation("quote"); + var peg$e53 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e54 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); + var peg$e55 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); + var peg$e56 = peg$literalExpectation("\u201C", false); + var peg$e57 = peg$literalExpectation("\u201D", false); + var peg$e58 = peg$literalExpectation("\"", false); + var peg$e59 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e60 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e61 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); + var peg$e62 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); + var peg$e63 = peg$anyExpectation(); + var peg$e64 = peg$classExpectation([","], false, false); var peg$f0 = function(filters) { return applyDefaults(filters); }; var peg$f1 = function(head, tail) { @@ -396,28 +402,31 @@ function peg$parse(input, options) { var peg$f31 = function() { return "posted"; }; var peg$f32 = function() { return "groupBy"; }; var peg$f33 = function() { return "feed"; }; - var peg$f34 = function() { return "eq"; }; - var peg$f35 = function() { return "neq"; }; - var peg$f36 = function() { return "gte"; }; - var peg$f37 = function() { return "gt"; }; - var peg$f38 = function() { return "lte"; }; - var peg$f39 = function() { return "lt"; }; - var peg$f40 = function(o) { + var peg$f34 = function() { return "title"; }; + var peg$f35 = function() { return "assignee"; }; + var peg$f36 = function() { return "createdBy"; }; + var peg$f37 = function() { return "eq"; }; + var peg$f38 = function() { return "neq"; }; + var peg$f39 = function() { return "gte"; }; + var peg$f40 = function() { return "gt"; }; + var peg$f41 = function() { return "lte"; }; + var peg$f42 = function() { return "lt"; }; + var peg$f43 = function(o) { if (nameOperator) { expectingNestedQuote = (o === "eq"); // Use simple parser if no valid operator is found } return o; }; - var peg$f41 = function(chars) { return chars.join("").trim(); }; - var peg$f42 = function() { return "and"; }; - var peg$f43 = function() { return expectingNestedQuote; }; - var peg$f44 = function(start, inner, end) { //handle no-breaking space + var peg$f44 = function(chars) { return chars.join("").trim(); }; + var peg$f45 = function() { return "and"; }; + var peg$f46 = function() { return expectingNestedQuote; }; + var peg$f47 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; - var peg$f45 = function(start) {return "“"}; - var peg$f46 = function(start) {return "”"}; - var peg$f47 = function(start) {return "\""}; - var peg$f48 = function(start, inner, end) { + var peg$f48 = function(start) {return "“"}; + var peg$f49 = function(start) {return "”"}; + var peg$f50 = function(start) {return "\""}; + var peg$f51 = function(start, inner, end) { return [...start, '"', ...inner, '"'].join(""); }; var peg$currPos = options.peg$currPos | 0; @@ -809,6 +818,15 @@ function peg$parse(input, options) { s1 = peg$parseposted(); if (s1 === peg$FAILED) { s1 = peg$parsefeed(); + if (s1 === peg$FAILED) { + s1 = peg$parsetitle(); + if (s1 === peg$FAILED) { + s1 = peg$parseassignee(); + if (s1 === peg$FAILED) { + s1 = peg$parsecreatedBy(); + } + } + } } } } @@ -1614,6 +1632,66 @@ function peg$parse(input, options) { return s0; } + function peg$parsetitle() { + var s0, s1; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 5); + if (s1.toLowerCase() === peg$c35) { + peg$currPos += 5; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e38); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f34(); + } + s0 = s1; + + return s0; + } + + function peg$parseassignee() { + var s0, s1; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 8); + if (s1.toLowerCase() === peg$c36) { + peg$currPos += 8; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e39); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f35(); + } + s0 = s1; + + return s0; + } + + function peg$parsecreatedBy() { + var s0, s1; + + s0 = peg$currPos; + s1 = input.substr(peg$currPos, 9); + if (s1.toLowerCase() === peg$c37) { + peg$currPos += 9; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e40); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f36(); + } + s0 = s1; + + return s0; + } + function peg$parseoperator() { var s0, s1; @@ -1624,81 +1702,81 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e39); } + if (peg$silentFails === 0) { peg$fail(peg$e42); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f34(); + s1 = peg$f37(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c35) { - s1 = peg$c35; + if (input.substr(peg$currPos, 2) === peg$c38) { + s1 = peg$c38; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e40); } + if (peg$silentFails === 0) { peg$fail(peg$e43); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f35(); + s1 = peg$f38(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c36) { - s1 = peg$c36; + if (input.substr(peg$currPos, 2) === peg$c39) { + s1 = peg$c39; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e41); } + if (peg$silentFails === 0) { peg$fail(peg$e44); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f36(); + s1 = peg$f39(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 62) { - s1 = peg$c37; + s1 = peg$c40; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e42); } + if (peg$silentFails === 0) { peg$fail(peg$e45); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f37(); + s1 = peg$f40(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c38) { - s1 = peg$c38; + if (input.substr(peg$currPos, 2) === peg$c41) { + s1 = peg$c41; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e43); } + if (peg$silentFails === 0) { peg$fail(peg$e46); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f38(); + s1 = peg$f41(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 60) { - s1 = peg$c39; + s1 = peg$c42; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e44); } + if (peg$silentFails === 0) { peg$fail(peg$e47); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f39(); + s1 = peg$f42(); } s0 = s1; } @@ -1709,7 +1787,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e38); } + if (peg$silentFails === 0) { peg$fail(peg$e41); } } return s0; @@ -1722,7 +1800,7 @@ function peg$parse(input, options) { s1 = peg$parseoperator(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f40(s1); + s1 = peg$f43(s1); } s0 = s1; @@ -1740,7 +1818,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e46); } + if (peg$silentFails === 0) { peg$fail(peg$e49); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -1750,7 +1828,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e46); } + if (peg$silentFails === 0) { peg$fail(peg$e49); } } } } else { @@ -1758,13 +1836,13 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f41(s1); + s1 = peg$f44(s1); } s0 = s1; peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e45); } + if (peg$silentFails === 0) { peg$fail(peg$e48); } } return s0; @@ -1776,7 +1854,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f42(); + s1 = peg$f45(); s0 = s1; return s0; @@ -1792,7 +1870,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e48); } + if (peg$silentFails === 0) { peg$fail(peg$e51); } } while (s1 !== peg$FAILED) { s0.push(s1); @@ -1801,12 +1879,12 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e48); } + if (peg$silentFails === 0) { peg$fail(peg$e51); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e47); } + if (peg$silentFails === 0) { peg$fail(peg$e50); } return s0; } @@ -1816,7 +1894,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f43(); + s1 = peg$f46(); if (s1) { s1 = undefined; } else { @@ -1853,7 +1931,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e50); } + if (peg$silentFails === 0) { peg$fail(peg$e53); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -1862,7 +1940,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e50); } + if (peg$silentFails === 0) { peg$fail(peg$e53); } } } s2 = input.charAt(peg$currPos); @@ -1870,7 +1948,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e51); } + if (peg$silentFails === 0) { peg$fail(peg$e54); } } if (s2 !== peg$FAILED) { s3 = []; @@ -1879,7 +1957,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e52); } + if (peg$silentFails === 0) { peg$fail(peg$e55); } } while (s4 !== peg$FAILED) { s3.push(s4); @@ -1888,7 +1966,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e52); } + if (peg$silentFails === 0) { peg$fail(peg$e55); } } } s4 = input.charAt(peg$currPos); @@ -1896,7 +1974,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e51); } + if (peg$silentFails === 0) { peg$fail(peg$e54); } } if (s4 !== peg$FAILED) { s5 = []; @@ -1905,7 +1983,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e46); } + if (peg$silentFails === 0) { peg$fail(peg$e49); } } while (s6 !== peg$FAILED) { s5.push(s6); @@ -1914,11 +1992,11 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e46); } + if (peg$silentFails === 0) { peg$fail(peg$e49); } } } peg$savedPos = s0; - s0 = peg$f44(s1, s3, s5); + s0 = peg$f47(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1930,7 +2008,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e49); } + if (peg$silentFails === 0) { peg$fail(peg$e52); } } return s0; @@ -1947,7 +2025,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e50); } + if (peg$silentFails === 0) { peg$fail(peg$e53); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -1956,7 +2034,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e50); } + if (peg$silentFails === 0) { peg$fail(peg$e53); } } } s2 = input.charAt(peg$currPos); @@ -1964,7 +2042,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e51); } + if (peg$silentFails === 0) { peg$fail(peg$e54); } } if (s2 !== peg$FAILED) { s3 = []; @@ -1973,7 +2051,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e52); } + if (peg$silentFails === 0) { peg$fail(peg$e55); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -1989,15 +2067,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c40; + s6 = peg$c43; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e53); } + if (peg$silentFails === 0) { peg$fail(peg$e56); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f45(s1); + s4 = peg$f48(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -2020,15 +2098,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8221) { - s6 = peg$c41; + s6 = peg$c44; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e54); } + if (peg$silentFails === 0) { peg$fail(peg$e57); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f46(s1); + s4 = peg$f49(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -2051,15 +2129,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 34) { - s6 = peg$c42; + s6 = peg$c45; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e55); } + if (peg$silentFails === 0) { peg$fail(peg$e58); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f47(s1); + s4 = peg$f50(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -2078,7 +2156,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e52); } + if (peg$silentFails === 0) { peg$fail(peg$e55); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -2094,15 +2172,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c40; + s6 = peg$c43; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e53); } + if (peg$silentFails === 0) { peg$fail(peg$e56); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f45(s1); + s4 = peg$f48(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -2125,15 +2203,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 8221) { - s6 = peg$c41; + s6 = peg$c44; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e54); } + if (peg$silentFails === 0) { peg$fail(peg$e57); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f46(s1); + s4 = peg$f49(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -2156,15 +2234,15 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { if (input.charCodeAt(peg$currPos) === 34) { - s6 = peg$c42; + s6 = peg$c45; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e55); } + if (peg$silentFails === 0) { peg$fail(peg$e58); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f47(s1); + s4 = peg$f50(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -2180,7 +2258,7 @@ function peg$parse(input, options) { s4 = peg$parseclosingQuote(); if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f48(s1, s3, s4); + s0 = peg$f51(s1, s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2192,7 +2270,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e49); } + if (peg$silentFails === 0) { peg$fail(peg$e52); } } return s0; @@ -2207,7 +2285,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e51); } + if (peg$silentFails === 0) { peg$fail(peg$e54); } } if (s1 !== peg$FAILED) { s2 = peg$currPos; @@ -2245,7 +2323,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e56); } + if (peg$silentFails === 0) { peg$fail(peg$e59); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -2254,7 +2332,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e56); } + if (peg$silentFails === 0) { peg$fail(peg$e59); } } } s2 = []; @@ -2263,7 +2341,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e57); } + if (peg$silentFails === 0) { peg$fail(peg$e60); } } while (s3 !== peg$FAILED) { s2.push(s3); @@ -2272,7 +2350,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e57); } + if (peg$silentFails === 0) { peg$fail(peg$e60); } } } s3 = []; @@ -2281,7 +2359,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e58); } + if (peg$silentFails === 0) { peg$fail(peg$e61); } } while (s4 !== peg$FAILED) { s3.push(s4); @@ -2290,7 +2368,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e58); } + if (peg$silentFails === 0) { peg$fail(peg$e61); } } } s4 = peg$parseoperator(); @@ -2309,7 +2387,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e59); } + if (peg$silentFails === 0) { peg$fail(peg$e62); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -2318,7 +2396,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e59); } + if (peg$silentFails === 0) { peg$fail(peg$e62); } } } s2 = peg$currPos; @@ -2339,7 +2417,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e60); } + if (peg$silentFails === 0) { peg$fail(peg$e63); } } peg$silentFails--; if (s4 === peg$FAILED) { @@ -2365,7 +2443,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e61); } + if (peg$silentFails === 0) { peg$fail(peg$e64); } } } } diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index c95674338336..9c4df995129f 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -130,6 +130,9 @@ key "key" / exported / posted / feed + / title + / assignee + / createdBy ) filterKey diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 571d54428e5d..6a50d9d297d4 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -67,6 +67,9 @@ const UserFriendlyKeyMap: Record { - if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID) && filterValue) { + if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID || filterKey === FILTER_KEYS.TITLE) && filterValue) { const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as FilterKeys[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey); if (keyInCorrectForm) { return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${sanitizeSearchValue(filterValue as string)}`; @@ -372,7 +375,9 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial 0 ) { @@ -549,7 +554,12 @@ function getFilterDisplayValue( cardList: OnyxTypes.CardList, cardFeedNamesWithType: CardFeedNamesWithType, ) { - if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) { + if ( + filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || + filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || + filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.ASSIGNEE || + filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CREATED_BY + ) { // login can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return personalDetails?.[filterValue]?.displayName || filterValue; diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index cde9ea05e619..217888f3740e 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -136,6 +136,21 @@ const baseFilterConfig = { description: 'common.in' as const, route: ROUTES.SEARCH_ADVANCED_FILTERS_IN, }, + title: { + getTitle: getFilterDisplayTitle, + description: 'common.title' as const, + route: ROUTES.SEARCH_ADVANCED_FILTERS_TITLE, + }, + assignee: { + getTitle: getFilterParticipantDisplayTitle, + description: 'common.assignee' as const, + route: ROUTES.SEARCH_ADVANCED_FILTERS_ASSIGNEE, + }, + createdBy: { + getTitle: getFilterParticipantDisplayTitle, + description: 'common.createdBy' as const, + route: ROUTES.SEARCH_ADVANCED_FILTERS_CREATED_BY, + }, }; /** @@ -212,7 +227,13 @@ const typeFiltersKeys: Record, filt .join(', '); } - if (nonDateFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION) { + if (nonDateFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION || nonDateFilterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TITLE) { return filters[nonDateFilterKey]; } @@ -497,7 +518,8 @@ function AdvancedSearchFilters() { key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION || key === CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT || key === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID || - key === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD + key === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD || + key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TITLE ) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, key, translate); } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY) { @@ -527,7 +549,12 @@ function AdvancedSearchFilters() { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, taxRates); } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, translate); - } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) { + } else if ( + key === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || + key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || + key === CONST.SEARCH.SYNTAX_FILTER_KEYS.ASSIGNEE || + key === CONST.SEARCH.SYNTAX_FILTER_KEYS.CREATED_BY + ) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters[key] ?? [], personalDetails); } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, translate, reports); diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAssigneePage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAssigneePage.tsx new file mode 100644 index 000000000000..0568daa3526e --- /dev/null +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAssigneePage.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SearchFiltersParticipantsSelector from '@components/Search/SearchFiltersParticipantsSelector'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as SearchActions from '@userActions/Search'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +function SearchFiltersAssigneePage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + + return ( + + { + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }} + /> + + { + SearchActions.updateAdvancedFilters({ + assignee: selectedAccountIDs, + }); + }} + /> + + + ); +} + +SearchFiltersAssigneePage.displayName = 'SearchFiltersAssigneePage'; + +export default SearchFiltersAssigneePage; diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCreatedByPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCreatedByPage.tsx new file mode 100644 index 000000000000..3cb66185c959 --- /dev/null +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCreatedByPage.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SearchFiltersParticipantsSelector from '@components/Search/SearchFiltersParticipantsSelector'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as SearchActions from '@userActions/Search'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +function SearchFiltersCreatedByPage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + + return ( + + { + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }} + /> + + { + SearchActions.updateAdvancedFilters({ + createdBy: selectedAccountIDs, + }); + }} + /> + + + ); +} + +SearchFiltersCreatedByPage.displayName = 'SearchFiltersCreatedByPage'; + +export default SearchFiltersCreatedByPage; diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTitlePage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTitlePage.tsx new file mode 100644 index 000000000000..cb622c39f00e --- /dev/null +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersTitlePage.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {updateAdvancedFilters} from '@libs/actions/Search'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; + +function SearchFiltersTitlePage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + const title = searchAdvancedFiltersForm?.[FILTER_KEYS.TITLE]; + + const updateTitleFilter = (values: FormOnyxValues) => { + updateAdvancedFilters(values); + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }; + + const validate = (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + const titleValue = values.title.trim(); + + if (titleValue.length > CONST.TASK_TITLE_CHARACTER_LIMIT) { + errors.title = translate('common.error.characterLimitExceedCounter', {length: titleValue.length, limit: CONST.TASK_TITLE_CHARACTER_LIMIT}); + } + + return errors; + }; + + return ( + + { + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }} + /> + + + + + + + ); +} + +SearchFiltersTitlePage.displayName = 'SearchFiltersTitlePage'; + +export default SearchFiltersTitlePage; diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts index 395cf66cf4c1..d9d849d8834d 100644 --- a/src/types/form/SearchAdvancedFiltersForm.ts +++ b/src/types/form/SearchAdvancedFiltersForm.ts @@ -44,6 +44,9 @@ const FILTER_KEYS = { FROM: 'from', TO: 'to', IN: 'in', + TITLE: 'title', + ASSIGNEE: 'assignee', + CREATED_BY: 'createdBy', } as const; type InputID = ValueOf; @@ -82,6 +85,9 @@ type SearchAdvancedFiltersForm = Form< [FILTER_KEYS.FROM]: string[]; [FILTER_KEYS.TO]: string[]; [FILTER_KEYS.IN]: string[]; + [FILTER_KEYS.TITLE]: string; + [FILTER_KEYS.ASSIGNEE]: string[]; + [FILTER_KEYS.CREATED_BY]: string[]; } >; From eb30e53ad909594b81e976d526344d5b11ee9aa0 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 4 Apr 2025 11:28:13 -0400 Subject: [PATCH 05/32] fix autocomplete --- src/components/Search/SearchAutocompleteList.tsx | 8 +++++++- src/components/Search/types.ts | 8 +++++++- src/libs/SearchAutocompleteUtils.ts | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 24e83bbeceba..4ee50cca7613 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -159,7 +159,13 @@ function SearchAutocompleteList( const typeAutocompleteList = Object.values(CONST.SEARCH.DATA_TYPES); const groupByAutocompleteList = Object.values(CONST.SEARCH.GROUP_BY); - const statusAutocompleteList = Object.values({...CONST.SEARCH.STATUS.EXPENSE, ...CONST.SEARCH.STATUS.INVOICE, ...CONST.SEARCH.STATUS.CHAT, ...CONST.SEARCH.STATUS.TRIP}); + const statusAutocompleteList = Object.values({ + ...CONST.SEARCH.STATUS.EXPENSE, + ...CONST.SEARCH.STATUS.INVOICE, + ...CONST.SEARCH.STATUS.CHAT, + ...CONST.SEARCH.STATUS.TRIP, + ...CONST.SEARCH.STATUS.TASK, + }); const expenseTypes = Object.values(CONST.SEARCH.TRANSACTION_TYPE); const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST); diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index be4fe330db77..8435680b251b 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -58,7 +58,13 @@ type InvoiceSearchStatus = ValueOf; type TripSearchStatus = ValueOf; type ChatSearchStatus = ValueOf; type TaskSearchStatus = ValueOf; -type SearchStatus = ExpenseSearchStatus | InvoiceSearchStatus | TripSearchStatus | ChatSearchStatus | Array; +type SearchStatus = + | ExpenseSearchStatus + | InvoiceSearchStatus + | TripSearchStatus + | ChatSearchStatus + | TaskSearchStatus + | Array; type SearchGroupBy = ValueOf; type TableColumnSize = ValueOf; diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index 2c98cc5e535f..cc68b36e0ff0 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -150,7 +150,13 @@ function filterOutRangesWithCorrectValue( const typeList = Object.values(CONST.SEARCH.DATA_TYPES) as string[]; const expenseTypeList = Object.values(CONST.SEARCH.TRANSACTION_TYPE) as string[]; - const statusList = Object.values({...CONST.SEARCH.STATUS.EXPENSE, ...CONST.SEARCH.STATUS.INVOICE, ...CONST.SEARCH.STATUS.CHAT, ...CONST.SEARCH.STATUS.TRIP}) as string[]; + const statusList = Object.values({ + ...CONST.SEARCH.STATUS.EXPENSE, + ...CONST.SEARCH.STATUS.INVOICE, + ...CONST.SEARCH.STATUS.CHAT, + ...CONST.SEARCH.STATUS.TRIP, + ...CONST.SEARCH.STATUS.TASK, + }) as string[]; const groupByList = Object.values(CONST.SEARCH.GROUP_BY) as string[]; switch (range.key) { From b056fd5e0d3cbf047dafa7c62990c81fd418b12d Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 4 Apr 2025 14:00:04 -0400 Subject: [PATCH 06/32] begin task list item --- src/components/Search/SearchContext.tsx | 30 +- src/components/Search/index.tsx | 8 + src/components/Search/types.ts | 4 +- .../SelectionList/Search/TaskListItem.tsx | 34 +- .../SelectionList/Search/TaskListItemRow.tsx | 522 +++--------------- src/components/SelectionList/types.ts | 27 +- src/libs/SearchUIUtils.ts | 43 +- src/types/onyx/SearchResults.ts | 29 +- 8 files changed, 208 insertions(+), 489 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 98165296b604..d2c3a3da6811 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useContext, useMemo, useState} from 'react'; -import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import type {ReportActionListItemType, ReportListItemType, TaskListItemType, TransactionListItemType} from '@components/SelectionList/types'; import {isMoneyRequestReport} from '@libs/ReportUtils'; import {isReportListItemType, isTransactionListItemType} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -22,7 +22,10 @@ const defaultSearchContext = { const Context = React.createContext(defaultSearchContext); -function getReportsFromSelectedTransactions(data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[], selectedTransactions: SelectedTransactions) { +function getReportsFromSelectedTransactions( + data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[] | TaskListItemType[], + selectedTransactions: SelectedTransactions, +) { if (data.length === 0) { return []; } @@ -70,17 +73,20 @@ function SearchContextProvider({children}: ChildrenProps) { })); }, []); - const setSelectedTransactions = useCallback((selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => { - // When selecting transactions, we also need to manage the reports to which these transactions belong. This is done to ensure proper exporting to CSV. - const selectedReports = getReportsFromSelectedTransactions(data, selectedTransactions); + const setSelectedTransactions = useCallback( + (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[] | TaskListItemType[]) => { + // When selecting transactions, we also need to manage the reports to which these transactions belong. This is done to ensure proper exporting to CSV. + const selectedReports = getReportsFromSelectedTransactions(data, selectedTransactions); - setSearchContextData((prevState) => ({ - ...prevState, - selectedTransactions, - shouldTurnOffSelectionMode: false, - selectedReports, - })); - }, []); + setSearchContextData((prevState) => ({ + ...prevState, + selectedTransactions, + shouldTurnOffSelectionMode: false, + selectedReports, + })); + }, + [], + ); const clearSelectedTransactions = useCallback( (searchHash?: number, shouldTurnOffSelectionMode = false) => { diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 9617d70c64d6..2a867cbc6dba 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -29,6 +29,7 @@ import { isReportActionListItemType, isReportListItemType, isSearchResultsEmpty as isSearchResultsEmptyUtil, + isTaskListItemType, isTransactionListItemType, shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, @@ -80,6 +81,13 @@ function mapToItemWithSelectionInfo( canSelectMultiple: boolean, shouldAnimateInHighlight: boolean, ) { + if (isTaskListItemType(item)) { + return { + ...item, + shouldAnimateInHighlight, + }; + } + if (isReportActionListItemType(item)) { return { ...item, diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 8435680b251b..1d31a796d2d0 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -1,5 +1,5 @@ import type {ValueOf} from 'type-fest'; -import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import type {ReportActionListItemType, ReportListItemType, TaskListItemType, TransactionListItemType} from '@components/SelectionList/types'; import type CONST from '@src/CONST'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; @@ -73,7 +73,7 @@ type SearchContext = { selectedTransactions: SelectedTransactions; selectedReports: SelectedReports[]; setCurrentSearchHash: (hash: number) => void; - setSelectedTransactions: (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => void; + setSelectedTransactions: (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[] | TaskListItemType[]) => void; clearSelectedTransactions: (hash?: number, shouldTurnOffSelectionMode?: boolean) => void; shouldTurnOffSelectionMode: boolean; shouldShowStatusBarLoading: boolean; diff --git a/src/components/SelectionList/Search/TaskListItem.tsx b/src/components/SelectionList/Search/TaskListItem.tsx index ab577519b141..84f4d379a623 100644 --- a/src/components/SelectionList/Search/TaskListItem.tsx +++ b/src/components/SelectionList/Search/TaskListItem.tsx @@ -1,14 +1,12 @@ import React from 'react'; -import {useSearchContext} from '@components/Search/SearchContext'; import BaseListItem from '@components/SelectionList/BaseListItem'; -import type {ListItem, TransactionListItemProps, TransactionListItemType} from '@components/SelectionList/types'; +import type {ListItem, TaskListItemProps, TaskListItemType} from '@components/SelectionList/types'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {handleActionButtonPress} from '@libs/actions/Search'; import variables from '@styles/variables'; -import TransactionListItemRow from './TransactionListItemRow'; +import TaskListItemRow from './TaskListItemRow'; function TaskListItem({ item, @@ -17,18 +15,15 @@ function TaskListItem({ isDisabled, canSelectMultiple, onSelectRow, - onCheckboxPress, onFocus, onLongPressRow, shouldSyncFocus, - isLoading, -}: TransactionListItemProps) { - const transactionItem = item as unknown as TransactionListItemType; +}: TaskListItemProps) { + const taskItem = item as unknown as TaskListItemType; const styles = useThemeStyles(); const theme = useTheme(); const {isLargeScreenWidth} = useResponsiveLayout(); - const {currentSearchHash} = useSearchContext(); const listItemPressableStyle = [ styles.selectionListPressableItemWrapper, @@ -72,18 +67,15 @@ function TaskListItem({ hoverStyle={item.isSelected && styles.activeComponentBG} pressableWrapperStyle={[styles.mh5, animatedHighlightStyle]} > - { - handleActionButtonPress(currentSearchHash, transactionItem, () => onSelectRow(item)); - }} - onCheckboxPress={() => onCheckboxPress?.(item)} - isDisabled={!!isDisabled} - canSelectMultiple={!!canSelectMultiple} - isButtonSelected={item.isSelected} - shouldShowTransactionCheckbox={false} - isLoading={isLoading ?? transactionItem.isActionLoading} + { + // // handleActionButtonPress(currentSearchHash, taskItem, () => onSelectRow(item)); + // }} + // isDisabled={!!isDisabled} + // isButtonSelected={item.isSelected} + // isLoading={isLoading ?? taskItem.isActionLoading} /> ); diff --git a/src/components/SelectionList/Search/TaskListItemRow.tsx b/src/components/SelectionList/Search/TaskListItemRow.tsx index 1b5fec14f37a..9033151092cd 100644 --- a/src/components/SelectionList/Search/TaskListItemRow.tsx +++ b/src/components/SelectionList/Search/TaskListItemRow.tsx @@ -1,478 +1,106 @@ -import {Str} from 'expensify-common'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import {getButtonRole} from '@components/Button/utils'; -import Checkbox from '@components/Checkbox'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import {PressableWithFeedback} from '@components/Pressable'; -import ReceiptImage from '@components/ReceiptImage'; -import type {TransactionListItemType} from '@components/SelectionList/types'; -import TextWithTooltip from '@components/TextWithTooltip'; -import Tooltip from '@components/Tooltip'; -import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import type {TaskListItemType} from '@components/SelectionList/types'; import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {convertToDisplayString} from '@libs/CurrencyUtils'; -import DateUtils from '@libs/DateUtils'; -import {getFileName} from '@libs/fileDownload/FileUtils'; -import Parser from '@libs/Parser'; -import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; -import StringUtils from '@libs/StringUtils'; -import { - getTagForDisplay, - getTaxAmount, - getCreated as getTransactionCreated, - getCurrency as getTransactionCurrency, - getDescription as getTransactionDescription, - hasReceipt, - isExpensifyCardTransaction, - isPending, - isReceiptBeingScanned, -} from '@libs/TransactionUtils'; -import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {SearchTransactionType} from '@src/types/onyx/SearchResults'; -import ActionCell from './ActionCell'; -import ExpenseItemHeaderNarrow from './ExpenseItemHeaderNarrow'; -import TextWithIconCell from './TextWithIconCell'; import UserInfoCell from './UserInfoCell'; -type CellProps = { - // eslint-disable-next-line react/no-unused-prop-types - showTooltip: boolean; - // eslint-disable-next-line react/no-unused-prop-types - isLargeScreenWidth: boolean; -}; - -type TransactionCellProps = { - transactionItem: TransactionListItemType; -} & CellProps; - -type TotalCellProps = { - // eslint-disable-next-line react/no-unused-prop-types - isChildListItem: boolean; -} & TransactionCellProps; - type TaskListItemRowProps = { - item: TransactionListItemType; - showTooltip: boolean; - onButtonPress: () => void; - onCheckboxPress: () => void; - showItemHeaderOnNarrowLayout?: boolean; + item: TaskListItemType; containerStyle?: StyleProp; - isChildListItem?: boolean; - isDisabled: boolean; - canSelectMultiple: boolean; - isButtonSelected?: boolean; - parentAction?: string; - shouldShowTransactionCheckbox?: boolean; - isLoading?: boolean; -}; - -const getTypeIcon = (type?: SearchTransactionType) => { - switch (type) { - case CONST.SEARCH.TRANSACTION_TYPE.CASH: - return Expensicons.Cash; - case CONST.SEARCH.TRANSACTION_TYPE.CARD: - return Expensicons.CreditCard; - case CONST.SEARCH.TRANSACTION_TYPE.DISTANCE: - return Expensicons.Car; - default: - return Expensicons.Cash; - } }; -function ReceiptCell({transactionItem}: TransactionCellProps) { - const theme = useTheme(); +function TaskListItemRow({item, containerStyle}: TaskListItemRowProps) { const styles = useThemeStyles(); + // const {isLargeScreenWidth} = useResponsiveLayout(); const StyleUtils = useStyleUtils(); - - const backgroundStyles = transactionItem.isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border); - - let source = transactionItem?.receipt?.source ?? ''; - if (source && typeof source === 'string') { - const filename = getFileName(source); - const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename); - const isReceiptPDF = Str.isPDF(filename); - source = tryResolveUrlFromApiRoot(isReceiptPDF && !receiptURIs.isLocalFile ? receiptURIs.thumbnail ?? '' : receiptURIs.image ?? ''); - } - - return ( - - - - ); -} - -function DateCell({transactionItem, showTooltip, isLargeScreenWidth}: TransactionCellProps) { - const styles = useThemeStyles(); - - const created = getTransactionCreated(transactionItem); - const date = DateUtils.formatWithUTCTimeZone(created, DateUtils.doesDateBelongToAPastYear(created) ? CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT : CONST.DATE.MONTH_DAY_ABBR_FORMAT); - - return ( - - ); -} - -function MerchantCell({transactionItem, showTooltip, isLargeScreenWidth}: TransactionCellProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const description = getTransactionDescription(transactionItem); - let merchantOrDescriptionToDisplay = transactionItem.formattedMerchant; - if (!merchantOrDescriptionToDisplay && !isLargeScreenWidth) { - merchantOrDescriptionToDisplay = Parser.htmlToText(description); - } - let merchant = transactionItem.shouldShowMerchant ? merchantOrDescriptionToDisplay : Parser.htmlToText(description); - - if (hasReceipt(transactionItem) && isReceiptBeingScanned(transactionItem) && transactionItem.shouldShowMerchant) { - merchant = translate('iou.receiptStatusTitle'); - } - const merchantToDisplay = StringUtils.getFirstLine(merchant); - return ( - - ); -} - -function TotalCell({showTooltip, isLargeScreenWidth, transactionItem}: TotalCellProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const currency = getTransactionCurrency(transactionItem); - let amount = convertToDisplayString(transactionItem.formattedTotal, currency); - - if (hasReceipt(transactionItem) && isReceiptBeingScanned(transactionItem)) { - amount = translate('iou.receiptStatusTitle'); - } - - return ( - - ); -} - -function TypeCell({transactionItem, isLargeScreenWidth}: TransactionCellProps) { - const theme = useTheme(); - const {translate} = useLocalize(); - const isPendingExpensifyCardTransaction = isExpensifyCardTransaction(transactionItem) && isPending(transactionItem); - const typeIcon = isPendingExpensifyCardTransaction ? Expensicons.CreditCardHourglass : getTypeIcon(transactionItem.transactionType); - - const tooltipText = isPendingExpensifyCardTransaction ? translate('iou.pending') : ''; - - return ( - - - - - - ); -} - -function CategoryCell({isLargeScreenWidth, showTooltip, transactionItem}: TransactionCellProps) { - const styles = useThemeStyles(); - return ( - - ); -} - -function TagCell({isLargeScreenWidth, showTooltip, transactionItem}: TransactionCellProps) { - const styles = useThemeStyles(); - return isLargeScreenWidth ? ( - - ) : ( - - ); -} - -function TaxCell({transactionItem, showTooltip}: TransactionCellProps) { - const styles = useThemeStyles(); - - const isFromExpenseReport = transactionItem.reportType === CONST.REPORT.TYPE.EXPENSE; - const taxAmount = getTaxAmount(transactionItem, isFromExpenseReport); - const currency = getTransactionCurrency(transactionItem); - - return ( - - ); -} - -function TaskListItemRow({ - item, - showTooltip, - isDisabled, - canSelectMultiple, - onButtonPress, - onCheckboxPress, - showItemHeaderOnNarrowLayout = true, - containerStyle, - isChildListItem = false, - isButtonSelected = false, - parentAction = '', - shouldShowTransactionCheckbox, - isLoading = false, -}: TaskListItemRowProps) { - const styles = useThemeStyles(); - const {isLargeScreenWidth} = useResponsiveLayout(); - const StyleUtils = useStyleUtils(); - const theme = useTheme(); - - if (!isLargeScreenWidth) { - return ( - - {showItemHeaderOnNarrowLayout && ( - - )} - - - {canSelectMultiple && !!shouldShowTransactionCheckbox && ( - - - {!!item.isSelected && ( - - )} - - - )} - - - - {!!item.category && ( - - - - )} - - - - - - - - - - - ); - } + // const theme = useTheme(); + + // if (!isLargeScreenWidth) { + // return ( + // + // {showItemHeaderOnNarrowLayout && ( + // + // )} + + // + + // + // + // {!!item.category && ( + // + // + // + // )} + // + // + // + // + // + // + // + // + // + // + // ); + // } return ( - {canSelectMultiple && ( - - )} - - - - - - - - + + {/* - - - - + */} - - {item.shouldShowCategory && ( - - - - )} - {item.shouldShowTag && ( - - - - )} - {item.shouldShowTax && ( - - - - )} - - - - - - diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index c5c16d6ee228..037b27249d0f 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -21,7 +21,7 @@ import type CursorStyles from '@styles/utils/cursor/types'; import type CONST from '@src/CONST'; import type {Attendee} from '@src/types/onyx/IOU'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {SearchPersonalDetails, SearchReport, SearchReportAction, SearchTransaction} from '@src/types/onyx/SearchResults'; +import type {SearchPersonalDetails, SearchReport, SearchReportAction, SearchTask, SearchTransaction} from '@src/types/onyx/SearchResults'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -268,6 +268,24 @@ type ReportActionListItemType = ListItem & keyForList: string; }; +type TaskListItemType = ListItem & + SearchTask & { + /** The personal details of the user who is assigned to the task */ + assignee: SearchPersonalDetails; + + /** The personal details of the user who created the task */ + createdBy: SearchPersonalDetails; + + /** final and formatted "assignee" value used for displaying and sorting */ + formattedAssignee: string; + + /** final and formatted "createdBy" value used for displaying and sorting */ + formattedCreatedBy: string; + + /** Key used internally by React */ + keyForList: string; + }; + type ReportListItemType = ListItem & SearchReport & { /** The personal details of the user requesting money */ @@ -353,6 +371,11 @@ type TransactionListItemProps = ListItemProps & { isLoading?: boolean; }; +type TaskListItemProps = ListItemProps & { + /** Whether the item's action is loading */ + isLoading?: boolean; +}; + type ReportListItemProps = ListItemProps; type ChatListItemProps = ListItemProps; @@ -726,6 +749,8 @@ export type { SectionWithIndexOffset, SelectionListHandle, TableListItemProps, + TaskListItemType, + TaskListItemProps, TransactionListItemProps, TransactionListItemType, UserListItemProps, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c8bb0434eb2c..371f940ab9ed 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -8,7 +8,7 @@ import ChatListItem from '@components/SelectionList/ChatListItem'; import ReportListItem from '@components/SelectionList/Search/ReportListItem'; import TaskListItem from '@components/SelectionList/Search/TaskListItem'; import TransactionListItem from '@components/SelectionList/Search/TransactionListItem'; -import type {ListItem, ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import type {ListItem, ReportActionListItemType, ReportListItemType, TaskListItemType, TransactionListItemType} from '@components/SelectionList/types'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -25,6 +25,7 @@ import type { SearchPersonalDetails, SearchPolicy, SearchReport, + SearchTask, SearchTransaction, SearchTransactionAction, } from '@src/types/onyx/SearchResults'; @@ -191,15 +192,23 @@ function isReportListItemType(item: ListItem): item is ReportListItemType { /** * Type guard that checks if something is a TransactionListItemType */ -function isTransactionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is TransactionListItemType { +function isTransactionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType | TaskListItemType): item is TransactionListItemType { const transactionListItem = item as TransactionListItemType; return transactionListItem.transactionID !== undefined; } +/** + * Type guard that check if something is a TaskListItemType + */ +function isTaskListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType | TaskListItemType): item is TaskListItemType { + const taskListItem = item as TaskListItemType; + return taskListItem.reportID !== undefined; +} + /** * Type guard that checks if something is a ReportActionListItemType */ -function isReportActionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is ReportActionListItemType { +function isReportActionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType | TaskListItemType): item is ReportActionListItemType { const reportActionListItem = item as ReportActionListItemType; return reportActionListItem.reportActionID !== undefined; } @@ -406,7 +415,27 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr * * Do not use directly, use only via `getSections()` facade. */ -function getTaskSections(data: OnyxTypes.SearchResults['data']): ReportActionListItemType[] {} +function getTaskSections(data: OnyxTypes.SearchResults['data']): TaskListItemType[] { + return Object.keys(data) + .filter(isReportEntry) + .map((key) => { + const taskItem = data[key] as SearchTask; + + const assignee = taskItem.managerID ? data.personalDetailsList?.[taskItem.managerID] : emptyPersonalDetails; + const createdBy = taskItem.accountID ? data.personalDetailsList?.[taskItem.accountID] : emptyPersonalDetails; + const formattedAssignee = formatPhoneNumber(getDisplayNameOrDefault(assignee)); + const formattedCreatedBy = formatPhoneNumber(getDisplayNameOrDefault(createdBy)); + + return { + ...taskItem, + assignee, + formattedAssignee, + createdBy, + formattedCreatedBy, + keyForList: taskItem.reportID, + }; + }); +} /** * @private @@ -555,7 +584,7 @@ function getSections(type: SearchDataTypes, status: SearchStatus, data: OnyxType return getReportActionsSections(data); } if (type === CONST.SEARCH.DATA_TYPES.TASK) { - return getReportActionsSections(data); + return getTaskSections(data); } if (!shouldGroupByReports) { return getTransactionsSections(data, metadata); @@ -578,6 +607,9 @@ function getSortedSections( if (type === CONST.SEARCH.DATA_TYPES.CHAT) { return getSortedReportActionData(data as ReportActionListItemType[]); } + if (type === CONST.SEARCH.DATA_TYPES.TASK) { + return getSortedReportData(data as ReportListItemType[]); + } if (!shouldGroupByReports) { return getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder); } @@ -838,6 +870,7 @@ export { getOverflowMenu, isCorrectSearchUserName, isReportActionEntry, + isTaskListItemType, getAction, createTypeMenuItems, createBaseSavedSearchMenuItem, diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 23f128fd7bd5..23d1cfdf17b7 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -3,7 +3,7 @@ import type {SearchStatus} from '@components/Search/types'; import type ChatListItem from '@components/SelectionList/ChatListItem'; import type ReportListItem from '@components/SelectionList/Search/ReportListItem'; import type TransactionListItem from '@components/SelectionList/Search/TransactionListItem'; -import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import type {ReportActionListItemType, ReportListItemType, TaskListItemType, TransactionListItemType} from '@components/SelectionList/types'; import type CONST from '@src/CONST'; import type ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxCommon from './OnyxCommon'; @@ -27,6 +27,8 @@ type ListItemType = C extends /** Model of search list item data type */ type ListItemDataType = C extends typeof CONST.SEARCH.DATA_TYPES.CHAT ? ReportActionListItemType[] + : C extends typeof CONST.SEARCH.DATA_TYPES.TASK + ? TaskListItemType[] : T extends typeof CONST.SEARCH.STATUS.EXPENSE.ALL ? TransactionListItemType[] : ReportListItemType[]; @@ -402,6 +404,30 @@ type SearchTransaction = { pendingAction?: OnyxCommon.PendingAction; }; +/** Model of tasks search result */ +type SearchTask = { + /** The account ID of the user who created the task */ + accountID: number; + + /** When the task was created */ + created: string; + + /** The description of the task that needs to be done */ + description: string; + + /** The person the task was assigned to */ + managerID: number; + + /** The chat report that the task was created in */ + parentReportID: string; + + /** The ID of the report that the task is associated with */ + reportID: string; + + /** The title of the task */ + reportName: string; +}; + /** Types of searchable transactions */ type SearchTransactionType = ValueOf; @@ -435,6 +461,7 @@ export default SearchResults; export type { ListItemType, ListItemDataType, + SearchTask, SearchTransaction, SearchTransactionType, SearchTransactionAction, From d1820ef0e35892df21c735168c6ca9960dd08182 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Mon, 7 Apr 2025 07:06:21 -0400 Subject: [PATCH 07/32] add new header --- src/CONST.ts | 4 +++ src/components/Search/index.tsx | 1 + .../SelectionList/SearchTableHeader.tsx | 36 +++++++++++++++++++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + 5 files changed, 43 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index be2037425928..ac245d9e69ac 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6418,6 +6418,10 @@ const CONST = { TYPE: 'type', ACTION: 'action', TAX_AMOUNT: 'taxAmount', + TITLE: 'title', + ASSIGNEE: 'assignee', + CREATED_BY: 'createdBy', + IN: 'in', }, SYNTAX_OPERATORS: { AND: 'and', diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 3176580a93e2..3c0b68cecffe 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -499,6 +499,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS onSelectRow={openReport} onCheckboxPress={toggleTransaction} onAllCheckboxPress={toggleAllTransactions} + // JACK_TODO: Fix this so that checkmarks can be disabled, without removing hte header canSelectMultiple={type !== CONST.SEARCH.DATA_TYPES.CHAT && canSelectMultiple} shouldPreventLongPressRow={isChat} SearchTableHeader={ diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index a722f012adf6..b1b6feebfd61 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -89,10 +89,42 @@ const expenseHeaders: SearchColumnConfig[] = [ }, ]; +const taskHeaders: SearchColumnConfig[] = [ + { + columnName: CONST.SEARCH.TABLE_COLUMNS.DATE, + translationKey: 'common.date', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.TITLE, + translationKey: 'common.title', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION, + translationKey: 'common.description', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.CREATED_BY, + translationKey: 'common.createdBy', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.IN, + translationKey: 'common.createdIn', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.ASSIGNEE, + translationKey: 'common.assignee', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.ACTION, + translationKey: 'common.action', + }, +]; + const SearchColumns = { [CONST.SEARCH.DATA_TYPES.EXPENSE]: expenseHeaders, [CONST.SEARCH.DATA_TYPES.INVOICE]: expenseHeaders, [CONST.SEARCH.DATA_TYPES.TRIP]: expenseHeaders, + [CONST.SEARCH.DATA_TYPES.TASK]: taskHeaders, [CONST.SEARCH.DATA_TYPES.CHAT]: null, }; @@ -114,6 +146,10 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shou const shouldShowColumn = useCallback( (columnName: SortableColumnName) => { + if (metadata.type === CONST.SEARCH.DATA_TYPES.TASK) { + return true; + } + const shouldShowFun = shouldShowColumnConfig[columnName]; return shouldShowFun(data, metadata); }, diff --git a/src/languages/en.ts b/src/languages/en.ts index 2b3948c8b792..6307a77cc031 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -539,6 +539,7 @@ const translations = { expenseReports: 'Expense Reports', rateOutOfPolicy: 'Rate out of policy', comments: 'Comments', + createdIn: 'Created in', }, supportalNoAccess: { title: 'Not so fast', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1c1e1a30ffa1..5f70fc6ac6e0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -530,6 +530,7 @@ const translations = { expenseReports: 'Informes de Gastos', rateOutOfPolicy: 'Tasa fuera de póliza', comments: 'Comentarios', + createdIn: 'Creado en', }, supportalNoAccess: { title: 'No tan rápido', From 7ecba84c2167d26a5450e6ced375730baa05f2b3 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Mon, 7 Apr 2025 07:33:11 -0400 Subject: [PATCH 08/32] add new task items --- .../SelectionList/Search/TaskListItem.tsx | 2 +- .../SelectionList/Search/TaskListItemRow.tsx | 76 ++++++++++++++++++- src/components/SelectionList/types.ts | 6 ++ src/libs/SearchUIUtils.ts | 19 ++++- src/styles/utils/index.ts | 5 ++ 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/components/SelectionList/Search/TaskListItem.tsx b/src/components/SelectionList/Search/TaskListItem.tsx index 84f4d379a623..617cb8ce620d 100644 --- a/src/components/SelectionList/Search/TaskListItem.tsx +++ b/src/components/SelectionList/Search/TaskListItem.tsx @@ -69,7 +69,7 @@ function TaskListItem({ > { // // handleActionButtonPress(currentSearchHash, taskItem, () => onSelectRow(item)); // }} diff --git a/src/components/SelectionList/Search/TaskListItemRow.tsx b/src/components/SelectionList/Search/TaskListItemRow.tsx index 9033151092cd..f3eee941bf40 100644 --- a/src/components/SelectionList/Search/TaskListItemRow.tsx +++ b/src/components/SelectionList/Search/TaskListItemRow.tsx @@ -2,17 +2,70 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {TaskListItemType} from '@components/SelectionList/types'; +import TextWithTooltip from '@components/TextWithTooltip'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; import CONST from '@src/CONST'; import UserInfoCell from './UserInfoCell'; type TaskListItemRowProps = { item: TaskListItemType; + showTooltip: boolean; containerStyle?: StyleProp; }; -function TaskListItemRow({item, containerStyle}: TaskListItemRowProps) { +type CellProps = { + // eslint-disable-next-line react/no-unused-prop-types + showTooltip: boolean; + // eslint-disable-next-line react/no-unused-prop-types + isLargeScreenWidth: boolean; +}; + +type TaskCellProps = { + taskItem: TaskListItemType; +} & CellProps; + +function DateCell({taskItem, showTooltip, isLargeScreenWidth}: TaskCellProps) { + const styles = useThemeStyles(); + + const created = taskItem.created; + const date = DateUtils.formatWithUTCTimeZone(created, DateUtils.doesDateBelongToAPastYear(created) ? CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT : CONST.DATE.MONTH_DAY_ABBR_FORMAT); + + return ( + + ); +} + +function TitleCell({taskItem, showTooltip, isLargeScreenWidth}: TaskCellProps) { + const styles = useThemeStyles(); + + return ( + + ); +} + +function DescriptionCell({taskItem, showTooltip, isLargeScreenWidth}: TaskCellProps) { + const styles = useThemeStyles(); + + return ( + + ); +} + +function TaskListItemRow({item, containerStyle, showTooltip}: TaskListItemRowProps) { const styles = useThemeStyles(); // const {isLargeScreenWidth} = useResponsiveLayout(); const StyleUtils = useStyleUtils(); @@ -82,13 +135,28 @@ function TaskListItemRow({item, containerStyle}: TaskListItemRowProps) { return ( - {/* + + + + + + + - */} + + { + return data.some((item: TransactionListItemType | ReportListItemType | TaskListItemType) => { if (isReportListItemType(item)) { // If the item is a ReportListItemType, iterate over its transactions and check them return item.transactions.some((transaction) => { @@ -229,6 +233,11 @@ function shouldShowYear(data: TransactionListItemType[] | ReportListItemType[] | }); } + if (isTaskListItemType(item)) { + const taskYear = new Date(item.created).getFullYear(); + return taskYear !== currentYear; + } + const createdYear = new Date(item?.modifiedCreated ? item.modifiedCreated : item?.created || '').getFullYear(); return createdYear !== currentYear; }); @@ -425,6 +434,7 @@ function getTaskSections(data: OnyxTypes.SearchResults['data']): TaskListItemTyp const createdBy = taskItem.accountID ? data.personalDetailsList?.[taskItem.accountID] : emptyPersonalDetails; const formattedAssignee = formatPhoneNumber(getDisplayNameOrDefault(assignee)); const formattedCreatedBy = formatPhoneNumber(getDisplayNameOrDefault(createdBy)); + const doesDataContainAPastYearTransaction = shouldShowYear(data); return { ...taskItem, @@ -433,6 +443,7 @@ function getTaskSections(data: OnyxTypes.SearchResults['data']): TaskListItemTyp createdBy, formattedCreatedBy, keyForList: taskItem.reportID, + shouldShowYear: doesDataContainAPastYearTransaction, }; }); } @@ -656,8 +667,8 @@ function getSortedTransactionData(data: TransactionListItemType[], sortBy?: Sear } return data.sort((a, b) => { - const aValue = sortingProperty === 'comment' ? a.comment?.comment : a[sortingProperty]; - const bValue = sortingProperty === 'comment' ? b.comment?.comment : b[sortingProperty]; + const aValue = sortingProperty === 'comment' ? a.comment?.comment : a[sortingProperty as keyof TransactionListItemType]; + const bValue = sortingProperty === 'comment' ? b.comment?.comment : b[sortingProperty as keyof TransactionListItemType]; return compareValues(aValue, bValue, sortOrder, sortingProperty); }); diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 1d8faeb41cc7..1ef4a8c9a32a 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1632,6 +1632,11 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ case CONST.SEARCH.TABLE_COLUMNS.MERCHANT: case CONST.SEARCH.TABLE_COLUMNS.FROM: case CONST.SEARCH.TABLE_COLUMNS.TO: + case CONST.SEARCH.TABLE_COLUMNS.ASSIGNEE: + case CONST.SEARCH.TABLE_COLUMNS.CREATED_BY: + case CONST.SEARCH.TABLE_COLUMNS.TITLE: + case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION: + case CONST.SEARCH.TABLE_COLUMNS.IN: columnWidth = styles.flex1; break; case CONST.SEARCH.TABLE_COLUMNS.CATEGORY: From eb39920a95b26c8b275e2a5716fb0997e7616074 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Mon, 7 Apr 2025 08:21:26 -0400 Subject: [PATCH 09/32] start adding rooms --- .../SelectionList/Search/RoomInfoCell.tsx | 59 +++++++++++++++++++ .../SelectionList/Search/TaskListItemRow.tsx | 13 +++- .../SelectionList/SearchTableHeader.tsx | 4 ++ 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/components/SelectionList/Search/RoomInfoCell.tsx diff --git a/src/components/SelectionList/Search/RoomInfoCell.tsx b/src/components/SelectionList/Search/RoomInfoCell.tsx new file mode 100644 index 000000000000..1fb7afa0aad5 --- /dev/null +++ b/src/components/SelectionList/Search/RoomInfoCell.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Avatar from '@components/Avatar'; +import Text from '@components/Text'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getPolicy} from '@libs/PolicyUtils'; +import {getIcons, getReportOrDraftReport} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type RoomInfoCellProps = { + reportID: string; +}; + +function RoomInfoCell({reportID}: RoomInfoCellProps) { + const styles = useThemeStyles(); + const {isLargeScreenWidth} = useResponsiveLayout(); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) ?? CONST.EMPTY_OBJECT; + + const report = getReportOrDraftReport(reportID); + + if (!report || !personalDetails) { + return null; + } + + const policy = getPolicy(report.policyID); + const icons = getIcons(report, personalDetails, null, '', -1, policy); + + const icon = icons?.at(0); + + return ( + + {!!icon && ( + + )} + + + {report.reportName} + + + ); +} + +RoomInfoCell.displayName = 'RoomInfoCell'; + +export default RoomInfoCell; diff --git a/src/components/SelectionList/Search/TaskListItemRow.tsx b/src/components/SelectionList/Search/TaskListItemRow.tsx index f3eee941bf40..5d186f0201e5 100644 --- a/src/components/SelectionList/Search/TaskListItemRow.tsx +++ b/src/components/SelectionList/Search/TaskListItemRow.tsx @@ -6,7 +6,10 @@ import TextWithTooltip from '@components/TextWithTooltip'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import Parser from '@libs/Parser'; +import {getReportOrDraftReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; +import RoomInfoCell from './RoomInfoCell'; import UserInfoCell from './UserInfoCell'; type TaskListItemRowProps = { @@ -43,11 +46,12 @@ function DateCell({taskItem, showTooltip, isLargeScreenWidth}: TaskCellProps) { function TitleCell({taskItem, showTooltip, isLargeScreenWidth}: TaskCellProps) { const styles = useThemeStyles(); + const taskTitle = Parser.replace(Parser.htmlToText(taskItem.reportName)); return ( ); @@ -55,11 +59,12 @@ function TitleCell({taskItem, showTooltip, isLargeScreenWidth}: TaskCellProps) { function DescriptionCell({taskItem, showTooltip, isLargeScreenWidth}: TaskCellProps) { const styles = useThemeStyles(); + const taskDescription = Parser.replace(Parser.htmlToText(taskItem.description)); return ( ); @@ -156,7 +161,6 @@ function TaskListItemRow({item, containerStyle, showTooltip}: TaskListItemRowPro isLargeScreenWidth /> - + + + metadata?.columnsToShow?.shouldShowTaxColumn ?? false, [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: () => true, [CONST.SEARCH.TABLE_COLUMNS.ACTION]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.TITLE]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.ASSIGNEE]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.CREATED_BY]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.IN]: () => true, // This column is never displayed on Search [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: () => false, }; From 25b748d2f84e771c99db26b4bea4bd72922a8ec9 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Mon, 7 Apr 2025 09:37:51 -0400 Subject: [PATCH 10/32] add action button to tasks --- .../{RoomInfoCell.tsx => ReportInfoCell.tsx} | 9 +-- .../SelectionList/Search/TaskListItemRow.tsx | 71 +++++++++++++++++-- .../SelectionList/SearchTableHeader.tsx | 2 +- src/languages/en.ts | 3 +- src/languages/es.ts | 3 +- src/libs/SearchUIUtils.ts | 3 +- src/types/onyx/SearchResults.ts | 6 ++ 7 files changed, 83 insertions(+), 14 deletions(-) rename src/components/SelectionList/Search/{RoomInfoCell.tsx => ReportInfoCell.tsx} (89%) diff --git a/src/components/SelectionList/Search/RoomInfoCell.tsx b/src/components/SelectionList/Search/ReportInfoCell.tsx similarity index 89% rename from src/components/SelectionList/Search/RoomInfoCell.tsx rename to src/components/SelectionList/Search/ReportInfoCell.tsx index 1fb7afa0aad5..bc42dd4976dc 100644 --- a/src/components/SelectionList/Search/RoomInfoCell.tsx +++ b/src/components/SelectionList/Search/ReportInfoCell.tsx @@ -6,7 +6,7 @@ import Text from '@components/Text'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {getPolicy} from '@libs/PolicyUtils'; -import {getIcons, getReportOrDraftReport} from '@libs/ReportUtils'; +import {getIcons, getReportName, getReportOrDraftReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -17,7 +17,7 @@ type RoomInfoCellProps = { function RoomInfoCell({reportID}: RoomInfoCellProps) { const styles = useThemeStyles(); const {isLargeScreenWidth} = useResponsiveLayout(); - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) ?? CONST.EMPTY_OBJECT; + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const report = getReportOrDraftReport(reportID); @@ -26,6 +26,7 @@ function RoomInfoCell({reportID}: RoomInfoCellProps) { } const policy = getPolicy(report.policyID); + const reportName = getReportName(report, policy, undefined, undefined); const icons = getIcons(report, personalDetails, null, '', -1, policy); const icon = icons?.at(0); @@ -35,11 +36,11 @@ function RoomInfoCell({reportID}: RoomInfoCellProps) { {!!icon && ( )} @@ -48,7 +49,7 @@ function RoomInfoCell({reportID}: RoomInfoCellProps) { numberOfLines={1} style={[isLargeScreenWidth ? styles.themeTextColor : styles.textMicroBold, styles.flexShrink1]} > - {report.reportName} + {reportName} ); diff --git a/src/components/SelectionList/Search/TaskListItemRow.tsx b/src/components/SelectionList/Search/TaskListItemRow.tsx index 5d186f0201e5..51f29eeec8d9 100644 --- a/src/components/SelectionList/Search/TaskListItemRow.tsx +++ b/src/components/SelectionList/Search/TaskListItemRow.tsx @@ -1,15 +1,23 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import Badge from '@components/Badge'; +import Button from '@components/Button'; +import * as Expensicons from '@components/Icon/Expensicons'; import type {TaskListItemType} from '@components/SelectionList/types'; import TextWithTooltip from '@components/TextWithTooltip'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {callFunctionIfActionIsAllowed} from '@libs/actions/Session'; +import {completeTask} from '@libs/actions/Task'; import DateUtils from '@libs/DateUtils'; import Parser from '@libs/Parser'; -import {getReportOrDraftReport} from '@libs/ReportUtils'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; -import RoomInfoCell from './RoomInfoCell'; +import ReportInfoCell from './ReportInfoCell'; import UserInfoCell from './UserInfoCell'; type TaskListItemRowProps = { @@ -19,9 +27,7 @@ type TaskListItemRowProps = { }; type CellProps = { - // eslint-disable-next-line react/no-unused-prop-types showTooltip: boolean; - // eslint-disable-next-line react/no-unused-prop-types isLargeScreenWidth: boolean; }; @@ -70,6 +76,54 @@ function DescriptionCell({taskItem, showTooltip, isLargeScreenWidth}: TaskCellPr ); } +function ActionCell({taskItem, isLargeScreenWidth}: TaskCellProps) { + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const theme = useTheme(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + + const isTaskCompleted = taskItem.statusNum === CONST.REPORT.STATUS_NUM.APPROVED && taskItem.stateNum === CONST.REPORT.STATE_NUM.APPROVED; + + const completeTaskCall = callFunctionIfActionIsAllowed(() => { + completeTask(taskItem, taskItem.reportID); + }); + + if (isTaskCompleted) { + return ( + + + + ); + } + + return ( +