Skip to content

Commit

Permalink
Merge pull request #46481 from software-mansion-labs/kicu/45026-query…
Browse files Browse the repository at this point in the history
…-in-api
  • Loading branch information
luacmartins authored Aug 1, 2024
2 parents e0f17df + 3d23bb2 commit 207365c
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 111 deletions.
57 changes: 16 additions & 41 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,14 @@ import ROUTES from '@src/ROUTES';
import type SearchResults from '@src/types/onyx/SearchResults';
import {useSearchContext} from './SearchContext';
import SearchPageHeader from './SearchPageHeader';
import type {SearchColumnType, SearchQueryJSON, SearchStatus, SelectedTransactionInfo, SelectedTransactions, SortOrder} from './types';
import type {SearchColumnType, SearchQueryJSON, SelectedTransactionInfo, SelectedTransactions, SortOrder} from './types';

type SearchProps = {
queryJSON: SearchQueryJSON;
policyIDs?: string;
isCustomQuery: boolean;
policyIDs?: string;
};

const sortableSearchTabs: SearchStatus[] = [CONST.SEARCH.STATUS.ALL];
const transactionItemMobileHeight = 100;
const reportItemTransactionHeight = 52;
const listItemPadding = 12; // this is equivalent to 'mb3' on every transaction/report list item
Expand All @@ -49,7 +48,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri
}

function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions) {
return {...item, isSelected: !!selectedTransactions[item.keyForList]?.isSelected};
return {...item, isSelected: selectedTransactions[item.keyForList]?.isSelected};
}

function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType, selectedTransactions: SelectedTransactions) {
Expand All @@ -58,7 +57,7 @@ function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListIt
: {
...item,
transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions)),
isSelected: item.transactions.every((transaction) => !!selectedTransactions[transaction.keyForList]?.isSelected),
isSelected: item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected),
};
}

Expand Down Expand Up @@ -87,19 +86,14 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState<string[]>([]);
const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false);
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
const {status, sortBy, sortOrder, hash} = queryJSON;
const {sortBy, sortOrder, hash} = queryJSON;

const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);

useEffect(() => {
if (isSmallScreenWidth) {
return;
}
clearSelectedTransactions(hash);
setCurrentSearchHash(hash);

// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [hash]);
}, [hash, clearSelectedTransactions, setCurrentSearchHash]);

useEffect(() => {
const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]);
Expand All @@ -114,6 +108,15 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
}
}, [isSmallScreenWidth, selectedTransactions, selectionMode?.isEnabled]);

useEffect(() => {
if (isOffline) {
return;
}

SearchActions.search({queryJSON, offset, policyIDs});
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isOffline, offset, queryJSON]);

const handleOnCancelConfirmModal = () => {
setSelectedTransactionsToDelete([]);
setDeleteExpensesConfirmModalVisible(false);
Expand All @@ -134,19 +137,6 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
setDeleteExpensesConfirmModalVisible(true);
};

useEffect(() => {
const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]);
if (!isSmallScreenWidth) {
if (selectedKeys.length === 0) {
turnOffMobileSelectionMode();
}
return;
}
if (selectedKeys.length > 0 && !selectionMode?.isEnabled) {
turnOnMobileSelectionMode();
}
}, [isSmallScreenWidth, selectedTransactions, selectionMode?.isEnabled]);

const getItemHeight = useCallback(
(item: TransactionListItemType | ReportListItemType) => {
if (SearchUtils.isTransactionListItemType(item)) {
Expand Down Expand Up @@ -183,15 +173,6 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {

const searchResults = currentSearchResults?.data ? currentSearchResults : lastSearchResultsRef.current;

useEffect(() => {
if (isOffline) {
return;
}

SearchActions.search({hash, query: status, policyIDs, offset, sortBy, sortOrder});
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [hash, isOffline, offset]);

const isDataLoaded = searchResults?.data !== undefined;
const shouldShowLoadingState = !isOffline && !isDataLoaded;
const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
Expand Down Expand Up @@ -306,15 +287,10 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
};

const onSortPress = (column: SearchColumnType, order: SortOrder) => {
const currentSearchParams = SearchUtils.getCurrentSearchParams();
const currentQueryJSON = SearchUtils.buildSearchQueryJSON(currentSearchParams.q, policyIDs);

const newQuery = SearchUtils.buildSearchQueryString({...currentQueryJSON, sortBy: column, sortOrder: order});
const newQuery = SearchUtils.buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order});
navigation.setParams({q: newQuery});
};

const isSortingAllowed = sortableSearchTabs.includes(status);

const shouldShowYear = SearchUtils.shouldShowYear(searchResults?.data);

const canSelectMultiple = isSmallScreenWidth ? selectionMode?.isEnabled : true;
Expand Down Expand Up @@ -344,7 +320,6 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
metadata={searchResults?.search}
onSortPress={onSortPress}
sortOrder={sortOrder}
isSortingAllowed={isSortingAllowed}
sortBy={sortBy}
shouldShowYear={shouldShowYear}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type QueryFilter = {
type AdvancedFiltersKeys = ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS> | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS;

type QueryFilters = {
[K in AdvancedFiltersKeys]?: QueryFilter | QueryFilter[];
[K in AdvancedFiltersKeys]?: QueryFilter[];
};

type SearchQueryString = string;
Expand Down
6 changes: 2 additions & 4 deletions src/components/SelectionList/SearchTableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,11 @@ type SearchTableHeaderProps = {
metadata: OnyxTypes.SearchResults['search'];
sortBy?: SearchColumnType;
sortOrder?: SortOrder;
isSortingAllowed: boolean;
onSortPress: (column: SearchColumnType, order: SortOrder) => void;
shouldShowYear: boolean;
};

function SearchTableHeader({data, metadata, sortBy, sortOrder, isSortingAllowed, onSortPress, shouldShowYear}: SearchTableHeaderProps) {
function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shouldShowYear}: SearchTableHeaderProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions();
Expand All @@ -116,7 +115,6 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, isSortingAllowed,

const isActive = sortBy === columnName;
const textStyle = columnName === CONST.SEARCH.TABLE_COLUMNS.RECEIPT ? StyleUtils.getTextOverflowStyle('clip') : null;
const isSortable = isSortingAllowed && isColumnSortable;

return (
<SortableHeaderText
Expand All @@ -126,7 +124,7 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, isSortingAllowed,
sortOrder={sortOrder ?? CONST.SEARCH.SORT_ORDER.ASC}
isActive={isActive}
containerStyle={[StyleUtils.getSearchTableColumnStyles(columnName, shouldShowYear)]}
isSortable={isSortable}
isSortable={isColumnSortable}
onPress={(order: SortOrder) => onSortPress(columnName, order)}
/>
);
Expand Down
8 changes: 3 additions & 5 deletions src/libs/API/parameters/Search.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import type {SortOrder} from '@components/Search/types';
import type {SearchQueryString} from '@components/Search/types';

type SearchParams = {
hash: number;
query: string;
jsonQuery: SearchQueryString;
// Tod this is temporary, remove top level policyIDs as part of: https://github.com/Expensify/App/issues/46592
policyIDs?: string;
sortBy?: string;
sortOrder?: SortOrder;
offset: number;
};

export default SearchParams;
72 changes: 34 additions & 38 deletions src/libs/SearchUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ValueOf} from 'type-fest';
import type {AdvancedFiltersKeys, ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SortOrder} from '@components/Search/types';
import type {ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SortOrder} from '@components/Search/types';
import ReportListItem from '@components/SelectionList/Search/ReportListItem';
import TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
import type {ListItem, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
Expand Down Expand Up @@ -341,16 +341,31 @@ function buildSearchQueryJSON(query: SearchQueryString, policyID?: string) {
}
}

function buildSearchQueryString(partialQueryJSON?: Partial<SearchQueryJSON>) {
function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
const queryParts: string[] = [];
const defaultQueryJSON = buildSearchQueryJSON('');

// For this const values are lowercase version of the keys. We are using lowercase for ast keys.
for (const [, value] of Object.entries(CONST.SEARCH.SYNTAX_ROOT_KEYS)) {
if (partialQueryJSON?.[value]) {
queryParts.push(`${value}:${partialQueryJSON[value]}`);
for (const [, key] of Object.entries(CONST.SEARCH.SYNTAX_ROOT_KEYS)) {
if (queryJSON?.[key]) {
queryParts.push(`${key}:${queryJSON[key]}`);
} else if (defaultQueryJSON) {
queryParts.push(`${value}:${defaultQueryJSON[value]}`);
queryParts.push(`${key}:${defaultQueryJSON[key]}`);
}
}

if (!queryJSON) {
return queryParts.join(' ');
}

const filters = getFilters(queryJSON);

for (const [, filterKey] of Object.entries(CONST.SEARCH.SYNTAX_FILTER_KEYS)) {
const queryFilter = filters[filterKey];

if (queryFilter) {
const filterValueString = buildFilterString(filterKey, queryFilter);
queryParts.push(filterValueString);
}
}

Expand Down Expand Up @@ -436,29 +451,9 @@ function buildQueryStringFromFilters(filterValues: Partial<SearchAdvancedFilters
return dateFilter ? `${filtersString} ${dateFilter}` : filtersString;
}

function getFilters(query: SearchQueryString, fields: Array<Partial<AdvancedFiltersKeys>>) {
let queryAST;

try {
queryAST = searchParser.parse(query) as SearchQueryJSON;
} catch (e) {
console.error(e);
return;
}

function getFilters(queryJSON: SearchQueryJSON) {
const filters = {} as QueryFilters;

fields.forEach((field) => {
const rootFieldKey = field as ValueOf<typeof CONST.SEARCH.SYNTAX_ROOT_KEYS>;
if (queryAST[rootFieldKey] === undefined) {
return;
}

filters[field] = {
operator: 'eq',
value: queryAST[rootFieldKey],
};
});
const filterKeys = Object.values(CONST.SEARCH.SYNTAX_FILTER_KEYS);

function traverse(node: ASTNode) {
if (!node.operator) {
Expand All @@ -474,34 +469,35 @@ function getFilters(query: SearchQueryString, fields: Array<Partial<AdvancedFilt
}

const nodeKey = node.left as ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>;
if (!fields.includes(nodeKey)) {
if (!filterKeys.includes(nodeKey)) {
return;
}

if (!filters[nodeKey]) {
filters[nodeKey] = [];
}

const filterArray = filters[nodeKey] as QueryFilter[];
// the "?? []" is added only for typescript because otherwise TS throws an error, in newer TS versions this should be fixed
const filterArray = filters[nodeKey] ?? [];
filterArray.push({
operator: node.operator,
value: node.right as string | number,
});
}

if (queryAST.filters) {
traverse(queryAST.filters);
if (queryJSON.filters) {
traverse(queryJSON.filters);
}

return filters;
}

function buildFilterValueString(filterName: string, queryFilters: QueryFilter[]) {
function buildFilterString(filterName: string, queryFilters: QueryFilter[]) {
let filterValueString = '';
queryFilters.forEach((queryFilter, index) => {
// If the previous queryFilter has the same operator (this rule applies only to eq and neq operators) then append the current value
if ((queryFilter.operator === 'eq' && queryFilters[index - 1]?.operator === 'eq') || (queryFilter.operator === 'neq' && queryFilters[index - 1]?.operator === 'neq')) {
filterValueString += `,${queryFilter.value}`;
filterValueString += `,${filterName}:${queryFilter.value}`;
} else {
filterValueString += ` ${filterName}${operatorToSignMap[queryFilter.operator]}${queryFilter.value}`;
}
Expand All @@ -511,14 +507,14 @@ function buildFilterValueString(filterName: string, queryFilters: QueryFilter[])
}

function getSearchHeaderTitle(queryJSON: SearchQueryJSON) {
const {inputQuery, type, status} = queryJSON;
const filters = getFilters(inputQuery, Object.values(CONST.SEARCH.SYNTAX_FILTER_KEYS)) ?? {};
const {type, status} = queryJSON;
const filters = getFilters(queryJSON) ?? {};

let title = `type:${type} status:${status}`;

Object.keys(filters).forEach((key) => {
const queryFilter = filters[key as ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>] as QueryFilter[];
title += buildFilterValueString(key, queryFilter);
const queryFilter = filters[key as ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>] ?? [];
title += buildFilterString(key, queryFilter);
});

return title;
Expand Down
31 changes: 10 additions & 21 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
import type {FormOnyxValues} from '@components/Form/types';
import type {SearchQueryString} from '@components/Search/types';
import type {SearchQueryJSON} from '@components/Search/types';
import * as API from '@libs/API';
import type {ExportSearchItemsToCSVParams, SearchParams} from '@libs/API/parameters';
import type {ExportSearchItemsToCSVParams} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ApiUtils from '@libs/ApiUtils';
import fileDownload from '@libs/fileDownload';
import enhanceParameters from '@libs/Network/enhanceParameters';
import {buildSearchQueryJSON} from '@libs/SearchUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {SearchTransaction} from '@src/types/onyx/SearchResults';
Expand Down Expand Up @@ -50,26 +49,16 @@ function getOnyxLoadingData(hash: number): {optimisticData: OnyxUpdate[]; finall
return {optimisticData, finallyData};
}

function search({hash, query, policyIDs, offset, sortBy, sortOrder}: SearchParams) {
const {optimisticData, finallyData} = getOnyxLoadingData(hash);

API.read(READ_COMMANDS.SEARCH, {hash, query, offset, policyIDs, sortBy, sortOrder}, {optimisticData, finallyData});
}

// TODO_SEARCH: use this function after backend changes.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function searchV2(queryString: SearchQueryString) {
const queryJSON = buildSearchQueryJSON(queryString);

if (!queryJSON) {
return;
}

function search({queryJSON, offset, policyIDs}: {queryJSON: SearchQueryJSON; offset?: number; policyIDs?: string}) {
const {optimisticData, finallyData} = getOnyxLoadingData(queryJSON.hash);

// TODO_SEARCH: uncomment this line after backend changes
// @ts-expect-error waiting for backend changes
API.read(READ_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery: JSON.stringify(queryJSON)}, {optimisticData, finallyData});
const queryWithOffset = {
...queryJSON,
offset,
};
const jsonQuery = JSON.stringify(queryWithOffset);

API.read(READ_COMMANDS.SEARCH, {hash: queryJSON.hash, jsonQuery, policyIDs}, {optimisticData, finallyData});
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Search/SearchPageBottomTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ function SearchPageBottomTab() {
{shouldUseNarrowLayout && queryJSON && (
<Search
queryJSON={queryJSON}
policyIDs={policyIDs}
isCustomQuery={isCustomQuery}
policyIDs={policyIDs}
/>
)}
</FullPageNotFoundView>
Expand Down

0 comments on commit 207365c

Please sign in to comment.