Skip to content

Commit

Permalink
feat(Search): add highlights (#10877)
Browse files Browse the repository at this point in the history
  • Loading branch information
Betree authored Dec 24, 2024
1 parent 75fa2e1 commit b58d921
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 11 deletions.
30 changes: 27 additions & 3 deletions components/search/AccountResult.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
import React from 'react';
import { Markup } from 'interweave';
import { useIntl } from 'react-intl';

import formatCollectiveType from '../../lib/i18n/collective-type';

import Avatar from '../Avatar';
import { Badge } from '../ui/Badge';

import { getHighlightsFields } from './lib';
import type { SearchHighlights } from './types';
import type { AccountResultData } from './useRecentlyVisited';

export function AccountResult({ account }: { account: AccountResultData }) {
export function AccountResult({ account, highlights }: { account: AccountResultData; highlights?: SearchHighlights }) {
const intl = useIntl();
const highlightFields = getHighlightsFields(highlights, ['name', 'slug']);
const otherHighlight = Object.values(highlightFields.others)[0]?.[0];
return (
<div className="flex w-full items-center gap-2">
<Avatar collective={account} size={36} />

<div className="flex-1 overflow-hidden">
<div className="flex items-center justify-between gap-2">
<div className="truncate font-medium">{account.name}</div>
<div className="truncate font-medium">
{highlightFields.top.name ? (
<Markup allowList={['mark']} content={highlightFields.top.name[0]} />
) : (
account.name
)}
</div>
<Badge type="outline" size="xs">
{formatCollectiveType(intl, account.type)}
</Badge>
</div>
<div className="truncate text-muted-foreground">@{account.slug}</div>
<div className="truncate text-muted-foreground">
@
{highlightFields.top.slug ? (
<Markup allowList={['mark']} content={highlightFields.top.slug[0]} />
) : (
account.slug
)}
{otherHighlight && (
<React.Fragment>
{' · '}
<Markup allowList={['mark']} className="italic" content={otherHighlight} />
</React.Fragment>
)}
</div>
</div>
</div>
);
Expand Down
23 changes: 20 additions & 3 deletions components/search/ExpenseResult.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
import React from 'react';
import { Markup } from 'interweave';
import { useIntl } from 'react-intl';

import { ExpenseType } from '../../lib/graphql/types/v2/schema';
import { i18nExpenseType } from '../../lib/i18n/expense';

import Avatar from '../Avatar';

import { getHighlightsFields } from './lib';
import type { SearchHighlights } from './types';
import type { ExpenseResultData } from './useRecentlyVisited';

export function ExpenseResult({ expense }: { expense: ExpenseResultData }) {
export function ExpenseResult({ expense, highlights }: { expense: ExpenseResultData; highlights?: SearchHighlights }) {
const intl = useIntl();
const highlightFields = getHighlightsFields(highlights, ['description']);
const otherHighlight = Object.values(highlightFields.others)[0]?.[0];
return (
<div className="flex flex-1 items-center gap-2">
<Avatar collective={expense.account} size={36} />

<div className="overflow-hidden">
<div className="truncate font-medium">{expense.description}</div>
<div className="truncate font-medium">
{' '}
{highlightFields.top.description ? (
<Markup allowList={['mark']} content={highlightFields.top.description[0]} />
) : (
expense.description
)}
</div>
<div className="truncate text-muted-foreground">
{i18nExpenseType(intl, expense.type)}
{expense.type !== ExpenseType.UNCLASSIFIED && i18nExpenseType(intl, expense.type)}
{expense.type === ExpenseType.RECEIPT && ' request'} to{' '}
<span className="text-foreground">{expense.account.name}</span> from{' '}
<span className="text-foreground">{expense.payee.name}</span>
</div>
{otherHighlight && (
<div className="truncate">
<Markup className="italic text-muted-foreground" allowList={['mark']} content={otherHighlight} />
</div>
)}
</div>
</div>
);
Expand Down
11 changes: 7 additions & 4 deletions components/search/SearchCommand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export const SearchCommand = ({ open, setOpen }) => {
{isLoading && <StyledSpinner size={16} className="absolute right-4 text-muted-foreground" />}
</div>

<CommandList className="max-h-[600px] border-b border-t-0">
<CommandList className="max-h-[600px] border-b border-t-0 [&_mark]:rounded-xl [&_mark]:bg-amber-100 [&_mark]:px-1 [&_mark]:py-2">
<CommandItem value="-" className="hidden" />

{recentlyVisited.length > 0 && debouncedInput === '' && (
Expand Down Expand Up @@ -221,7 +221,7 @@ export const SearchCommand = ({ open, setOpen }) => {
nodes={data?.search.results.accounts.collection.nodes}
renderNode={account => (
<CommandItem key={account.id} onSelect={() => handleResultSelect({ type: 'account', data: account })}>
<AccountResult account={account} />
<AccountResult account={account} highlights={data.search.results.accounts.highlights[account.id]} />
</CommandItem>
)}
/>
Expand All @@ -233,7 +233,7 @@ export const SearchCommand = ({ open, setOpen }) => {
input={debouncedInput}
renderNode={expense => (
<CommandItem key={expense.id} onSelect={() => handleResultSelect({ type: 'expense', data: expense })}>
<ExpenseResult expense={expense} />
<ExpenseResult expense={expense} highlights={data.search.results.expenses.highlights[expense.id]} />
</CommandItem>
)}
/>
Expand All @@ -248,7 +248,10 @@ export const SearchCommand = ({ open, setOpen }) => {
key={transaction.id}
onSelect={() => handleResultSelect({ type: 'transaction', data: transaction })}
>
<TransactionResult transaction={transaction} />
<TransactionResult
transaction={transaction}
highlights={data.search.results.transactions.highlights[transaction.id]}
/>
</CommandItem>
)}
/>
Expand Down
18 changes: 17 additions & 1 deletion components/search/TransactionResult.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import React from 'react';
import clsx from 'clsx';
import { Markup } from 'interweave';
import { ArrowDown, ArrowUp } from 'lucide-react';
import { useIntl } from 'react-intl';

import { i18nTransactionKind } from '../../lib/i18n/transaction';

import FormattedMoneyAmount from '../FormattedMoneyAmount';

import { getHighlightsFields } from './lib';
import type { SearchHighlights } from './types';
import type { TransactionResultData } from './useRecentlyVisited';

export function TransactionResult({ transaction }: { transaction: TransactionResultData }) {
export function TransactionResult({
transaction,
highlights,
}: {
transaction: TransactionResultData;
highlights?: SearchHighlights;
}) {
const intl = useIntl();
const highlightFields = getHighlightsFields(highlights, []);
const otherHighlight = Object.values(highlightFields.others)[0]?.[0];
return (
<div className="flex flex-1 items-center gap-2">
<div
Expand Down Expand Up @@ -42,6 +53,11 @@ export function TransactionResult({ transaction }: { transaction: TransactionRes
</div>
</div>
</div>
{otherHighlight && (
<div className="truncate">
<Markup className="italic text-muted-foreground" allowList={['mark']} content={otherHighlight} />
</div>
)}
</div>
);
}
24 changes: 24 additions & 0 deletions components/search/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { SearchHighlights } from './types';

export function getHighlightsFields<T extends string>(
highlights: SearchHighlights,
topFields: readonly T[],
): {
others: Record<string, string[]>;
top: Record<T, string[]>;
} {
const top: Record<string, string[]> = {};
const others: Record<string, string[]> = {};

if (highlights?.fields) {
for (const [field, values] of Object.entries(highlights.fields)) {
if (topFields.includes(field as T)) {
top[field as T] = values;
} else {
others[field] = values;
}
}
}

return { top: top as Record<T, string[]>, others };
}
3 changes: 3 additions & 0 deletions components/search/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const searchCommandQuery = gql`
search(searchTerm: $searchTerm, defaultLimit: $limit, host: $host, account: $account) {
results {
accounts {
highlights
collection {
totalCount
limit
Expand All @@ -24,6 +25,7 @@ export const searchCommandQuery = gql`
}
}
expenses {
highlights
collection {
totalCount
limit
Expand Down Expand Up @@ -55,6 +57,7 @@ export const searchCommandQuery = gql`
}
}
transactions @include(if: $includeTransactions) {
highlights
collection {
totalCount
limit
Expand Down
4 changes: 4 additions & 0 deletions components/search/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type SearchHighlights = {
score: number;
fields: Record<string, string[]>;
};

0 comments on commit b58d921

Please sign in to comment.