Compare commits

...

13 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
2044580642 Notes tag autocomplete 2024-05-01 14:45:29 -07:00
Joel Jeremy Marquez
01a2f0665e VRT 2024-04-30 07:23:09 -07:00
Joel Jeremy Marquez
12b5e0ac7c Revert pill colors 2024-04-29 15:42:38 -07:00
Joel Jeremy Marquez
060716b53c Fix overlapping UI 2024-04-29 15:17:50 -07:00
Joel Jeremy Marquez
6f1b66f9dc Append note tag filters 2024-04-29 15:12:11 -07:00
Joel Jeremy Marquez
eea961a20d Cleanup style 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
5da0f86384 Remove font weight 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
176a4782a1 Rename variables 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
2ece4d35a7 Fix filtering 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
fe2166207b Update colors 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
849a43d9ce Fix tests - extract the handler to higher level component 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
30d4ca9ac6 Release notes 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
f49c7f516e Format notes as clickable tags 2024-04-27 14:23:03 -07:00
35 changed files with 549 additions and 231 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -1,12 +1,10 @@
import React, { PureComponent, createRef, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import { Navigate, useParams, useLocation, useMatch } from 'react-router-dom';
import { debounce } from 'debounce';
import { bindActionCreators } from 'redux';
import { validForTransfer } from 'loot-core/client/transfer';
import * as actions from 'loot-core/src/client/actions';
import { useFilters } from 'loot-core/src/client/data-hooks/filters';
import { SchedulesProvider } from 'loot-core/src/client/data-hooks/schedules';
import * as queries from 'loot-core/src/client/queries';
@@ -26,6 +24,7 @@ import {
import { applyChanges, groupById } from 'loot-core/src/shared/util';
import { useAccounts } from '../../hooks/useAccounts';
import { useActions } from '../../hooks/useActions';
import { useCategories } from '../../hooks/useCategories';
import { useDateFormat } from '../../hooks/useDateFormat';
import { useFailedAccounts } from '../../hooks/useFailedAccounts';
@@ -179,7 +178,7 @@ class AccountInternal extends PureComponent {
this.state = {
search: '',
filters: props.conditions || [],
filterConditions: props.filterConditions || [],
loading: true,
workingHard: false,
reconcileAmount: null,
@@ -256,7 +255,7 @@ class AccountInternal extends PureComponent {
// Important that any async work happens last so that the
// listeners are set up synchronously
await this.props.initiallyLoadPayees();
await this.fetchTransactions(this.state.filters);
await this.fetchTransactions(this.state.filterConditions);
// If there is a pending undo, apply it immediately (this happens
// when an undo changes the location to this page)
@@ -285,7 +284,7 @@ class AccountInternal extends PureComponent {
//Resest sort/filter/search on account change
if (this.props.accountId !== prevProps.accountId) {
this.setState({ sort: [], search: '', filters: [] });
this.setState({ sort: [], search: '', filterConditions: [] });
}
}
@@ -313,10 +312,10 @@ class AccountInternal extends PureComponent {
this.paged?.run();
};
fetchTransactions = filters => {
fetchTransactions = filterConditions => {
const query = this.makeRootQuery();
this.rootQuery = this.currentQuery = query;
if (filters) this.applyFilters(filters);
if (filterConditions) this.applyFilters(filterConditions);
else this.updateQuery(query);
if (this.props.accountId) {
@@ -418,7 +417,10 @@ class AccountInternal extends PureComponent {
onSearchDone = debounce(() => {
if (this.state.search === '') {
this.updateQuery(this.currentQuery, this.state.filters.length > 0);
this.updateQuery(
this.currentQuery,
this.state.filterConditions.length > 0,
);
} else {
this.updateQuery(
queries.makeTransactionSearchQuery(
@@ -511,7 +513,7 @@ class AccountInternal extends PureComponent {
return (
account &&
this.state.search === '' &&
this.state.filters.length === 0 &&
this.state.filterConditions.length === 0 &&
(this.state.sort.length === 0 ||
(this.state.sort.field === 'date' &&
this.state.sort.ascDesc === 'desc'))
@@ -599,7 +601,7 @@ class AccountInternal extends PureComponent {
{
transactions: [],
transactionCount: 0,
filters: [],
filterConditions: [],
search: '',
sort: [],
showBalances: true,
@@ -612,9 +614,9 @@ class AccountInternal extends PureComponent {
break;
case 'remove-sorting': {
this.setState({ sort: [] }, () => {
const filters = this.state.filters;
if (filters.length > 0) {
this.applyFilters([...filters]);
const filterConditions = this.state.filterConditions;
if (filterConditions.length > 0) {
this.applyFilters([...filterConditions]);
} else {
this.fetchTransactions();
}
@@ -637,12 +639,12 @@ class AccountInternal extends PureComponent {
if (this.state.showReconciled) {
this.props.savePrefs({ ['hide-reconciled-' + accountId]: true });
this.setState({ showReconciled: false }, () =>
this.fetchTransactions(this.state.filters),
this.fetchTransactions(this.state.filterConditions),
);
} else {
this.props.savePrefs({ ['hide-reconciled-' + accountId]: false });
this.setState({ showReconciled: true }, () =>
this.fetchTransactions(this.state.filters),
this.fetchTransactions(this.state.filterConditions),
);
}
break;
@@ -797,7 +799,7 @@ class AccountInternal extends PureComponent {
onShowTransactions = async ids => {
this.onApplyFilter({
customName: 'Selected transactions',
filter: { id: { $oneof: ids } },
queryFilter: { id: { $oneof: ids } },
});
};
@@ -1183,10 +1185,10 @@ class AccountInternal extends PureComponent {
);
};
onCondOpChange = (value, filters) => {
onConditionsOpChange = (value, conditions) => {
this.setState({ conditionsOp: value });
this.setState({ filterId: { ...this.state.filterId, status: 'changed' } });
this.applyFilters([...filters]);
this.applyFilters([...conditions]);
if (this.state.search !== '') {
this.onSearch(this.state.search);
}
@@ -1194,11 +1196,11 @@ class AccountInternal extends PureComponent {
onReloadSavedFilter = (savedFilter, item) => {
if (item === 'reload') {
const [getFilter] = this.props.filtersList.filter(
const [savedFilter] = this.props.savedFilters.filter(
f => f.id === this.state.filterId.id,
);
this.setState({ conditionsOp: getFilter.conditionsOp });
this.applyFilters([...getFilter.conditions]);
this.setState({ conditionsOp: savedFilter.conditionsOp });
this.applyFilters([...savedFilter.conditions]);
} else {
if (savedFilter.status) {
this.setState({ conditionsOp: savedFilter.conditionsOp });
@@ -1217,9 +1219,11 @@ class AccountInternal extends PureComponent {
}
};
onUpdateFilter = (oldFilter, updatedFilter) => {
onUpdateFilter = (oldCondition, updatedCondition) => {
this.applyFilters(
this.state.filters.map(f => (f === oldFilter ? updatedFilter : f)),
this.state.filterConditions.map(c =>
c === oldCondition ? updatedCondition : c,
),
);
this.setState({
filterId: {
@@ -1232,9 +1236,9 @@ class AccountInternal extends PureComponent {
}
};
onDeleteFilter = filter => {
this.applyFilters(this.state.filters.filter(f => f !== filter));
if (this.state.filters.length === 1) {
onDeleteFilter = condition => {
this.applyFilters(this.state.filterConditions.filter(c => c !== condition));
if (this.state.filterConditions.length === 1) {
this.setState({ filterId: [] });
this.setState({ conditionsOp: 'and' });
} else {
@@ -1250,23 +1254,31 @@ class AccountInternal extends PureComponent {
}
};
onApplyFilter = async cond => {
let filters = this.state.filters;
if (cond.customName) {
filters = filters.filter(f => f.customName !== cond.customName);
onApplyFilter = async conditionOrSavedFilter => {
let filterConditions = this.state.filterConditions;
if (conditionOrSavedFilter.customName) {
filterConditions = filterConditions.filter(
c => c.customName !== conditionOrSavedFilter.customName,
);
}
if (cond.conditions) {
this.setState({ filterId: { ...cond, status: 'saved' } });
this.setState({ conditionsOp: cond.conditionsOp });
this.applyFilters([...cond.conditions]);
if (conditionOrSavedFilter.conditions) {
// A saved filter was passed in.
const savedFilter = conditionOrSavedFilter;
this.setState({
filterId: { ...savedFilter, status: 'saved' },
});
this.setState({ conditionsOp: savedFilter.conditionsOp });
this.applyFilters([...savedFilter.conditions]);
} else {
// A condition was passed in.
const condition = conditionOrSavedFilter;
this.setState({
filterId: {
...this.state.filterId,
status: this.state.filterId && 'changed',
},
});
this.applyFilters([...filters, cond]);
this.applyFilters([...filterConditions, condition]);
}
if (this.state.search !== '') {
this.onSearch(this.state.search);
@@ -1292,28 +1304,38 @@ class AccountInternal extends PureComponent {
}
};
applyFilters = async conditions => {
applyFilters = async (conditions, append = false) => {
if (conditions.length > 0) {
const customFilters = conditions
const customQueryFilters = conditions
.filter(cond => !!cond.customName)
.map(f => f.filter);
const { filters } = await send('make-filters-from-conditions', {
conditions: conditions.filter(cond => !cond.customName),
});
.map(f => f.queryFilter);
const { filters: queryFilters } = await send(
'make-filters-from-conditions',
{
conditions: conditions.filter(cond => !cond.customName),
},
);
const conditionsOpKey = this.state.conditionsOp === 'or' ? '$or' : '$and';
this.currentQuery = this.rootQuery.filter({
[conditionsOpKey]: [...filters, ...customFilters],
[conditionsOpKey]: [...queryFilters, ...customQueryFilters],
});
this.setState({ filters: conditions }, () => {
this.updateQuery(this.currentQuery, true);
});
this.setState(
prevState => ({
filterConditions: append
? [...prevState.filterConditions, ...conditions]
: conditions,
}),
() => {
this.updateQuery(this.currentQuery, true);
},
);
} else {
this.setState(
{
transactions: [],
transactionCount: 0,
filters: conditions,
filterConditions: conditions,
},
() => {
this.fetchTransactions();
@@ -1327,8 +1349,8 @@ class AccountInternal extends PureComponent {
};
applySort = (field, ascDesc, prevField, prevAscDesc) => {
const filters = this.state.filters;
const isFiltered = filters.length > 0;
const filterConditions = this.state.filterConditions;
const isFiltered = filterConditions.length > 0;
const sortField = getField(!field ? this.state.sort.field : field);
const sortAscDesc = !ascDesc ? this.state.sort.ascDesc : ascDesc;
const sortPrevField = getField(
@@ -1395,7 +1417,7 @@ class AccountInternal extends PureComponent {
// called directly from UI by sorting a column.
// active filters need to be applied before sorting
case isFiltered:
this.applyFilters([...filters]);
this.applyFilters([...filterConditions]);
sortCurrentQuery(this, sortField, sortAscDesc);
break;
@@ -1457,7 +1479,6 @@ class AccountInternal extends PureComponent {
addNotification,
accountsSyncing,
failedAccounts,
pushModal,
replaceModal,
showExtraBalances,
accountId,
@@ -1524,7 +1545,7 @@ class AccountInternal extends PureComponent {
workingHard={workingHard}
account={account}
filterId={filterId}
filtersList={this.props.filtersList}
savedFilters={this.props.savedFilters}
location={this.props.location}
accountName={accountName}
accountsSyncing={accountsSyncing}
@@ -1541,7 +1562,7 @@ class AccountInternal extends PureComponent {
isSorted={this.state.sort.length !== 0}
reconcileAmount={reconcileAmount}
search={this.state.search}
filters={this.state.filters}
filterConditions={this.state.filterConditions}
conditionsOp={this.state.conditionsOp}
savePrefs={this.props.savePrefs}
pushModal={this.props.pushModal}
@@ -1567,7 +1588,7 @@ class AccountInternal extends PureComponent {
onUpdateFilter={this.onUpdateFilter}
onClearFilters={this.onClearFilters}
onReloadSavedFilter={this.onReloadSavedFilter}
onCondOpChange={this.onCondOpChange}
onConditionsOpChange={this.onConditionsOpChange}
onDeleteFilter={this.onDeleteFilter}
onApplyFilter={this.onApplyFilter}
onScheduleAction={this.onScheduleAction}
@@ -1601,7 +1622,8 @@ class AccountInternal extends PureComponent {
isNew={this.isNew}
isMatched={this.isMatched}
isFiltered={
this.state.search !== '' || this.state.filters.length > 0
this.state.search !== '' ||
this.state.filterConditions.length > 0
}
dateFormat={dateFormat}
hideFraction={hideFraction}
@@ -1622,7 +1644,6 @@ class AccountInternal extends PureComponent {
</View>
) : null
}
pushModal={pushModal}
onSort={this.onSort}
sortField={this.state.sort.field}
ascDesc={this.state.sort.ascDesc}
@@ -1638,6 +1659,7 @@ class AccountInternal extends PureComponent {
this.setState({ isAdding: false })
}
onCreatePayee={this.onCreatePayee}
onApplyFilters={this.applyFilters}
/>
</View>
</View>
@@ -1685,36 +1707,10 @@ export function Account() {
const modalShowing = useSelector(state => state.modals.modalStack.length > 0);
const accountsSyncing = useSelector(state => state.account.accountsSyncing);
const lastUndoState = useSelector(state => state.app.lastUndoState);
const conditions =
location.state && location.state.conditions
? location.state.conditions
: [];
const filterConditions = location?.state?.filterConditions || [];
const state = {
newTransactions,
matchedTransactions,
accounts,
failedAccounts,
dateFormat,
hideFraction,
expandSplits,
showBalances,
showCleared: !hideCleared,
showReconciled: !hideReconciled,
showExtraBalances,
payees,
modalShowing,
accountsSyncing,
lastUndoState,
conditions,
};
const dispatch = useDispatch();
const filtersList = useFilters();
const actionCreators = useMemo(
() => bindActionCreators(actions, dispatch),
[dispatch],
);
const savedFiters = useFilters();
const actionCreators = useActions();
const transform = useMemo(() => {
const filterByAccount = queries.getAccountFilter(params.id, '_account');
@@ -1743,17 +1739,31 @@ export function Account() {
return (
<SchedulesProvider transform={transform}>
<SplitsExpandedProvider
initialMode={state.expandSplits ? 'collapse' : 'expand'}
initialMode={expandSplits ? 'collapse' : 'expand'}
>
<AccountHack
{...state}
newTransactions={newTransactions}
matchedTransactions={matchedTransactions}
accounts={accounts}
failedAccounts={failedAccounts}
dateFormat={dateFormat}
hideFraction={hideFraction}
expandSplits={expandSplits}
showBalances={showBalances}
showCleared={!hideCleared}
showReconciled={!hideReconciled}
showExtraBalances={showExtraBalances}
payees={payees}
modalShowing={modalShowing}
accountsSyncing={accountsSyncing}
lastUndoState={lastUndoState}
filterConditions={filterConditions}
categoryGroups={categoryGroups}
{...actionCreators}
modalShowing={state.modalShowing}
accountId={params.id}
categoryId={location?.state?.categoryId}
location={location}
filtersList={filtersList}
savedFilters={savedFiters}
/>
</SplitsExpandedProvider>
</SchedulesProvider>

View File

@@ -141,7 +141,7 @@ export function Balances({
showExtraBalances,
onToggleExtraBalances,
account,
filteredItems,
showFilteredBalances,
transactions,
}) {
const selectedItems = useSelectedItems();
@@ -200,9 +200,7 @@ export function Balances({
{selectedItems.size > 0 && (
<SelectedBalance selectedItems={selectedItems} account={account} />
)}
{filteredItems.length > 0 && (
<FilteredBalance selectedItems={transactions} />
)}
{showFilteredBalances && <FilteredBalance selectedItems={transactions} />}
</View>
);
}

View File

@@ -39,7 +39,7 @@ export function AccountHeader({
accountName,
account,
filterId,
filtersList,
savedFilters,
accountsSyncing,
failedAccounts,
accounts,
@@ -54,7 +54,7 @@ export function AccountHeader({
canCalculateBalance,
isSorted,
search,
filters,
filterConditions,
conditionsOp,
pushModal,
onSearch,
@@ -78,7 +78,7 @@ export function AccountHeader({
onUpdateFilter,
onClearFilters,
onReloadSavedFilter,
onCondOpChange,
onConditionsOpChange,
onDeleteFilter,
onScheduleAction,
onSetTransfer,
@@ -241,7 +241,7 @@ export function AccountHeader({
showExtraBalances={showExtraBalances}
onToggleExtraBalances={onToggleExtraBalances}
account={account}
filteredItems={filters}
showFilteredBalances={filterConditions.length > 0}
transactions={transactions}
/>
@@ -320,7 +320,7 @@ export function AccountHeader({
)}
<Button
type="bare"
disabled={search !== '' || filters.length > 0}
disabled={search !== '' || filterConditions.length > 0}
style={{ padding: 6, marginLeft: 10 }}
onClick={onToggleSplits}
title={
@@ -375,17 +375,17 @@ export function AccountHeader({
)}
</Stack>
{filters && filters.length > 0 && (
{filterConditions?.length > 0 && (
<FiltersStack
filters={filters}
conditions={filterConditions}
conditionsOp={conditionsOp}
onUpdateFilter={onUpdateFilter}
onDeleteFilter={onDeleteFilter}
onClearFilters={onClearFilters}
onReloadSavedFilter={onReloadSavedFilter}
filterId={filterId}
filtersList={filtersList}
onCondOpChange={onCondOpChange}
savedFilters={savedFilters}
onConditionsOpChange={onConditionsOpChange}
/>
)}
</View>

View File

@@ -8,7 +8,6 @@ import React, {
type HTMLProps,
type ReactNode,
type KeyboardEvent,
type ChangeEvent,
} from 'react';
import Downshift, { type StateChangeTypes } from 'downshift';
@@ -27,9 +26,7 @@ type CommonAutocompleteProps<T extends Item> = {
embedded?: boolean;
containerProps?: HTMLProps<HTMLDivElement>;
labelProps?: { id?: string };
inputProps?: Omit<ComponentProps<typeof Input>, 'onChange'> & {
onChange?: (value: string) => void;
};
inputProps?: ComponentProps<typeof Input>;
suggestions?: T[];
tooltipStyle?: CSSProperties;
tooltipProps?: ComponentProps<typeof Tooltip>;
@@ -502,7 +499,7 @@ function SingleAutocomplete<T extends Item>({
// Handle it ourselves
e.stopPropagation();
onSelect(value, (e.target as HTMLInputElement).value);
return onSelectAfter();
onSelectAfter();
} else {
// No highlighted item, still allow the table to save the item
// as `null`, even though we're allowing the table to move
@@ -541,10 +538,6 @@ function SingleAutocomplete<T extends Item>({
}
}
},
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const { onChange } = inputProps || {};
onChange?.(e.target.value);
},
}),
)}
{isOpen &&

View File

@@ -0,0 +1,196 @@
import React, { useMemo, type ComponentProps, useRef, useState } from 'react';
import { useLiveQuery } from 'loot-core/client/query-hooks';
import { q } from 'loot-core/shared/query';
import {
type TransactionEntity,
type NoteEntity,
} from 'loot-core/types/models';
import { useMergedRefs } from '../../hooks/useMergedRefs';
import { theme } from '../../style';
import { View } from '../common/View';
import { Autocomplete } from './Autocomplete';
import { ItemHeader } from './ItemHeader';
export function NotesTagAutocomplete({
embedded,
...props
}: {
embedded?: boolean;
} & ComponentProps<typeof Autocomplete<{ id: string; name: string }>>) {
const notesData = useLiveQuery<NoteEntity[]>(
() =>
q('notes')
.filter({ note: { $like: '#%' } })
.select('note'),
[],
);
const transactionData = useLiveQuery<TransactionEntity[]>(
() =>
q('transactions')
.filter({ notes: { $like: '#%' } })
.select('notes'),
[],
);
const allNotes = useMemo(
() =>
[
...(notesData || []).map(n => n.note),
...(transactionData || []).map(t => t.notes),
].filter(n => !!n),
[notesData, transactionData],
);
const uniqueTags = useMemo(
() =>
[
...new Set(
allNotes
.flatMap(note => note.split(' '))
.filter(note => note.startsWith('#') && note.length > 1),
),
].map(tag => ({ id: tag, name: tag })),
[allNotes],
);
const inputRef = useRef<HTMLInputElement>(null);
const mergedInputRef = useMergedRefs<HTMLInputElement>(
inputRef,
props.inputProps?.inputRef,
);
const [value, setValue] = useState(props.value || props.inputProps?.value);
const onUpdate = (tag: string, inputValue: string) => {
console.log(`onUpdate inputValue = ${inputValue}`)
setValue(inputValue);
props.onUpdate?.(tag, inputValue);
};
const onSelect = (tag: string) => {
let localValue = value;
if (localValue && tag?.startsWith('#') && !/\s/.test(tag)) {
// A tag was selected. Append it to the existing notes.
localValue = insertTextAtCaret(
localValue,
tag.substring(1), // Remove hashtag
inputRef.current?.selectionStart,
);
setValue(localValue);
}
props.onSelect?.(tag, localValue);
};
return (
<Autocomplete
openOnFocus={false}
clearOnBlur={false}
clearOnSelect={false}
highlightFirst={true}
embedded={embedded}
suggestions={uniqueTags}
filterSuggestions={(suggestions, notes) => {
const tag = getTagAtCaret(notes, inputRef.current?.selectionStart);
if (!tag) {
return [];
}
return tag === '#'
? suggestions
: suggestions.filter(suggestion =>
suggestion.name.toLowerCase().includes(tag.toLowerCase()),
);
}}
renderItems={(items, getItemProps, highlightedIndex) => (
<TagList
items={items}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
embedded={embedded}
/>
)}
{...props}
inputProps={{
...props.inputProps,
inputRef: mergedInputRef,
value: value || '',
}}
onUpdate={onUpdate}
onSelect={onSelect}
/>
);
}
function TagList<T extends { id: string; name: string }>({
items,
getItemProps,
highlightedIndex,
embedded,
}: {
items: T[];
getItemProps: (arg: { item: T }) => ComponentProps<typeof View>;
highlightedIndex: number;
embedded?: boolean;
}) {
return (
<View>
<View
style={{
overflow: 'auto',
padding: '5px 0',
...(!embedded && { maxHeight: 175 }),
}}
>
<ItemHeader title="Tags" type="tag" />
{items.map((item, idx) => {
return [
<div
{...(getItemProps ? getItemProps({ item }) : null)}
key={item.id}
style={{
backgroundColor:
highlightedIndex === idx
? theme.menuAutoCompleteBackgroundHover
: 'transparent',
padding: 4,
paddingLeft: 20,
borderRadius: embedded ? 4 : 0,
}}
data-testid={`${item.name}-tag-item`}
data-highlighted={highlightedIndex === idx || undefined}
>
{item.name}
</div>,
];
})}
</View>
</View>
);
}
function getTagAtCaret(notes: string, caretPosition: number) {
let startIndex = caretPosition;
while (startIndex > 0 && notes[startIndex - 1] !== ' ') startIndex--;
const whiteSpaceIndex = notes.indexOf(' ', caretPosition);
const endIndex = whiteSpaceIndex !== -1 ? whiteSpaceIndex : notes.length;
return notes[startIndex] === '#' ? notes.slice(startIndex, endIndex) : '';
}
function insertTextAtCaret(
text: string,
textToInsert: string,
caretPosition: number,
) {
// Insert the text at the caret position
return (
text.substring(0, caretPosition) +
textToInsert +
text.substring(caretPosition)
);
}

View File

@@ -293,7 +293,7 @@ export function PayeeAutocomplete({
setPayeeFieldFocused(false);
},
onFocus: () => setPayeeFieldFocused(true),
onChange: setRawPayee,
onChangeValue: setRawPayee,
}}
onUpdate={(id, inputValue) => onUpdate?.(id, makeNew(id, inputValue))}
onSelect={handleSelect}

View File

@@ -277,7 +277,7 @@ function BudgetInner(props: BudgetInnerProps) {
};
const onShowActivity = (categoryId, month) => {
const conditions = [
const filterConditions = [
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
{
field: 'date',
@@ -290,7 +290,7 @@ function BudgetInner(props: BudgetInnerProps) {
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId,
},
});

View File

@@ -4,26 +4,29 @@ import { type RuleConditionEntity } from 'loot-core/src/types/models';
import { View } from '../common/View';
import { CondOpMenu } from './CondOpMenu';
import { ConditionsOpMenu } from './ConditionsOpMenu';
import { FilterExpression } from './FilterExpression';
type AppliedFiltersProps = {
filters: RuleConditionEntity[];
conditions: RuleConditionEntity[];
onUpdate: (
filter: RuleConditionEntity,
newFilter: RuleConditionEntity,
) => void;
onDelete: (filter: RuleConditionEntity) => void;
conditionsOp: string;
onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
onConditionsOpChange: (
value: string,
conditions: RuleConditionEntity[],
) => void;
};
export function AppliedFilters({
filters,
conditions,
onUpdate,
onDelete,
conditionsOp,
onCondOpChange,
onConditionsOpChange,
}: AppliedFiltersProps) {
return (
<View
@@ -33,12 +36,12 @@ export function AppliedFilters({
flexWrap: 'wrap',
}}
>
<CondOpMenu
<ConditionsOpMenu
conditionsOp={conditionsOp}
onCondOpChange={onCondOpChange}
filters={filters}
onChange={onConditionsOpChange}
conditions={conditions}
/>
{filters.map((filter: RuleConditionEntity, i: number) => (
{conditions.map((filter: RuleConditionEntity, i: number) => (
<FilterExpression
key={i}
customName={filter.customName}

View File

@@ -7,16 +7,16 @@ import { Text } from '../common/Text';
import { View } from '../common/View';
import { FieldSelect } from '../modals/EditRule';
export function CondOpMenu({
export function ConditionsOpMenu({
conditionsOp,
onCondOpChange,
filters,
onChange,
conditions,
}: {
conditionsOp: string;
onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
filters: RuleConditionEntity[];
onChange: (value: string, conditions: RuleConditionEntity[]) => void;
conditions: RuleConditionEntity[];
}) {
return filters.length > 1 ? (
return conditions.length > 1 ? (
<Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}>
<FieldSelect
style={{ display: 'inline-flex' }}
@@ -25,9 +25,7 @@ export function CondOpMenu({
['or', 'any'],
]}
value={conditionsOp}
onChange={(name: string, value: string) =>
onCondOpChange(value, filters)
}
onChange={(name: string, value: string) => onChange(value, conditions)}
/>
of:
</Text>

View File

@@ -58,7 +58,6 @@ export function FilterExpression({
type="bare"
disabled={customName != null}
onClick={() => setEditing(true)}
style={{ marginRight: -7 }}
>
<div style={{ paddingBlock: 1, paddingLeft: 5, paddingRight: 2 }}>
{customName ? (
@@ -84,8 +83,7 @@ export function FilterExpression({
style={{
width: 8,
height: 8,
margin: 5,
marginLeft: 3,
margin: '4px 0',
}}
/>
</Button>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { type TransactionFilterEntity } from 'loot-core/types/models';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { Stack } from '../common/Stack';
@@ -12,17 +13,17 @@ import {
} from './SavedFilterMenuButton';
export function FiltersStack({
filters,
conditions,
conditionsOp,
onUpdateFilter,
onDeleteFilter,
onClearFilters,
onReloadSavedFilter,
filterId,
filtersList,
onCondOpChange,
savedFilters,
onConditionsOpChange,
}: {
filters: RuleConditionEntity[];
conditions: RuleConditionEntity[];
conditionsOp: string;
onUpdateFilter: (
filter: RuleConditionEntity,
@@ -32,8 +33,8 @@ export function FiltersStack({
onClearFilters: () => void;
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
filterId: SavedFilter;
filtersList: RuleConditionEntity[];
onCondOpChange: () => void;
savedFilters: TransactionFilterEntity[];
onConditionsOpChange: () => void;
}) {
return (
<View>
@@ -44,20 +45,20 @@ export function FiltersStack({
align="flex-start"
>
<AppliedFilters
filters={filters}
conditions={conditions}
conditionsOp={conditionsOp}
onCondOpChange={onCondOpChange}
onConditionsOpChange={onConditionsOpChange}
onUpdate={onUpdateFilter}
onDelete={onDeleteFilter}
/>
<View style={{ flex: 1 }} />
<SavedFilterMenuButton
filters={filters}
conditions={conditions}
conditionsOp={conditionsOp}
filterId={filterId}
onClearFilters={onClearFilters}
onReloadSavedFilter={onReloadSavedFilter}
filtersList={filtersList}
savedFilters={savedFilters}
/>
</Stack>
</View>

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
import { type TransactionFilterEntity } from 'loot-core/types/models';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { SvgExpandArrow } from '../../icons/v0';
@@ -20,19 +21,19 @@ export type SavedFilter = {
};
export function SavedFilterMenuButton({
filters,
conditions,
conditionsOp,
filterId,
onClearFilters,
onReloadSavedFilter,
filtersList,
savedFilters,
}: {
filters: RuleConditionEntity[];
conditions: RuleConditionEntity[];
conditionsOp: string;
filterId: SavedFilter;
onClearFilters: () => void;
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
filtersList: RuleConditionEntity[];
savedFilters: TransactionFilterEntity[];
}) {
const [nameOpen, setNameOpen] = useState(false);
const [adding, setAdding] = useState(false);
@@ -62,7 +63,7 @@ export function SavedFilterMenuButton({
setAdding(false);
setMenuOpen(false);
savedFilter = {
conditions: filters,
conditions,
conditionsOp,
id: filterId.id,
name: filterId.name,
@@ -70,7 +71,7 @@ export function SavedFilterMenuButton({
};
const response = await sendCatch('filter-update', {
state: savedFilter,
filters: [...filtersList],
filters: [...savedFilters],
});
if (response.error) {
@@ -106,7 +107,7 @@ export function SavedFilterMenuButton({
async function onAddUpdate() {
if (adding) {
const newSavedFilter = {
conditions: filters,
conditions,
conditionsOp,
name,
status: 'saved',
@@ -114,7 +115,7 @@ export function SavedFilterMenuButton({
const response = await sendCatch('filter-create', {
state: newSavedFilter,
filters: [...filtersList],
filters: [...savedFilters],
});
if (response.error) {
@@ -140,7 +141,7 @@ export function SavedFilterMenuButton({
const response = await sendCatch('filter-update', {
state: updatedFilter,
filters: [...filtersList],
filters: [...savedFilters],
});
if (response.error) {
@@ -155,7 +156,7 @@ export function SavedFilterMenuButton({
return (
<View>
{filters.length > 0 && (
{conditions.length > 0 && (
<Button
type="bare"
style={{ marginTop: 10 }}

View File

@@ -31,7 +31,7 @@ export function Header({
onApply,
onUpdateFilter,
onDeleteFilter,
onCondOpChange,
onConditionsOpChange,
headerPrefixItems,
children,
}) {
@@ -164,11 +164,11 @@ export function Header({
align="flex-start"
>
<AppliedFilters
filters={filters}
conditions={filters}
onUpdate={onUpdateFilter}
onDelete={onDeleteFilter}
conditionsOp={conditionsOp}
onCondOpChange={onCondOpChange}
onConditionsOpChange={onConditionsOpChange}
/>
</View>
)}

View File

@@ -191,7 +191,7 @@ export function BarGraph({
.map(e => e.id);
const offBudgetAccounts = accounts.filter(f => f.offbudget).map(e => e.id);
const conditions = [
const filterConditions = [
...filters,
{ field, op: 'is', value: item.id, type: 'id' },
{
@@ -232,7 +232,7 @@ export function BarGraph({
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId: item.id,
},
});

View File

@@ -215,7 +215,7 @@ export function DonutGraph({
.map(e => e.id);
const offBudgetAccounts = accounts.filter(f => f.offbudget).map(e => e.id);
const conditions = [
const filterConditions = [
...filters,
{ field, op: 'is', value: item.id, type: 'id' },
{
@@ -256,7 +256,7 @@ export function DonutGraph({
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId: item.id,
},
});

View File

@@ -150,7 +150,7 @@ export function LineGraph({
.map(e => e.id);
const offBudgetAccounts = accounts.filter(f => f.offbudget).map(e => e.id);
const conditions = [
const filterConditions = [
...filters,
{ field, op: 'is', value: id, type: 'id' },
{
@@ -183,7 +183,7 @@ export function LineGraph({
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId: item.id,
},
});

View File

@@ -181,7 +181,7 @@ export function StackedBarGraph({
.map(e => e.id);
const offBudgetAccounts = accounts.filter(f => f.offbudget).map(e => e.id);
const conditions = [
const filterConditions = [
...filters,
{ field, op: 'is', value: id, type: 'id' },
{
@@ -214,7 +214,7 @@ export function StackedBarGraph({
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId: item.id,
},
});

View File

@@ -31,7 +31,7 @@ export function CashFlow() {
onApply: onApplyFilter,
onDelete: onDeleteFilter,
onUpdate: onUpdateFilter,
onCondOpChange,
onConditionsOpChange,
} = useFilters<RuleConditionEntity>();
const [allMonths, setAllMonths] = useState<null | Array<{
@@ -121,7 +121,7 @@ export function CashFlow() {
onUpdateFilter={onUpdateFilter}
onDeleteFilter={onDeleteFilter}
conditionsOp={conditionsOp}
onCondOpChange={onCondOpChange}
onConditionsOpChange={onConditionsOpChange}
headerPrefixItems={undefined}
>
<View

View File

@@ -55,7 +55,7 @@ export function CustomReport() {
onApply: onApplyFilter,
onDelete: onDeleteFilter,
onUpdate: onUpdateFilter,
onCondOpChange,
onConditionsOpChange,
} = useFilters();
const location = useLocation();
@@ -400,7 +400,7 @@ export function CustomReport() {
setGraphType(input.graphType);
onApplyFilter(null);
input.conditions.forEach(condition => onApplyFilter(condition));
onCondOpChange(input.conditionsOp);
onConditionsOpChange(input.conditionsOp);
};
const onChangeAppliedFilter = (filter, changedElement) => {
@@ -539,7 +539,7 @@ export function CustomReport() {
align="flex-start"
>
<AppliedFilters
filters={filters}
conditions={filters}
onUpdate={(oldFilter, newFilter) => {
setSessionReport(
'conditions',
@@ -556,8 +556,8 @@ export function CustomReport() {
onChangeAppliedFilter(deletedFilter, onDeleteFilter);
}}
conditionsOp={conditionsOp}
onCondOpChange={filter =>
onChangeAppliedFilter(filter, onCondOpChange)
onConditionsOpChange={filter =>
onChangeAppliedFilter(filter, onConditionsOpChange)
}
onUpdateChange={onReportChange}
/>

View File

@@ -30,7 +30,7 @@ export function NetWorth() {
onApply: onApplyFilter,
onDelete: onDeleteFilter,
onUpdate: onUpdateFilter,
onCondOpChange,
onConditionsOpChange,
} = useFilters();
const [allMonths, setAllMonths] = useState(null);
@@ -102,7 +102,7 @@ export function NetWorth() {
onUpdateFilter={onUpdateFilter}
onDeleteFilter={onDeleteFilter}
conditionsOp={conditionsOp}
onCondOpChange={onCondOpChange}
onConditionsOpChange={onConditionsOpChange}
/>
<View

View File

@@ -136,7 +136,9 @@ type CellProps = Omit<ComponentProps<typeof View>, 'children' | 'value'> & {
plain?: boolean;
exposed?: boolean;
children?: ReactNode | (() => ReactNode);
unexposedContent?: ReactNode;
unexposedContent?: (
props: ComponentProps<typeof UnexposedCellContent>,
) => ReactNode;
value?: string;
valueStyle?: CSSProperties;
onExpose?: (name: string) => void;
@@ -228,7 +230,9 @@ export function Cell({
}
}
>
{unexposedContent || (
{unexposedContent ? (
unexposedContent({ value, formatter })
) : (
<UnexposedCellContent value={value} formatter={formatter} />
)}
</View>

View File

@@ -1,5 +1,7 @@
import React, { useRef, useCallback, useLayoutEffect } from 'react';
import { useDispatch } from 'react-redux';
import { pushModal } from 'loot-core/client/actions';
import { send } from 'loot-core/src/platform/client/fetch';
import {
splitTransaction,
@@ -76,7 +78,6 @@ export function TransactionList({
dateFormat,
hideFraction,
addNotification,
pushModal,
renderEmpty,
onSort,
sortField,
@@ -85,9 +86,11 @@ export function TransactionList({
onRefetch,
onCloseAddTransaction,
onCreatePayee,
onApplyFilters,
}) {
const transactionsLatest = useRef();
const navigate = useNavigate();
const dispatch = useDispatch();
useLayoutEffect(() => {
transactionsLatest.current = transactions;
@@ -161,13 +164,19 @@ export function TransactionList({
});
const onNavigateToSchedule = useCallback(scheduleId => {
pushModal('schedule-edit', { id: scheduleId });
dispatch(pushModal('schedule-edit', { id: scheduleId }));
});
const onNavigateToFilteredTagView = useCallback(noteTag => {
const filterConditions = [
{ field: 'notes', op: 'contains', value: noteTag, type: 'string' },
];
onApplyFilters(filterConditions, true);
});
return (
<TransactionTable
ref={tableRef}
pushModal={pushModal}
transactions={allTransactions}
loadMoreTransactions={loadMoreTransactions}
accounts={accounts}
@@ -200,6 +209,7 @@ export function TransactionList({
style={{ backgroundColor: theme.tableBackground }}
onNavigateToTransferAccount={onNavigateToTransferAccount}
onNavigateToSchedule={onNavigateToSchedule}
onNavigateToFilteredTagView={onNavigateToFilteredTagView}
onSort={onSort}
sortField={sortField}
ascDesc={ascDesc}

View File

@@ -10,6 +10,7 @@ import React, {
useLayoutEffect,
useEffect,
} from 'react';
import { useDispatch } from 'react-redux';
import {
format as formatDate,
@@ -17,6 +18,7 @@ import {
isValid as isDateValid,
} from 'date-fns';
import { pushModal } from 'loot-core/client/actions';
import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
import {
getAccountsById,
@@ -57,6 +59,7 @@ import {
import { styles, theme } from '../../style';
import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete';
import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete';
import { NotesTagAutocomplete } from '../autocomplete/NotesTagAutocomplete';
import { PayeeAutocomplete } from '../autocomplete/PayeeAutocomplete';
import { Button } from '../common/Button';
import { Text } from '../common/Text';
@@ -413,7 +416,8 @@ function HeaderCell({
width={width}
name={id}
alignItems={alignItems}
unexposedContent={
value={value}
unexposedContent={({ value: cellValue }) => (
<Button
type="bare"
onClick={onClick}
@@ -427,7 +431,7 @@ function HeaderCell({
marginRight,
}}
>
<UnexposedCellContent value={value} />
<UnexposedCellContent value={cellValue} />
{icon === 'asc' && (
<SvgArrowDown width={10} height={10} style={{ marginLeft: 5 }} />
)}
@@ -435,22 +439,20 @@ function HeaderCell({
<SvgArrowUp width={10} height={10} style={{ marginLeft: 5 }} />
)}
</Button>
}
)}
/>
);
}
function PayeeCell({
id,
payeeId,
accountId,
payee,
focused,
inherited,
payees,
accounts,
valueStyle,
transaction,
payee,
transferAcct,
isPreview,
onEdit,
@@ -462,16 +464,12 @@ function PayeeCell({
}) {
const isCreatingPayee = useRef(false);
// Filter out the account we're currently in as it is not a valid transfer
accounts = accounts.filter(account => account.id !== accountId);
payees = payees.filter(payee => payee.transfer_acct !== accountId);
return (
<CustomCell
width="flex"
name="payee"
textAlign="flex"
value={payeeId}
value={payee?.id}
valueStyle={{
...valueStyle,
...(inherited && { color: theme.tableTextInactive }),
@@ -488,7 +486,8 @@ function PayeeCell({
isCreatingPayee.current = false;
}
}}
unexposedContent={
formatter={() => getPayeePretty(transaction, payee, transferAcct)}
unexposedContent={props => (
<>
<PayeeIcons
transaction={transaction}
@@ -496,12 +495,9 @@ function PayeeCell({
onNavigateToTransferAccount={onNavigateToTransferAccount}
onNavigateToSchedule={onNavigateToSchedule}
/>
<UnexposedCellContent
value={payeeId}
formatter={() => getPayeePretty(transaction, payee, transferAcct)}
/>
<UnexposedCellContent {...props} />
</>
}
)}
>
{({
onBlur,
@@ -515,7 +511,7 @@ function PayeeCell({
<PayeeAutocomplete
payees={payees}
accounts={accounts}
value={payeeId}
value={payee?.id}
shouldSaveFromKey={shouldSaveFromKey}
inputProps={{
onBlur,
@@ -527,7 +523,7 @@ function PayeeCell({
focused={true}
onUpdate={(id, value) => onUpdate?.(value)}
onSelect={onSave}
onManagePayees={() => onManagePayees(payeeId)}
onManagePayees={() => onManagePayees(payee?.id)}
menuPortalTarget={undefined}
/>
);
@@ -641,8 +637,10 @@ const Transaction = memo(function Transaction(props) {
onToggleSplit,
onNavigateToTransferAccount,
onNavigateToSchedule,
onNavigateToFilteredTagView,
} = props;
const dispatch = useDispatch();
const dispatchSelected = useSelectedDispatch();
const [prevShowZero, setPrevShowZero] = useState(showZeroInDeposit);
@@ -685,13 +683,15 @@ const Transaction = memo(function Transaction(props) {
) {
if (showReconciliationWarning === false) {
setShowReconciliationWarning(true);
props.pushModal('confirm-transaction-edit', {
onConfirm: () => {
setShowReconciliationWarning(false);
onUpdateAfterConfirm(name, value);
},
confirmReason: 'editReconciled',
});
dispatch(
pushModal('confirm-transaction-edit', {
onConfirm: () => {
setShowReconciliationWarning(false);
onUpdateAfterConfirm(name, value);
},
confirmReason: 'editReconciled',
}),
);
}
} else {
onUpdateAfterConfirm(name, value);
@@ -700,12 +700,14 @@ const Transaction = memo(function Transaction(props) {
// Allow un-reconciling (unlocking) transactions
if (name === 'cleared' && transaction.reconciled) {
props.pushModal('confirm-transaction-edit', {
onConfirm: () => {
onUpdateAfterConfirm('reconciled', false);
},
confirmReason: 'unlockReconciled',
});
dispatch(
pushModal('confirm-transaction-edit', {
onConfirm: () => {
onUpdateAfterConfirm('reconciled', false);
},
confirmReason: 'unlockReconciled',
}),
);
}
}
@@ -713,7 +715,7 @@ const Transaction = memo(function Transaction(props) {
const newTransaction = { ...transaction, [name]: value };
// Don't change the note to an empty string if it's null (since they are both rendered the same)
if (name === 'note' && value === '' && transaction.note == null) {
if (name === 'notes' && value === '' && transaction.notes == null) {
return;
}
@@ -977,15 +979,14 @@ const Transaction = memo(function Transaction(props) {
<PayeeCell
/* Payee field for all transactions */
id={id}
payeeId={payeeId}
accountId={accountId}
payee={payee}
focused={focusedField === 'payee'}
inherited={inheritedFields && inheritedFields.has('payee')}
payees={payees}
accounts={accounts}
/* Filter out the account we're currently in as it is not a valid transfer */
accounts={accounts.filter(account => account.id !== accountId)}
payees={payees.filter(payee => payee.transfer_acct !== accountId)}
valueStyle={valueStyle}
transaction={transaction}
payee={payee}
transferAcct={transferAcct}
importedPayee={importedPayee}
isPreview={isPreview}
@@ -1002,20 +1003,42 @@ const Transaction = memo(function Transaction(props) {
/* Notes field for all transactions */
<Cell name="notes" width="flex" />
) : (
<InputCell
width="flex"
<CustomCell
name="notes"
width="flex"
textAlign="flex"
exposed={focusedField === 'notes'}
focused={focusedField === 'notes'}
value={notes || ''}
valueStyle={valueStyle}
formatter={value =>
notesTagFormatter(value, onNavigateToFilteredTagView)
}
exposed={focusedField === 'notes'}
onExpose={name => !isPreview && onEdit(id, name)}
inputProps={{
value: notes || '',
onUpdate: onUpdate.bind(null, 'notes'),
valueStyle={valueStyle}
onUpdate={value => {
onUpdate('notes', value);
}}
/>
>
{({
onBlur,
onKeyDown,
onUpdate,
onSave,
shouldSaveFromKey,
inputStyle,
}) => (
<NotesTagAutocomplete
value={notes || ''}
shouldSaveFromKey={shouldSaveFromKey}
inputProps={{
onBlur,
onKeyDown,
style: inputStyle,
}}
onUpdate={(_, inputValue) => onUpdate(inputValue)}
onSelect={(_, inputValue) => onSave(inputValue)}
/>
)}
</CustomCell>
)}
{isPreview ? (
@@ -1392,6 +1415,7 @@ function NewTransaction({
onCreatePayee,
onNavigateToTransferAccount,
onNavigateToSchedule,
onNavigateToFilteredTagView,
balance,
}) {
const error = transactions[0].error;
@@ -1445,6 +1469,7 @@ function NewTransaction({
style={{ marginTop: -1 }}
onNavigateToTransferAccount={onNavigateToTransferAccount}
onNavigateToSchedule={onNavigateToSchedule}
onNavigateToFilteredTagView={onNavigateToFilteredTagView}
balance={balance}
/>
))}
@@ -1524,6 +1549,14 @@ function TransactionTableInner({
[props.onCloseAddTransaction, props.onNavigateToSchedule],
);
const onNavigateToFilteredTagView = useCallback(
noteTag => {
props.onCloseAddTransaction();
props.onNavigateToFilteredTagView(noteTag);
},
[props.onCloseAddTransaction, props.onNavigateToFilteredTagView],
);
useEffect(() => {
if (!isAddingPrev && props.isAdding) {
newNavigator.onEdit('temp', 'date');
@@ -1626,7 +1659,7 @@ function TransactionTableInner({
onToggleSplit={props.onToggleSplit}
onNavigateToTransferAccount={onNavigateToTransferAccount}
onNavigateToSchedule={onNavigateToSchedule}
pushModal={props.pushModal}
onNavigateToFilteredTagView={onNavigateToFilteredTagView}
/>
</>
);
@@ -1684,6 +1717,7 @@ function TransactionTableInner({
onCreatePayee={props.onCreatePayee}
onNavigateToTransferAccount={onNavigateToTransferAccount}
onNavigateToSchedule={onNavigateToSchedule}
onNavigateToFilteredTagView={onNavigateToFilteredTagView}
onDistributeRemainder={props.onDistributeRemainder}
balance={
props.transactions?.length > 0
@@ -2172,3 +2206,45 @@ export const TransactionTable = forwardRef((props, ref) => {
});
TransactionTable.displayName = 'TransactionTable';
function notesTagFormatter(value, onNoteTagClick) {
const words = value?.split(' ') || [];
return (
<>
{words.map((word, i, arr) => {
const separator = arr.length - 1 === i ? '' : ' ';
if (word.startsWith('#') && word.length > 1) {
return (
<span key={`${word}-${i}`}>
<Button
type="bare"
key={i}
style={{
display: 'inline-flex',
padding: '3px 7px',
borderRadius: 16,
userSelect: 'none',
backgroundColor: theme.noteTagBackground,
color: theme.noteTagText,
cursor: 'pointer',
}}
hoveredStyle={{
backgroundColor: theme.noteTagBackgroundHover,
color: theme.noteTagText,
}}
onClick={e => {
e.stopPropagation();
onNoteTagClick?.(word);
}}
>
{word}
</Button>
{separator}
</span>
);
}
return `${word}${separator}`;
})}
</>
);
}

View File

@@ -41,7 +41,7 @@ export function useFilters<T>(initialFilters: T[] = []) {
[setFilters],
);
const onCondOpChange = useCallback(
const onConditionsOpChange = useCallback(
condOp => {
setConditionsOp(condOp);
},
@@ -56,8 +56,16 @@ export function useFilters<T>(initialFilters: T[] = []) {
onApply,
onUpdate,
onDelete,
onCondOpChange,
onConditionsOpChange,
}),
[filters, saved, onApply, onUpdate, onDelete, onCondOpChange, conditionsOp],
[
filters,
saved,
onApply,
onUpdate,
onDelete,
onConditionsOpChange,
conditionsOp,
],
);
}

View File

@@ -191,3 +191,7 @@ export const reportsBlue = colorPalette.blue400;
export const reportsGreen = colorPalette.green400;
export const reportsLabel = pageText;
export const reportsInnerLabel = colorPalette.navy800;
export const noteTagBackground = colorPalette.purple700;
export const noteTagBackgroundHover = colorPalette.purple500;
export const noteTagText = colorPalette.purple100;

View File

@@ -190,3 +190,7 @@ export const reportsBlue = colorPalette.blue400;
export const reportsGreen = colorPalette.green400;
export const reportsLabel = colorPalette.navy900;
export const reportsInnerLabel = colorPalette.navy800;
export const noteTagBackground = colorPalette.purple100;
export const noteTagBackgroundHover = colorPalette.purple150;
export const noteTagText = colorPalette.purple700;

View File

@@ -193,3 +193,7 @@ export const reportsBlue = colorPalette.blue400;
export const reportsGreen = colorPalette.green400;
export const reportsLabel = colorPalette.navy900;
export const reportsInnerLabel = colorPalette.navy800;
export const noteTagBackground = colorPalette.purple100;
export const noteTagBackgroundHover = colorPalette.purple150;
export const noteTagText = colorPalette.purple700;

View File

@@ -193,3 +193,7 @@ export const reportsBlue = colorPalette.blue400;
export const reportsGreen = colorPalette.green400;
export const reportsLabel = pageText;
export const reportsInnerLabel = colorPalette.navy800;
export const noteTagBackground = colorPalette.purple800;
export const noteTagBackgroundHover = colorPalette.purple600;
export const noteTagText = colorPalette.purple100;

View File

@@ -0,0 +1,6 @@
---
category: Features
authors: [joel-jeremy]
---
Format notes that starts with # as clickable tags.