Compare commits

..

5 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
a61d66f23f Modal migrations 2024-10-18 23:09:51 -07:00
Joel Jeremy Marquez
c1b475777b More modals migration 2024-10-18 17:43:40 -07:00
Joel Jeremy Marquez
b1a83123f5 Modal changes 2024-10-18 05:27:18 -07:00
Joel Jeremy Marquez
71eb54e9f3 More initial changes 2024-10-18 04:58:05 -07:00
Joel Jeremy Marquez
3e2f96a32c Initial commit - unfinished 2024-10-18 04:56:34 -07:00
438 changed files with 11790 additions and 8491 deletions

View File

@@ -1,55 +0,0 @@
name: /update-vrt
on:
issue_comment:
types: [ created ]
permissions:
pull-requests: write
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
update-vrt:
name: Update VRT
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '/update-vrt')
container:
image: mcr.microsoft.com/playwright:v1.41.1-jammy
steps:
- name: Get PR branch
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
id: comment-branch
- uses: actions/checkout@v4
with:
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
ref: ${{ steps.comment-branch.outputs.head_ref }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Wait for Netlify build to finish
id: netlify
env:
COMMIT_SHA: ${{ steps.comment-branch.outputs.head_sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./.github/actions/netlify-wait-for-build
- name: Run VRT Tests on Netlify URL
run: yarn vrt --update-snapshots
env:
E2E_START_URL: ${{ steps.netlify.outputs.url }}
- name: Commit and push changes
run: |
git config --system --add safe.directory "*"
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add "**/*.png"
if git diff --staged --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update VRT"
git push origin HEAD:${{ steps.comment-branch.outputs.head_ref }}

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "24.11.0",
"version": "24.10.1",
"license": "MIT",
"description": "An API for Actual",
"engines": {

View File

@@ -30,7 +30,7 @@ test.describe.parallel('Reports', () => {
test('loads net worth and cash flow reports', async () => {
const reports = await reportsPage.getAvailableReportList();
expect(reports).toEqual(['Net Worth', 'Cash Flow', 'Monthly Spending']);
expect(reports).toEqual(['Net Worth', 'Cash Flow']);
await expect(page).toMatchThemeScreenshots();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -1,5 +1,7 @@
import { type CSSObject } from '@emotion/css/dist/declarations/src/create-instance';
import { type State } from './src/state';
// Allow images to be imported
declare module '*.png';
@@ -7,3 +9,8 @@ declare module 'react' {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-empty-object-type
interface CSSProperties extends CSSObject {}
}
declare module 'react-redux' {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/consistent-type-definitions
interface DefaultRootState extends State {}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/web",
"version": "24.11.0",
"version": "24.10.1",
"license": "MIT",
"files": [
"build"

View File

@@ -1,8 +1,9 @@
// @ts-strict-ignore
import { type ReactNode, createContext, useContext } from 'react';
import { useWindowSize } from 'usehooks-ts';
import { useViewportSize } from '@react-aria/utils';
import { breakpoints } from '../../tokens';
import { breakpoints } from './tokens';
type TResponsiveContext = {
atLeastMediumWidth: boolean;
@@ -14,14 +15,20 @@ type TResponsiveContext = {
width: number;
};
const ResponsiveContext = createContext<TResponsiveContext | undefined>(
undefined,
);
const ResponsiveContext = createContext<TResponsiveContext>(null);
export function ResponsiveProvider(props: { children: ReactNode }) {
const { height, width } = useWindowSize({
debounceDelay: 250,
});
/*
* Ensure we render on every viewport size change,
* even though we're interested in document.documentElement.client<Width|Height>
* clientWidth/Height are the document size, do not change on pinch-zoom,
* and are what our `min-width` media queries are reading
* Viewport size changes on pinch-zoom, which may be useful later when dealing with on-screen keyboards
*/
useViewportSize();
const height = document.documentElement.clientHeight;
const width = document.documentElement.clientWidth;
// Possible view modes: narrow, small, medium, wide
// To check if we're at least small width, check !isNarrowWidth
@@ -45,9 +52,5 @@ export function ResponsiveProvider(props: { children: ReactNode }) {
}
export function useResponsive() {
const context = useContext(ResponsiveContext);
if (!context) {
throw new Error('useResponsive must be used within a ResponsiveProvider');
}
return context;
return useContext(ResponsiveContext);
}

View File

@@ -1,5 +1,4 @@
import { initBackend as initSQLBackend } from 'absurd-sql/dist/indexeddb-main-thread';
import { registerSW } from 'virtual:pwa-register';
import * as Platform from 'loot-core/src/client/platform';
@@ -40,19 +39,6 @@ function createBackendWorker() {
createBackendWorker();
let isUpdateReadyForDownload = false;
let markUpdateReadyForDownload;
const isUpdateReadyForDownloadPromise = new Promise(resolve => {
markUpdateReadyForDownload = () => {
isUpdateReadyForDownload = true;
resolve(true);
};
});
const updateSW = registerSW({
immediate: true,
onNeedRefresh: markUpdateReadyForDownload,
});
global.Actual = {
IS_DEV,
ACTUAL_VERSION,
@@ -154,14 +140,7 @@ global.Actual = {
window.open(url, '_blank');
},
onEventFromMain: () => {},
isUpdateReadyForDownload: () => isUpdateReadyForDownload,
waitForUpdateReadyForDownload: () => isUpdateReadyForDownloadPromise,
applyAppUpdate: async () => {
updateSW();
// Wait for the app to reload
await new Promise(() => {});
},
applyAppUpdate: () => {},
updateAppMenu: () => {},
ipcConnect: () => {},

View File

@@ -12,13 +12,6 @@ import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import {
closeBudget,
loadBudget,
loadGlobalPrefs,
setAppState,
sync,
} from 'loot-core/client/actions';
import { SpreadsheetProvider } from 'loot-core/client/SpreadsheetProvider';
import * as Platform from 'loot-core/src/client/platform';
import {
@@ -28,6 +21,14 @@ import {
import { useMetadataPref } from '../hooks/useMetadataPref';
import { installPolyfills } from '../polyfills';
import { ResponsiveProvider } from '../ResponsiveProvider';
import {
closeBudget,
loadBudget,
loadGlobalPrefs,
setAppState,
sync,
} from '../state/actions';
import { styles, hasHiddenScrollbars, ThemeStyle, useTheme } from '../style';
import { ExposeNavigate } from '../util/router-tools';
@@ -39,7 +40,6 @@ import { FatalError } from './FatalError';
import { FinancesApp } from './FinancesApp';
import { ManagementApp } from './manager/ManagementApp';
import { Modals } from './Modals';
import { ResponsiveProvider } from './responsive/ResponsiveProvider';
import { ScrollProvider } from './ScrollProvider';
import { SidebarProvider } from './sidebar/SidebarProvider';
import { UpdateNotification } from './UpdateNotification';
@@ -51,20 +51,8 @@ function AppInner() {
const { showBoundary: showErrorBoundary } = useErrorBoundary();
const dispatch = useDispatch();
const maybeUpdate = async <T,>(cb?: () => T): Promise<T> => {
if (global.Actual.isUpdateReadyForDownload()) {
dispatch(
setAppState({
loadingText: t('Downloading and applying update...'),
}),
);
await global.Actual.applyAppUpdate();
}
return cb?.();
};
async function init() {
const socketName = await maybeUpdate(() => global.Actual.getServerSocket());
const socketName = await global.Actual.getServerSocket();
dispatch(
setAppState({
@@ -98,16 +86,14 @@ function AppInner() {
loadingText: t('Retrieving remote files...'),
}),
);
const files = await send('get-remote-files');
if (files) {
const remoteFile = files.find(f => f.fileId === cloudFileId);
if (remoteFile && remoteFile.deleted) {
dispatch(closeBudget());
send('get-remote-files').then(files => {
if (files) {
const remoteFile = files.find(f => f.fileId === cloudFileId);
if (remoteFile && remoteFile.deleted) {
dispatch(closeBudget());
}
}
}
await maybeUpdate();
});
}
}

View File

@@ -3,8 +3,7 @@ import { Trans } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useTransition, animated } from 'react-spring';
import { type State } from 'loot-core/src/client/state-types';
import { type State } from '../state';
import { theme, styles } from '../style';
import { AnimatedRefresh } from './AnimatedRefresh';

View File

@@ -4,6 +4,7 @@ import { SvgPencil1 } from '../icons/v2';
import { theme } from '../style';
import { Button } from './common/Button2';
import { InitialFocus } from './common/InitialFocus';
import { Input } from './common/Input';
import { View } from './common/View';
@@ -32,24 +33,24 @@ export function EditablePageHeaderTitle({
if (isEditing) {
return (
<Input
autoFocus
autoSelect
defaultValue={title}
onEnter={e => onSaveValue(e.currentTarget.value)}
onBlur={e => onSaveValue(e.target.value)}
onEscape={() => setIsEditing(false)}
style={{
fontSize: 25,
fontWeight: 500,
marginTop: -3,
marginBottom: -3,
marginLeft: -6,
paddingTop: 2,
paddingBottom: 2,
width: Math.max(20, title.length) + 'ch',
}}
/>
<InitialFocus>
<Input
defaultValue={title}
onEnter={e => onSaveValue(e.currentTarget.value)}
onBlur={e => onSaveValue(e.target.value)}
onEscape={() => setIsEditing(false)}
style={{
fontSize: 25,
fontWeight: 500,
marginTop: -3,
marginBottom: -3,
marginLeft: -6,
paddingTop: 2,
paddingBottom: 2,
width: Math.max(20, title.length) + 'ch',
}}
/>
</InitialFocus>
);
}

View File

@@ -10,14 +10,15 @@ import {
useHref,
} from 'react-router-dom';
import { addNotification, sync } from 'loot-core/client/actions';
import { type State } from 'loot-core/src/client/state-types';
import * as undo from 'loot-core/src/platform/client/undo';
import { useAccounts } from '../hooks/useAccounts';
import { useLocalPref } from '../hooks/useLocalPref';
import { useMetaThemeColor } from '../hooks/useMetaThemeColor';
import { useNavigate } from '../hooks/useNavigate';
import { useResponsive } from '../ResponsiveProvider';
import { type State } from '../state';
import { addNotification, sync } from '../state/actions';
import { theme } from '../style';
import { getIsOutdated, getLatestVersion } from '../util/versions';
@@ -33,7 +34,6 @@ import { ManagePayeesPage } from './payees/ManagePayeesPage';
import { Reports } from './reports';
import { LoadingIndicator } from './reports/LoadingIndicator';
import { NarrowAlternate, WideComponent } from './responsive';
import { useResponsive } from './responsive/ResponsiveProvider';
import { Settings } from './settings';
import { FloatableSidebar } from './sidebar';
import { Titlebar } from './Titlebar';
@@ -100,29 +100,6 @@ export function FinancesApp() {
}, 100);
}, []);
useEffect(() => {
async function run() {
await global.Actual.waitForUpdateReadyForDownload();
dispatch(
addNotification({
type: 'message',
title: t('A new version of Actual is available!'),
message: t('Click the button below to reload and apply the update.'),
sticky: true,
id: 'update-reload-notification',
button: {
title: t('Update now'),
action: async () => {
await global.Actual.applyAppUpdate();
},
},
}),
);
}
run();
}, []);
useEffect(() => {
async function run() {
const latestVersion = await getLatestVersion();

View File

@@ -6,11 +6,10 @@ import { useLocation } from 'react-router-dom';
import { useToggle } from 'usehooks-ts';
import { openDocsForCurrentPage } from 'loot-core/client/actions';
import { pushModal } from 'loot-core/client/actions/modals';
import { useFeatureFlag } from '../hooks/useFeatureFlag';
import { SvgHelp } from '../icons/v2/Help';
import { pushModal } from '../state/actions';
import { openUrl } from '../util/router-tools';
import { Button } from './common/Button2';
import { Menu } from './common/Menu';
@@ -46,6 +45,26 @@ const HelpButton = forwardRef<HTMLButtonElement, HelpButtonProps>(
HelpButton.displayName = 'HelpButton';
const getPageDocs = (page: string) => {
switch (page) {
case '/budget':
return 'https://actualbudget.org/docs/getting-started/envelope-budgeting';
case '/reports':
return 'https://actualbudget.org/docs/reports/';
case '/schedules':
return 'https://actualbudget.org/docs/schedules';
case '/payees':
return 'https://actualbudget.org/docs/transactions/payees';
case '/rules':
return 'https://actualbudget.org/docs/budgeting/rules';
case '/settings':
return 'https://actualbudget.org/docs/settings';
default:
// All pages under /accounts, plus any missing pages
return 'https://actualbudget.org/docs';
}
};
export const HelpMenu = () => {
const showGoalTemplates = useFeatureFlag('goalTemplatesEnabled');
const { t } = useTranslation();
@@ -58,7 +77,7 @@ export const HelpMenu = () => {
const handleItemSelect = (item: HelpMenuItem) => {
switch (item) {
case 'docs':
dispatch(openDocsForCurrentPage());
openUrl(getPageDocs(page));
break;
case 'keyboard-shortcuts':
dispatch(pushModal('keyboard-shortcuts'));

View File

@@ -3,9 +3,8 @@ import React, { useState, useEffect, useRef, type CSSProperties } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { type State } from 'loot-core/src/client/state-types';
import { useActions } from '../hooks/useActions';
import { type State } from '../state';
import { theme, styles } from '../style';
import { Button } from './common/Button2';

View File

@@ -9,8 +9,6 @@ import React, {
} from 'react';
import { useDispatch } from 'react-redux';
import { pushModal } from 'loot-core/src/client/actions/modals';
import { initiallyLoadPayees } from 'loot-core/src/client/actions/queries';
import { send } from 'loot-core/src/platform/client/fetch';
import * as undo from 'loot-core/src/platform/client/undo';
import { getNormalisedString } from 'loot-core/src/shared/normalisation';
@@ -23,6 +21,7 @@ import { useCategories } from '../hooks/useCategories';
import { usePayees } from '../hooks/usePayees';
import { useSchedules } from '../hooks/useSchedules';
import { useSelected, SelectedProvider } from '../hooks/useSelected';
import { initiallyLoadPayees, pushModal } from '../state/actions';
import { theme } from '../style';
import { Button } from './common/Button2';
@@ -204,13 +203,6 @@ export function ManageRules({
setLoading(false);
}
async function onDeleteRule(id: string) {
setLoading(true);
await send('rule-delete', id);
await loadRules();
setLoading(false);
}
const onEditRule = useCallback(rule => {
dispatch(
pushModal('edit-rule', {
@@ -294,7 +286,7 @@ export function ManageRules({
<Search
placeholder="Filter rules..."
value={filter}
onChangeValue={onSearchChange}
onChange={onSearchChange}
/>
</View>
<View style={{ flex: 1 }}>
@@ -313,7 +305,6 @@ export function ManageRules({
hoveredRule={hoveredRule}
onHover={onHover}
onEditRule={onEditRule}
onDeleteRule={rule => onDeleteRule(rule.id)}
/>
)}
</SimpleTable>

View File

@@ -3,14 +3,10 @@ import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { closeModal } from 'loot-core/client/actions';
import { send } from 'loot-core/src/platform/client/fetch';
import * as monthUtils from 'loot-core/src/shared/months';
import { useMetadataPref } from '../hooks/useMetadataPref';
import { useModalState } from '../hooks/useModalState';
import { closeModal } from '../state/actions';
import { ModalTitle, ModalHeader } from './common/Modal';
import { AccountAutocompleteModal } from './modals/AccountAutocompleteModal';
import { AccountMenuModal } from './modals/AccountMenuModal';
import { BudgetListModal } from './modals/BudgetListModal';
@@ -67,7 +63,6 @@ import { DiscoverSchedules } from './schedules/DiscoverSchedules';
import { PostsOfflineNotification } from './schedules/PostsOfflineNotification';
import { ScheduleDetails } from './schedules/ScheduleDetails';
import { ScheduleLink } from './schedules/ScheduleLink';
import { NamespaceContext } from './spreadsheet/NamespaceContext';
export function Modals() {
const location = useLocation();
@@ -84,520 +79,244 @@ export function Modals() {
const modals = modalStack
.map(({ name, options }) => {
switch (name) {
case 'goal-templates':
return budgetId ? <GoalTemplateModal key={name} /> : null;
case GoalTemplateModal.modalName:
return budgetId ? (
<GoalTemplateModal key={name} name={name} {...options} />
) : null;
case 'keyboard-shortcuts':
case KeyboardShortcutModal.modalName:
// don't show the hotkey help modal when a budget is not open
return budgetId ? <KeyboardShortcutModal key={name} /> : null;
return budgetId ? (
<KeyboardShortcutModal key={name} name={name} {...options} />
) : null;
// Must be `case ImportTransactionsModal.modalName` once component is migrated to TS
case 'import-transactions':
return <ImportTransactionsModal key={name} options={options} />;
case 'add-account':
return (
<CreateAccountModal
key={name}
upgradingAccountId={options?.upgradingAccountId}
/>
<ImportTransactionsModal key={name} name={name} {...options} />
);
case 'add-local-account':
return <CreateLocalAccountModal key={name} />;
case CreateAccountModal.modalName:
return <CreateAccountModal key={name} name={name} {...options} />;
case 'close-account':
case CreateLocalAccountModal.modalName:
return (
<CloseAccountModal
key={name}
account={options.account}
balance={options.balance}
canDelete={options.canDelete}
/>
<CreateLocalAccountModal key={name} name={name} {...options} />
);
case CloseAccountModal.modalName:
return <CloseAccountModal key={name} name={name} {...options} />;
// Must be `case SelectLinkedAccountsModal.modalName` once component is migrated to TS
case 'select-linked-accounts':
return (
<SelectLinkedAccountsModal
key={name}
externalAccounts={options.accounts}
requisitionId={options.requisitionId}
syncSource={options.syncSource}
/>
<SelectLinkedAccountsModal key={name} name={name} {...options} />
);
case 'confirm-category-delete':
case ConfirmCategoryDeleteModal.modalName:
return (
<ConfirmCategoryDeleteModal
key={name}
category={options.category}
group={options.group}
onDelete={options.onDelete}
/>
<ConfirmCategoryDeleteModal key={name} name={name} {...options} />
);
case 'confirm-unlink-account':
case ConfirmUnlinkAccountModal.modalName:
return (
<ConfirmUnlinkAccountModal
key={name}
accountName={options.accountName}
onUnlink={options.onUnlink}
/>
<ConfirmUnlinkAccountModal key={name} name={name} {...options} />
);
case 'confirm-transaction-edit':
case ConfirmTransactionEditModal.modalName:
return (
<ConfirmTransactionEditModal
key={name}
onCancel={options.onCancel}
onConfirm={options.onConfirm}
confirmReason={options.confirmReason}
/>
<ConfirmTransactionEditModal key={name} name={name} {...options} />
);
case 'confirm-transaction-delete':
case ConfirmTransactionDeleteModal.modalName:
return (
<ConfirmTransactionDeleteModal
key={name}
message={options.message}
onConfirm={options.onConfirm}
name={name}
{...options}
/>
);
case 'load-backup':
return (
<LoadBackupModal
key={name}
watchUpdates
budgetId={options.budgetId}
backupDisabled={false}
/>
);
case LoadBackupModal.modalName:
return <LoadBackupModal key={name} name={name} {...options} />;
case 'manage-rules':
return <ManageRulesModal key={name} payeeId={options?.payeeId} />;
case ManageRulesModal.modalName:
return <ManageRulesModal key={name} name={name} {...options} />;
// Must be `case EditRuleModal.modalName` once component is migrated to TS
case 'edit-rule':
return (
<EditRuleModal
key={name}
defaultRule={options.rule}
onSave={options.onSave}
/>
);
return <EditRuleModal key={name} name={name} {...options} />;
// Must be `case MergeUnusedPayeesModal.modalName` once component is migrated to TS
case 'merge-unused-payees':
return <MergeUnusedPayeesModal key={name} name={name} {...options} />;
case GoCardlessInitialiseModal.modalName:
return (
<MergeUnusedPayeesModal
key={name}
payeeIds={options.payeeIds}
targetPayeeId={options.targetPayeeId}
/>
<GoCardlessInitialiseModal key={name} name={name} {...options} />
);
case 'gocardless-init':
case SimpleFinInitialiseModal.modalName:
return (
<GoCardlessInitialiseModal
key={name}
onSuccess={options.onSuccess}
/>
<SimpleFinInitialiseModal key={name} name={name} {...options} />
);
case 'simplefin-init':
case GoCardlessExternalMsgModal.modalName:
return (
<SimpleFinInitialiseModal
key={name}
onSuccess={options.onSuccess}
/>
<GoCardlessExternalMsgModal key={name} name={name} {...options} />
);
case 'gocardless-external-msg':
case CreateEncryptionKeyModal.modalName:
return (
<GoCardlessExternalMsgModal
key={name}
onMoveExternal={options.onMoveExternal}
onClose={() => {
options.onClose?.();
send('gocardless-poll-web-token-stop');
}}
onSuccess={options.onSuccess}
/>
<CreateEncryptionKeyModal key={name} name={name} {...options} />
);
case 'create-encryption-key':
return <CreateEncryptionKeyModal key={name} options={options} />;
case 'fix-encryption-key':
return <FixEncryptionKeyModal key={name} options={options} />;
case FixEncryptionKeyModal.modalName:
return <FixEncryptionKeyModal key={name} name={name} {...options} />;
// Must be `case EditFieldModal.modalName` once component is migrated to TS
case 'edit-field':
return <EditFieldModal key={name} name={name} {...options} />;
case CategoryAutocompleteModal.modalName:
return (
<EditFieldModal
key={name}
name={options.name}
onSubmit={options.onSubmit}
onClose={options.onClose}
/>
<CategoryAutocompleteModal key={name} name={name} {...options} />
);
case 'category-autocomplete':
case AccountAutocompleteModal.modalName:
return (
<CategoryAutocompleteModal
key={name}
autocompleteProps={{
value: null,
onSelect: options.onSelect,
categoryGroups: options.categoryGroups,
showHiddenCategories: options.showHiddenCategories,
}}
month={options.month}
onClose={options.onClose}
/>
<AccountAutocompleteModal key={name} name={name} {...options} />
);
case 'account-autocomplete':
return (
<AccountAutocompleteModal
key={name}
autocompleteProps={{
value: null,
onSelect: options.onSelect,
includeClosedAccounts: options.includeClosedAccounts,
}}
onClose={options.onClose}
/>
);
case 'payee-autocomplete':
return (
<PayeeAutocompleteModal
key={name}
autocompleteProps={{
value: null,
onSelect: options.onSelect,
}}
onClose={options.onClose}
/>
);
case PayeeAutocompleteModal.modalName:
return <PayeeAutocompleteModal key={name} name={name} {...options} />;
// Create a new component for this modal
case 'new-category':
return (
<SingleInputModal
key={name}
name={name}
Header={props => (
<ModalHeader
{...props}
title={<ModalTitle title="New Category" shrinkOnOverflow />}
/>
)}
inputPlaceholder="Category name"
buttonText="Add"
onValidate={options.onValidate}
onSubmit={options.onSubmit}
/>
);
return <SingleInputModal key={name} name={name} {...options} />;
// Create a new component for this modal
case 'new-category-group':
return <SingleInputModal key={name} name={name} {...options} />;
case EnvelopeBudgetSummaryModal.modalName:
return (
<SingleInputModal
key={name}
name={name}
Header={props => (
<ModalHeader
{...props}
title={
<ModalTitle title="New Category Group" shrinkOnOverflow />
}
/>
)}
inputPlaceholder="Category group name"
buttonText="Add"
onValidate={options.onValidate}
onSubmit={options.onSubmit}
/>
<EnvelopeBudgetSummaryModal key={name} name={name} {...options} />
);
case 'envelope-budget-summary':
case TrackingBudgetSummaryModal.modalName:
return (
<NamespaceContext.Provider
key={name}
value={monthUtils.sheetForMonth(options.month)}
>
<EnvelopeBudgetSummaryModal
key={name}
month={options.month}
onBudgetAction={options.onBudgetAction}
/>
</NamespaceContext.Provider>
);
case 'tracking-budget-summary':
return (
<TrackingBudgetSummaryModal key={name} month={options.month} />
<TrackingBudgetSummaryModal key={name} name={name} {...options} />
);
// Must be `case ScheduleDetails.modalName` once component is migrated to TS
case 'schedule-edit':
return (
<ScheduleDetails
key={name}
id={options?.id || null}
transaction={options?.transaction || null}
/>
);
return <ScheduleDetails key={name} name={name} {...options} />;
case 'schedule-link':
return (
<ScheduleLink
key={name}
transactionIds={options?.transactionIds}
getTransaction={options?.getTransaction}
accountName={options?.accountName}
onScheduleLinked={options?.onScheduleLinked}
/>
);
case ScheduleLink.modalName:
return <ScheduleLink key={name} name={name} {...options} />;
case 'schedules-discover':
return <DiscoverSchedules key={name} />;
case DiscoverSchedules.modalName:
return <DiscoverSchedules key={name} name={name} {...options} />;
// Must be `case PostsOfflineNotification.modalName` once component is migrated to TS
case 'schedule-posts-offline-notification':
return <PostsOfflineNotification key={name} />;
return <PostsOfflineNotification key={name} name={name} />;
case 'account-menu':
case AccountMenuModal.modalName:
return <AccountMenuModal key={name} name={name} {...options} />;
case CategoryMenuModal.modalName:
return <CategoryMenuModal key={name} name={name} {...options} />;
case EnvelopeBudgetMenuModal.modalName:
return (
<AccountMenuModal
key={name}
accountId={options.accountId}
onSave={options.onSave}
onEditNotes={options.onEditNotes}
onCloseAccount={options.onCloseAccount}
onReopenAccount={options.onReopenAccount}
onClose={options.onClose}
/>
<EnvelopeBudgetMenuModal key={name} name={name} {...options} />
);
case 'category-menu':
case TrackingBudgetMenuModal.modalName:
return (
<CategoryMenuModal
key={name}
categoryId={options.categoryId}
onSave={options.onSave}
onEditNotes={options.onEditNotes}
onDelete={options.onDelete}
onToggleVisibility={options.onToggleVisibility}
onClose={options.onClose}
/>
<TrackingBudgetMenuModal key={name} name={name} {...options} />
);
case 'envelope-budget-menu':
case CategoryGroupMenuModal.modalName:
return <CategoryGroupMenuModal key={name} name={name} {...options} />;
case NotesModal.modalName:
return <NotesModal key={name} name={name} {...options} />;
case EnvelopeBalanceMenuModal.modalName:
return (
<NamespaceContext.Provider
key={name}
value={monthUtils.sheetForMonth(options.month)}
>
<EnvelopeBudgetMenuModal
categoryId={options.categoryId}
onUpdateBudget={options.onUpdateBudget}
onCopyLastMonthAverage={options.onCopyLastMonthAverage}
onSetMonthsAverage={options.onSetMonthsAverage}
onApplyBudgetTemplate={options.onApplyBudgetTemplate}
/>
</NamespaceContext.Provider>
<EnvelopeBalanceMenuModal key={name} name={name} {...options} />
);
case 'tracking-budget-menu':
case EnvelopeToBudgetMenuModal.modalName:
return (
<NamespaceContext.Provider
key={name}
value={monthUtils.sheetForMonth(options.month)}
>
<TrackingBudgetMenuModal
categoryId={options.categoryId}
onUpdateBudget={options.onUpdateBudget}
onCopyLastMonthAverage={options.onCopyLastMonthAverage}
onSetMonthsAverage={options.onSetMonthsAverage}
onApplyBudgetTemplate={options.onApplyBudgetTemplate}
/>
</NamespaceContext.Provider>
<EnvelopeToBudgetMenuModal key={name} name={name} {...options} />
);
case 'category-group-menu':
case HoldBufferModal.modalName:
return <HoldBufferModal key={name} name={name} {...options} />;
case TrackingBalanceMenuModal.modalName:
return (
<CategoryGroupMenuModal
key={name}
groupId={options.groupId}
onSave={options.onSave}
onAddCategory={options.onAddCategory}
onEditNotes={options.onEditNotes}
onSaveNotes={options.onSaveNotes}
onDelete={options.onDelete}
onToggleVisibility={options.onToggleVisibility}
onClose={options.onClose}
/>
<TrackingBalanceMenuModal key={name} name={name} {...options} />
);
case 'notes':
return (
<NotesModal
key={name}
id={options.id}
name={options.name}
onSave={options.onSave}
/>
);
case TransferModal.modalName:
return <TransferModal key={name} name={name} {...options} />;
case 'envelope-balance-menu':
return (
<NamespaceContext.Provider
key={name}
value={monthUtils.sheetForMonth(options.month)}
>
<EnvelopeBalanceMenuModal
categoryId={options.categoryId}
onCarryover={options.onCarryover}
onTransfer={options.onTransfer}
onCover={options.onCover}
/>
</NamespaceContext.Provider>
);
case CoverModal.modalName:
return <CoverModal key={name} name={name} {...options} />;
case 'envelope-summary-to-budget-menu':
return (
<NamespaceContext.Provider
key={name}
value={monthUtils.sheetForMonth(options.month)}
>
<EnvelopeToBudgetMenuModal
onTransfer={options.onTransfer}
onCover={options.onCover}
onHoldBuffer={options.onHoldBuffer}
onResetHoldBuffer={options.onResetHoldBuffer}
/>
</NamespaceContext.Provider>
);
case 'hold-buffer':
return (
<NamespaceContext.Provider
key={name}
value={monthUtils.sheetForMonth(options.month)}
>
<HoldBufferModal
month={options.month}
onSubmit={options.onSubmit}
/>
</NamespaceContext.Provider>
);
case 'tracking-balance-menu':
return (
<NamespaceContext.Provider
key={name}
value={monthUtils.sheetForMonth(options.month)}
>
<TrackingBalanceMenuModal
categoryId={options.categoryId}
onCarryover={options.onCarryover}
/>
</NamespaceContext.Provider>
);
case 'transfer':
return (
<TransferModal
key={name}
title={options.title}
categoryId={options.categoryId}
month={options.month}
amount={options.amount}
onSubmit={options.onSubmit}
showToBeBudgeted={options.showToBeBudgeted}
/>
);
case 'cover':
return (
<CoverModal
key={name}
title={options.title}
categoryId={options.categoryId}
month={options.month}
showToBeBudgeted={options.showToBeBudgeted}
onSubmit={options.onSubmit}
/>
);
case 'scheduled-transaction-menu':
case ScheduledTransactionMenuModal.modalName:
return (
<ScheduledTransactionMenuModal
key={name}
transactionId={options.transactionId}
onPost={options.onPost}
onSkip={options.onSkip}
name={name}
{...options}
/>
);
case 'budget-page-menu':
case BudgetPageMenuModal.modalName:
return <BudgetPageMenuModal key={name} name={name} {...options} />;
case EnvelopeBudgetMonthMenuModal.modalName:
return (
<BudgetPageMenuModal
key={name}
onAddCategoryGroup={options.onAddCategoryGroup}
onToggleHiddenCategories={options.onToggleHiddenCategories}
onSwitchBudgetFile={options.onSwitchBudgetFile}
/>
<EnvelopeBudgetMonthMenuModal key={name} name={name} {...options} />
);
case 'envelope-budget-month-menu':
case TrackingBudgetMonthMenuModal.modalName:
return (
<NamespaceContext.Provider
key={name}
value={monthUtils.sheetForMonth(options.month)}
>
<EnvelopeBudgetMonthMenuModal
month={options.month}
onBudgetAction={options.onBudgetAction}
onEditNotes={options.onEditNotes}
/>
</NamespaceContext.Provider>
<TrackingBudgetMonthMenuModal key={name} name={name} {...options} />
);
case 'tracking-budget-month-menu':
return (
<NamespaceContext.Provider
key={name}
value={monthUtils.sheetForMonth(options.month)}
>
<TrackingBudgetMonthMenuModal
month={options.month}
onBudgetAction={options.onBudgetAction}
onEditNotes={options.onEditNotes}
/>
</NamespaceContext.Provider>
);
case 'budget-list':
return <BudgetListModal key={name} />;
case 'delete-budget':
return <DeleteFileModal key={name} file={options.file} />;
case 'import':
return <ImportModal key={name} />;
case 'files-settings':
return <FilesSettingsModal key={name} />;
case 'confirm-change-document-dir':
case BudgetListModal.modalName:
return <BudgetListModal key={name} name={name} {...options} />;
case DeleteFileModal.modalName:
return <DeleteFileModal key={name} name={name} {...options} />;
case ImportModal.modalName:
return <ImportModal key={name} name={name} />;
case FilesSettingsModal.modalName:
return <FilesSettingsModal key={name} name={name} {...options} />;
case ConfirmChangeDocumentDirModal.modalName:
return (
<ConfirmChangeDocumentDirModal
key={name}
currentBudgetDirectory={options.currentBudgetDirectory}
newDirectory={options.newDirectory}
name={name}
{...options}
/>
);
case 'import-ynab4':
return <ImportYNAB4Modal key={name} />;
case 'import-ynab5':
return <ImportYNAB5Modal key={name} />;
case 'import-actual':
return <ImportActualModal key={name} />;
case 'out-of-sync-migrations':
return <OutOfSyncMigrationsModal key={name} />;
case ImportYNAB4Modal.modalName:
return <ImportYNAB4Modal key={name} name={name} {...options} />;
case ImportYNAB5Modal.modalName:
return <ImportYNAB5Modal key={name} name={name} {...options} />;
case ImportActualModal.modalName:
return <ImportActualModal key={name} name={name} {...options} />;
case OutOfSyncMigrationsModal.modalName:
return (
<OutOfSyncMigrationsModal key={name} name={name} {...options} />
);
default:
throw new Error('Unknown modal');

View File

@@ -5,11 +5,11 @@ import ReactMarkdown from 'react-markdown';
import { css } from '@emotion/css';
import remarkGfm from 'remark-gfm';
import { useResponsive } from '../ResponsiveProvider';
import { theme } from '../style';
import { remarkBreaks, sequentialNewlinesPlugin } from '../util/markdown';
import { Text } from './common/Text';
import { useResponsive } from './responsive/ResponsiveProvider';
const remarkPlugins = [sequentialNewlinesPlugin, remarkGfm, remarkBreaks];

View File

@@ -10,12 +10,12 @@ import { useDispatch, useSelector } from 'react-redux';
import { css } from '@emotion/css';
import { removeNotification } from 'loot-core/client/actions';
import { type State } from 'loot-core/src/client/state-types';
import type { NotificationWithId } from 'loot-core/src/client/state-types/notifications';
import { AnimatedLoading } from '../icons/AnimatedLoading';
import { SvgDelete } from '../icons/v0';
import { useResponsive } from '../ResponsiveProvider';
import { type State } from '../state';
import { removeNotification } from '../state/actions';
import type { NotificationWithId } from '../state/notifications';
import { styles, theme } from '../style';
import { Button, ButtonWithLoading } from './common/Button2';
@@ -23,7 +23,6 @@ import { Link } from './common/Link';
import { Stack } from './common/Stack';
import { Text } from './common/Text';
import { View } from './common/View';
import { useResponsive } from './responsive/ResponsiveProvider';
function compileMessage(
message: string,

View File

@@ -1,10 +1,10 @@
import React, { type ReactNode, type CSSProperties } from 'react';
import { useResponsive } from '../ResponsiveProvider';
import { theme, styles } from '../style';
import { Text } from './common/Text';
import { View } from './common/View';
import { useResponsive } from './responsive/ResponsiveProvider';
const HEADER_HEIGHT = 50;

View File

@@ -8,9 +8,9 @@ import React, {
import { css } from '@emotion/css';
import { usePrivacyMode } from '../hooks/usePrivacyMode';
import { useResponsive } from '../ResponsiveProvider';
import { View } from './common/View';
import { useResponsive } from './responsive/ResponsiveProvider';
type ConditionalPrivacyFilterProps = {
children: ReactNode;

View File

@@ -3,12 +3,12 @@ import React, { useRef, useState, type CSSProperties } from 'react';
import type { Theme } from 'loot-core/src/types/prefs';
import { SvgMoonStars, SvgSun, SvgSystem } from '../icons/v2';
import { useResponsive } from '../ResponsiveProvider';
import { themeOptions, useTheme } from '../style';
import { Button } from './common/Button2';
import { Menu } from './common/Menu';
import { Popover } from './common/Popover';
import { useResponsive } from './responsive/ResponsiveProvider';
type ThemeSelectorProps = {
style?: CSSProperties;

View File

@@ -7,10 +7,7 @@ import { css } from '@emotion/css';
import * as Platform from 'loot-core/src/client/platform';
import * as queries from 'loot-core/src/client/queries';
import { listen } from 'loot-core/src/platform/client/fetch';
import {
isDevelopmentEnvironment,
isElectron,
} from 'loot-core/src/shared/environment';
import { isDevelopmentEnvironment } from 'loot-core/src/shared/environment';
import { useActions } from '../hooks/useActions';
import { useGlobalPref } from '../hooks/useGlobalPref';
@@ -24,6 +21,7 @@ import {
SvgViewHide,
SvgViewShow,
} from '../icons/v2';
import { useResponsive } from '../ResponsiveProvider';
import { theme, styles } from '../style';
import { AccountSyncCheck } from './accounts/AccountSyncCheck';
@@ -36,7 +34,6 @@ import { Text } from './common/Text';
import { View } from './common/View';
import { HelpMenu } from './HelpMenu';
import { LoggedInUser } from './LoggedInUser';
import { useResponsive } from './responsive/ResponsiveProvider';
import { useServerURL } from './ServerContext';
import { useSidebar } from './sidebar/SidebarProvider';
import { useSheetValue } from './spreadsheet/useSheetValue';
@@ -343,7 +340,7 @@ export function Titlebar({ style }: TitlebarProps) {
<PrivacyButton />
{serverURL ? <SyncButton /> : null}
<LoggedInUser />
{!isElectron() && <HelpMenu />}
<HelpMenu />
</SpaceBetween>
</View>
);

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { type State } from 'loot-core/src/client/state-types';
import { useActions } from '../hooks/useActions';
import { SvgClose } from '../icons/v1';
import { type State } from '../state';
import { theme } from '../style';
import { Button } from './common/Button2';

View File

@@ -15,7 +15,6 @@ import { t } from 'i18next';
import { v4 as uuidv4 } from 'uuid';
import { validForTransfer } from 'loot-core/client/transfer';
import { type UndoState } from 'loot-core/server/undo';
import { useFilters } from 'loot-core/src/client/data-hooks/filters';
import {
SchedulesProvider,
@@ -45,6 +44,7 @@ import {
type TransactionEntity,
type TransactionFilterEntity,
} from 'loot-core/src/types/models';
import { type UndoState } from 'loot-core/types/server-events';
import { useAccounts } from '../../hooks/useAccounts';
import { useActions } from '../../hooks/useActions';
@@ -1803,15 +1803,6 @@ class AccountInternal extends PureComponent<
sortField={this.state.sort?.field}
ascDesc={this.state.sort?.ascDesc}
onChange={this.onTransactionsChange}
onBatchDelete={this.onBatchDelete}
onBatchDuplicate={this.onBatchDuplicate}
onBatchLinkSchedule={this.onBatchLinkSchedule}
onBatchUnlinkSchedule={this.onBatchUnlinkSchedule}
onCreateRule={this.onCreateRule}
onScheduleAction={this.onScheduleAction}
onMakeAsNonSplitTransactions={
this.onMakeAsNonSplitTransactions
}
onRefetch={this.refetchTransactions}
onCloseAddTransaction={() =>
this.setState({ isAdding: false })
@@ -1866,9 +1857,9 @@ export function Account() {
const location = useLocation();
const { grouped: categoryGroups } = useCategories();
const newTransactions = useSelector(state => state.queries.newTransactions);
const newTransactions = useSelector(state => state.account.newTransactions);
const matchedTransactions = useSelector(
state => state.queries.matchedTransactions,
state => state.account.matchedTransactions,
);
const accounts = useAccounts();
const payees = usePayees();

View File

@@ -1,23 +1,21 @@
import React, { useCallback, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { Trans } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { t } from 'i18next';
import { unlinkAccount } from 'loot-core/client/actions';
import { type AccountEntity } from 'loot-core/types/models';
import { authorizeBank } from '../../gocardless';
import { useAccounts } from '../../hooks/useAccounts';
import { SvgExclamationOutline } from '../../icons/v1';
import { unlinkAccount } from '../../state/actions';
import { theme } from '../../style';
import { Button } from '../common/Button2';
import { Link } from '../common/Link';
import { Popover } from '../common/Popover';
import { View } from '../common/View';
function getErrorMessage(type: string, code: string) {
function getErrorMessage(type, code) {
switch (type.toUpperCase()) {
case 'ITEM_ERROR':
switch (code.toUpperCase()) {
@@ -82,29 +80,7 @@ export function AccountSyncCheck() {
const [open, setOpen] = useState(false);
const triggerRef = useRef(null);
const reauth = useCallback(
(acc: AccountEntity) => {
setOpen(false);
if (acc.account_id) {
authorizeBank(dispatch, { upgradingAccountId: acc.account_id });
}
},
[dispatch],
);
const unlink = useCallback(
(acc: AccountEntity) => {
if (acc.id) {
dispatch(unlinkAccount(acc.id));
}
setOpen(false);
},
[dispatch],
);
if (!failedAccounts || !id) {
if (!failedAccounts) {
return null;
}
@@ -114,15 +90,22 @@ export function AccountSyncCheck() {
}
const account = accounts.find(account => account.id === id);
if (!account) {
return null;
}
const { type, code } = error;
const showAuth =
(type === 'ITEM_ERROR' && code === 'ITEM_LOGIN_REQUIRED') ||
(type === 'INVALID_INPUT' && code === 'INVALID_ACCESS_TOKEN');
function reauth() {
setOpen(false);
authorizeBank(dispatch, { upgradingAccountId: account.account_id });
}
async function unlink() {
dispatch(unlinkAccount(account.id));
setOpen(false);
}
return (
<View>
<Button
@@ -164,20 +147,20 @@ export function AccountSyncCheck() {
<View style={{ justifyContent: 'flex-end', flexDirection: 'row' }}>
{showAuth ? (
<>
<Button onPress={() => unlink(account)}>
<Button onPress={unlink}>
<Trans>Unlink</Trans>
</Button>
<Button
variant="primary"
autoFocus
onPress={() => reauth(account)}
onPress={reauth}
style={{ marginLeft: 5 }}
>
<Trans>Reauthorize</Trans>
</Button>
</>
) : (
<Button onPress={() => unlink(account)}>
<Button onPress={unlink}>
<Trans>Unlink account</Trans>
</Button>
)}

View File

@@ -30,6 +30,7 @@ import {
import { theme, styles } from '../../style';
import { AnimatedRefresh } from '../AnimatedRefresh';
import { Button } from '../common/Button2';
import { InitialFocus } from '../common/InitialFocus';
import { Input } from '../common/Input';
import { Menu } from '../common/Menu';
import { MenuButton } from '../common/MenuButton';
@@ -344,8 +345,8 @@ export function AccountHeader({
<Search
placeholder={t('Search')}
value={search}
onChangeValue={onSearch}
ref={searchInput}
onChange={onSearch}
inputRef={searchInput}
/>
{workingHard ? (
<View>
@@ -573,24 +574,24 @@ function AccountNameField({
if (editingName) {
return (
<Fragment>
<Input
autoFocus
autoSelect
defaultValue={accountName}
onEnter={e => onSaveName(e.target.value)}
onBlur={e => onSaveName(e.target.value)}
onEscape={() => onExposeName(false)}
style={{
fontSize: 25,
fontWeight: 500,
marginTop: -3,
marginBottom: -4,
marginLeft: -6,
paddingTop: 2,
paddingBottom: 2,
width: Math.max(20, accountName.length) + 'ch',
}}
/>
<InitialFocus>
<Input
defaultValue={accountName}
onEnter={e => onSaveName(e.currentTarget.value)}
onBlur={e => onSaveName(e.target.value)}
onEscape={() => onExposeName(false)}
style={{
fontSize: 25,
fontWeight: 500,
marginTop: -3,
marginBottom: -4,
marginLeft: -6,
paddingTop: 2,
paddingBottom: 2,
width: Math.max(20, accountName.length) + 'ch',
}}
/>
</InitialFocus>
{saveNameError && (
<View style={{ color: theme.warningText }}>{saveNameError}</View>
)}

View File

@@ -1,5 +1,4 @@
import React, { type FormEvent, useCallback, useRef, useState } from 'react';
import { Form } from 'react-aria-components';
import React, { useState } from 'react';
import { Trans } from 'react-i18next';
import * as queries from 'loot-core/src/client/queries';
@@ -10,6 +9,7 @@ import { type AccountEntity } from 'loot-core/types/models';
import { SvgCheckCircle1 } from '../../icons/v2';
import { styles, theme } from '../../style';
import { Button } from '../common/Button2';
import { InitialFocus } from '../common/InitialFocus';
import { Input } from '../common/Input';
import { Text } from '../common/Text';
import { View } from '../common/View';
@@ -124,49 +124,43 @@ export function ReconcileMenu({
});
const format = useFormat();
const [inputValue, setInputValue] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [inputFocused, setInputFocused] = useState(false);
const onSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
function onSubmit() {
if (inputValue === '') {
setInputFocused(true);
return;
}
if (inputValue === '') {
inputRef.current?.focus();
return;
}
const amount =
inputValue != null ? currencyToInteger(inputValue) : clearedBalance;
const amount =
inputValue != null ? currencyToInteger(inputValue) : clearedBalance;
onReconcile(amount);
onClose();
},
[clearedBalance, inputValue, onClose, onReconcile],
);
onReconcile(amount);
onClose();
}
return (
<Form onSubmit={onSubmit}>
<View style={{ padding: '5px 8px' }}>
<Text>
<Trans>
Enter the current balance of your bank account that you want to
reconcile with:
</Trans>
</Text>
{clearedBalance != null && (
<View style={{ padding: '5px 8px' }}>
<Text>
<Trans>
Enter the current balance of your bank account that you want to
reconcile with:
</Trans>
</Text>
{clearedBalance != null && (
<InitialFocus>
<Input
ref={inputRef}
defaultValue={format(clearedBalance, 'financial')}
onChangeValue={setInputValue}
style={{ margin: '7px 0' }}
autoFocus
autoSelect
focused={inputFocused}
onEnter={onSubmit}
/>
)}
<Button variant="primary" type="submit">
<Trans>Reconcile</Trans>
</Button>
</View>
</Form>
</InitialFocus>
)}
<Button variant="primary" onPress={onSubmit}>
<Trans>Reconcile</Trans>
</Button>
</View>
);
}

View File

@@ -13,10 +13,10 @@ import { css, cx } from '@emotion/css';
import { type AccountEntity } from 'loot-core/src/types/models';
import { useAccounts } from '../../hooks/useAccounts';
import { useResponsive } from '../../ResponsiveProvider';
import { theme, styles } from '../../style';
import { TextOneLine } from '../common/TextOneLine';
import { View } from '../common/View';
import { useResponsive } from '../responsive/ResponsiveProvider';
import { Autocomplete } from './Autocomplete';
import { ItemHeader } from './ItemHeader';
@@ -170,7 +170,7 @@ type AccountItemProps = {
function AccountItem({
item,
className = '',
className,
highlighted,
embedded,
...props

View File

@@ -4,12 +4,11 @@ import React, {
useRef,
useEffect,
useMemo,
type ComponentProps,
type HTMLProps,
type ReactNode,
type KeyboardEvent,
type ChangeEvent,
type ComponentPropsWithRef,
type ComponentPropsWithoutRef,
} from 'react';
import { css, cx } from '@emotion/css';
@@ -18,24 +17,26 @@ import Downshift, { type StateChangeTypes } from 'downshift';
import { getNormalisedString } from 'loot-core/src/shared/normalisation';
import { SvgRemove } from '../../icons/v2';
import { useResponsive } from '../../ResponsiveProvider';
import { theme, styles } from '../../style';
import { Button } from '../common/Button';
import { Input } from '../common/Input';
import { Popover } from '../common/Popover';
import { View } from '../common/View';
import { useResponsive } from '../responsive/ResponsiveProvider';
type CommonAutocompleteProps<T extends Item> = {
autoFocus?: boolean;
focused?: boolean;
embedded?: boolean;
containerProps?: HTMLProps<HTMLDivElement>;
labelProps?: { id?: string };
inputProps?: ComponentPropsWithRef<typeof Input>;
inputProps?: Omit<ComponentProps<typeof Input>, 'onChange'> & {
onChange?: (value: string) => void;
};
suggestions?: T[];
renderInput?: (props: ComponentPropsWithRef<typeof Input>) => ReactNode;
renderInput?: (props: ComponentProps<typeof Input>) => ReactNode;
renderItems?: (
items: T[],
getItemProps: (arg: { item: T }) => ComponentPropsWithRef<typeof View>,
getItemProps: (arg: { item: T }) => ComponentProps<typeof View>,
idx: number,
value?: string,
) => ReactNode;
@@ -137,14 +138,14 @@ function fireUpdate<T extends Item>(
onUpdate?.(selected, value);
}
function defaultRenderInput(props: ComponentPropsWithRef<typeof Input>) {
function defaultRenderInput(props: ComponentProps<typeof Input>) {
// data-1p-ignore disables 1Password autofill behaviour
return <Input data-1p-ignore {...props} />;
}
function defaultRenderItems<T extends Item>(
items: T[],
getItemProps: (arg: { item: T }) => ComponentPropsWithRef<typeof View>,
getItemProps: (arg: { item: T }) => ComponentProps<typeof View>,
highlightedIndex: number,
) {
return (
@@ -209,7 +210,7 @@ type SingleAutocompleteProps<T extends Item> = CommonAutocompleteProps<T> & {
};
function SingleAutocomplete<T extends Item>({
autoFocus,
focused,
embedded = false,
containerProps,
labelProps = {},
@@ -449,8 +450,8 @@ function SingleAutocomplete<T extends Item>({
<View ref={triggerRef} style={{ flexShrink: 0 }}>
{renderInput(
getInputProps({
focused,
...inputProps,
autoFocus,
onFocus: e => {
inputProps.onFocus?.(e);
@@ -549,9 +550,8 @@ function SingleAutocomplete<T extends Item>({
}
},
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const { onChangeValue, onChange } = inputProps || {};
onChangeValue?.(e.target.value);
onChange?.(e);
const { onChange } = inputProps || {};
onChange?.(e.target.value);
},
}),
)}
@@ -641,6 +641,7 @@ function MultiAutocomplete<T extends Item>({
clearOnBlur = true,
...props
}: MultiAutocompleteProps<T>) {
const [focused, setFocused] = useState(false);
const selectedItemIds = selectedItems.map(getItemId);
function onRemoveItem(id: T['id']) {
@@ -657,7 +658,7 @@ function MultiAutocomplete<T extends Item>({
function onKeyDown(
e: KeyboardEvent<HTMLInputElement>,
prevOnKeyDown?: ComponentPropsWithoutRef<typeof Input>['onKeyDown'],
prevOnKeyDown?: ComponentProps<typeof Input>['onKeyDown'],
) {
if (e.key === 'Backspace' && e.currentTarget.value === '') {
onRemoveItem(selectedItemIds[selectedItems.length - 1]);
@@ -681,7 +682,7 @@ function MultiAutocomplete<T extends Item>({
strict={strict}
renderInput={inputProps => (
<View
className={`${css({
style={{
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
@@ -689,11 +690,11 @@ function MultiAutocomplete<T extends Item>({
backgroundColor: theme.tableBackground,
borderRadius: 4,
border: '1px solid ' + theme.formInputBorder,
'&:focus-within': {
...(focused && {
border: '1px solid ' + theme.formInputBorderSelected,
boxShadow: '0 1px 1px ' + theme.formInputShadowSelected,
},
})} ${inputProps.className || ''}`}
}),
}}
>
{selectedItems.map((item, idx) => {
item = findItem(strict, suggestions, item);
@@ -710,14 +711,21 @@ function MultiAutocomplete<T extends Item>({
<Input
{...inputProps}
onKeyDown={e => onKeyDown(e, inputProps.onKeyDown)}
className={String(
css({
flex: 1,
minWidth: 30,
border: 0,
'&[data-focused]': { border: 0, boxShadow: 'none' },
}),
)}
onFocus={e => {
setFocused(true);
inputProps.onFocus(e);
}}
onBlur={e => {
setFocused(false);
inputProps.onBlur(e);
}}
style={{
flex: 1,
minWidth: 30,
border: 0,
':focus': { border: 0, boxShadow: 'none' },
...inputProps.style,
}}
/>
</View>
)}
@@ -753,8 +761,8 @@ export function AutocompleteFooter({
}
type AutocompleteProps<T extends Item> =
| ComponentPropsWithoutRef<typeof SingleAutocomplete<T>>
| ComponentPropsWithoutRef<typeof MultiAutocomplete<T>>;
| ComponentProps<typeof SingleAutocomplete<T>>
| ComponentProps<typeof MultiAutocomplete<T>>;
export function Autocomplete<T extends Item>({
...props

View File

@@ -25,13 +25,13 @@ import {
import { useCategories } from '../../hooks/useCategories';
import { useSyncedPref } from '../../hooks/useSyncedPref';
import { SvgSplit } from '../../icons/v0';
import { useResponsive } from '../../ResponsiveProvider';
import { theme, styles } from '../../style';
import { useEnvelopeSheetValue } from '../budget/envelope/EnvelopeBudgetComponents';
import { makeAmountFullStyle } from '../budget/util';
import { Text } from '../common/Text';
import { TextOneLine } from '../common/TextOneLine';
import { View } from '../common/View';
import { useResponsive } from '../responsive/ResponsiveProvider';
import { useSheetValue } from '../spreadsheet/useSheetValue';
import { Autocomplete, defaultFilterSuggestion } from './Autocomplete';
@@ -88,8 +88,7 @@ function CategoryList({
<View>
<View
style={{
overflowY: 'auto',
willChange: 'transform',
overflow: 'auto',
padding: '5px 0',
...(!embedded && { maxHeight: 175 }),
}}
@@ -365,7 +364,7 @@ type CategoryItemProps = {
function CategoryItem({
item,
className = '',
className,
style,
highlighted,
embedded,

View File

@@ -1,7 +1,7 @@
import React, { type CSSProperties } from 'react';
import { useResponsive } from '../../ResponsiveProvider';
import { styles, theme } from '../../style';
import { useResponsive } from '../responsive/ResponsiveProvider';
type ItemHeaderProps = {
title: string;

View File

@@ -7,7 +7,7 @@ import { TestProvider } from 'loot-core/src/mocks/redux';
import type { AccountEntity, PayeeEntity } from 'loot-core/types/models';
import { useCommonPayees } from '../../hooks/usePayees';
import { ResponsiveProvider } from '../responsive/ResponsiveProvider';
import { ResponsiveProvider } from '../../ResponsiveProvider';
import {
PayeeAutocomplete,

View File

@@ -16,8 +16,6 @@ import { useDispatch } from 'react-redux';
import { css, cx } from '@emotion/css';
import { createPayee } from 'loot-core/src/client/actions/queries';
import { getActivePayees } from 'loot-core/src/client/reducers/queries';
import { getNormalisedString } from 'loot-core/src/shared/normalisation';
import {
type AccountEntity,
@@ -27,11 +25,13 @@ import {
import { useAccounts } from '../../hooks/useAccounts';
import { useCommonPayees, usePayees } from '../../hooks/usePayees';
import { SvgAdd, SvgBookmark } from '../../icons/v1';
import { useResponsive } from '../../ResponsiveProvider';
import { createPayee } from '../../state/actions';
import { getActivePayees } from '../../state/queries';
import { theme, styles } from '../../style';
import { Button } from '../common/Button';
import { TextOneLine } from '../common/TextOneLine';
import { View } from '../common/View';
import { useResponsive } from '../responsive/ResponsiveProvider';
import {
Autocomplete,
@@ -341,6 +341,8 @@ export function PayeeAutocomplete({
}
}
const [payeeFieldFocused, setPayeeFieldFocused] = useState(false);
return (
<Autocomplete
key={focusTransferPayees ? 'transfers' : 'all'}
@@ -358,11 +360,16 @@ export function PayeeAutocomplete({
}
return item.name;
}}
focused={payeeFieldFocused}
inputProps={{
...inputProps,
autoCapitalize: 'words',
onBlur: () => setRawPayee(''),
onChangeValue: setRawPayee,
onBlur: () => {
setRawPayee('');
setPayeeFieldFocused(false);
},
onFocus: () => setPayeeFieldFocused(true),
onChange: setRawPayee,
}}
onUpdate={(id, inputValue) => onUpdate?.(id, makeNew(id, inputValue))}
onSelect={handleSelect}
@@ -549,7 +556,7 @@ type PayeeItemProps = {
function PayeeItem({
item,
className = '',
className,
highlighted,
embedded,
...props

View File

@@ -11,10 +11,10 @@ import { css } from '@emotion/css';
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
import { SvgArrowThinRight } from '../../icons/v1';
import { useResponsive } from '../../ResponsiveProvider';
import { theme, styles } from '../../style';
import { Tooltip } from '../common/Tooltip';
import { View } from '../common/View';
import { useResponsive } from '../responsive/ResponsiveProvider';
import { type Binding } from '../spreadsheet';
import { CellValue, CellValueText } from '../spreadsheet/CellValue';
import { useFormat } from '../spreadsheet/useFormat';

View File

@@ -28,7 +28,6 @@ export const BudgetCategories = memo(
onSaveGroup,
onDeleteCategory,
onDeleteGroup,
onApplyBudgetTemplatesInGroup,
onReorderCategory,
onReorderGroup,
}) => {
@@ -246,7 +245,6 @@ export const BudgetCategories = memo(
onReorderCategory={onReorderCategory}
onToggleCollapse={onToggleCollapse}
onShowNewCategory={onShowNewCategory}
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
/>
);
break;

View File

@@ -28,7 +28,6 @@ export function BudgetTable(props) {
onDeleteCategory,
onSaveGroup,
onDeleteGroup,
onApplyBudgetTemplatesInGroup,
onReorderCategory,
onReorderGroup,
onShowActivity,
@@ -236,7 +235,6 @@ export function BudgetTable(props) {
onReorderGroup={_onReorderGroup}
onBudgetAction={onBudgetAction}
onShowActivity={onShowActivity}
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
/>
</View>
</View>

View File

@@ -25,9 +25,6 @@ type ExpenseGroupProps = {
onEditName?: ComponentProps<typeof SidebarGroup>['onEdit'];
onSave?: ComponentProps<typeof SidebarGroup>['onSave'];
onDelete?: ComponentProps<typeof SidebarGroup>['onDelete'];
onApplyBudgetTemplatesInGroup?: ComponentProps<
typeof SidebarGroup
>['onApplyBudgetTemplatesInGroup'];
onDragChange: OnDragChangeCallback<
ComponentProps<typeof SidebarGroup>['group']
>;
@@ -46,7 +43,6 @@ export function ExpenseGroup({
onEditName,
onSave,
onDelete,
onApplyBudgetTemplatesInGroup,
onDragChange,
onReorderGroup,
onReorderCategory,
@@ -129,7 +125,6 @@ export function ExpenseGroup({
onEdit={onEditName}
onSave={onSave}
onDelete={onDelete}
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
onShowNewCategory={onShowNewCategory}
/>
<RenderMonths component={MonthComponent} args={{ group }} />

View File

@@ -7,7 +7,6 @@ import {
type CategoryEntity,
} from 'loot-core/src/types/models';
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
import { SvgCheveronDown } from '../../icons/v1';
import { theme } from '../../style';
import { Button } from '../common/Button2';
@@ -52,7 +51,6 @@ export function SidebarCategory({
const temporary = category.id === 'new';
const [menuOpen, setMenuOpen] = useState(false);
const triggerRef = useRef(null);
const contextMenusEnabled = useFeatureFlag('contextMenus');
const displayed = (
<View
@@ -63,13 +61,6 @@ export function SidebarCategory({
WebkitUserSelect: 'none',
opacity: category.hidden || categoryGroup?.hidden ? 0.33 : undefined,
backgroundColor: 'transparent',
height: 20,
}}
ref={triggerRef}
onContextMenu={e => {
if (!contextMenusEnabled) return;
e.preventDefault();
setMenuOpen(true);
}}
>
<div
@@ -83,7 +74,7 @@ export function SidebarCategory({
>
{category.name}
</div>
<View style={{ flexShrink: 0, marginLeft: 5 }}>
<View style={{ flexShrink: 0, marginLeft: 5 }} ref={triggerRef}>
<Button
variant="bare"
className="hover-visible"
@@ -103,7 +94,6 @@ export function SidebarCategory({
isOpen={menuOpen}
onOpenChange={() => setMenuOpen(false)}
style={{ width: 200 }}
isNonModal
>
<Menu
onMenuSelect={type => {

View File

@@ -3,7 +3,6 @@ import React, { type CSSProperties, useRef, useState } from 'react';
import { type ConnectDragSource } from 'react-dnd';
import { useTranslation } from 'react-i18next';
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
import { SvgExpandArrow } from '../../icons/v0';
import { SvgCheveronDown } from '../../icons/v1';
import { theme } from '../../style';
@@ -33,7 +32,6 @@ type SidebarGroupProps = {
onEdit?: (id: string) => void;
onSave?: (group: object) => Promise<void>;
onDelete?: (id: string) => Promise<void>;
onApplyBudgetTemplatesInGroup?: (categories: object[]) => void;
onShowNewCategory?: (groupId: string) => void;
onHideNewGroup?: () => void;
onToggleCollapse?: (id: string) => void;
@@ -49,18 +47,15 @@ export function SidebarGroup({
onEdit,
onSave,
onDelete,
onApplyBudgetTemplatesInGroup,
onShowNewCategory,
onHideNewGroup,
onToggleCollapse,
}: SidebarGroupProps) {
const { t } = useTranslation();
const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled');
const temporary = group.id === 'new';
const [menuOpen, setMenuOpen] = useState(false);
const triggerRef = useRef(null);
const contextMenusEnabled = useFeatureFlag('contextMenus');
const displayed = (
<View
@@ -69,17 +64,10 @@ export function SidebarGroup({
alignItems: 'center',
userSelect: 'none',
WebkitUserSelect: 'none',
height: 20,
}}
ref={triggerRef}
onClick={() => {
onToggleCollapse(group.id);
}}
onContextMenu={e => {
if (!contextMenusEnabled) return;
e.preventDefault();
setMenuOpen(true);
}}
>
{!dragPreview && (
<SvgExpandArrow
@@ -107,7 +95,7 @@ export function SidebarGroup({
</div>
{!dragPreview && (
<>
<View style={{ marginLeft: 5, flexShrink: 0 }}>
<View style={{ marginLeft: 5, flexShrink: 0 }} ref={triggerRef}>
<Button
variant="bare"
className="hover-visible"
@@ -123,7 +111,6 @@ export function SidebarGroup({
isOpen={menuOpen}
onOpenChange={() => setMenuOpen(false)}
style={{ width: 200 }}
isNonModal
>
<Menu
onMenuSelect={type => {
@@ -135,12 +122,6 @@ export function SidebarGroup({
onDelete(group.id);
} else if (type === 'toggle-visibility') {
onSave({ ...group, hidden: !group.hidden });
} else if (type === 'apply-multiple-category-template') {
onApplyBudgetTemplatesInGroup?.(
group.categories
.filter(c => !c['hidden'])
.map(c => c['id']),
);
}
setMenuOpen(false);
}}
@@ -152,14 +133,6 @@ export function SidebarGroup({
text: group.hidden ? t('Show') : t('Hide'),
},
onDelete && { name: 'delete', text: t('Delete') },
...(isGoalTemplatesEnabled
? [
{
name: 'apply-multiple-category-template',
text: t('Apply budget templates'),
},
]
: []),
]}
/>
</Popover>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useRef, useState } from 'react';
import React, { useState } from 'react';
import { envelopeBudget } from 'loot-core/src/client/queries';
@@ -23,20 +23,10 @@ export function BalanceMovementMenu({
const catBalance = useEnvelopeSheetValue(
envelopeBudget.catBalance(categoryId),
);
const [menu, _setMenu] = useState('menu');
const ref = useRef<HTMLSpanElement>(null);
// Keep focus inside the popover on menu change
const setMenu = useCallback(
(menu: string) => {
ref.current?.focus();
_setMenu(menu);
},
[ref],
);
const [menu, setMenu] = useState('menu');
return (
<span tabIndex={-1} ref={ref}>
<>
{menu === 'menu' && (
<BalanceMenu
categoryId={categoryId}
@@ -80,6 +70,6 @@ export function BalanceMovementMenu({
}}
/>
)}
</span>
</>
);
}

View File

@@ -1,5 +1,4 @@
import React, { type FormEvent, useCallback, useMemo, useState } from 'react';
import { Form } from 'react-aria-components';
import React, { useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { type CategoryEntity } from 'loot-core/src/types/models';
@@ -7,6 +6,7 @@ import { type CategoryEntity } from 'loot-core/src/types/models';
import { useCategories } from '../../../hooks/useCategories';
import { CategoryAutocomplete } from '../../autocomplete/CategoryAutocomplete';
import { Button } from '../../common/Button2';
import { InitialFocus } from '../../common/InitialFocus';
import { View } from '../../common/View';
import { addToBeBudgetedGroup, removeCategoriesFromGroups } from '../util';
@@ -39,55 +39,52 @@ export function CoverMenu({
: categoryGroups;
}, [categoryId, showToBeBudgeted, originalCategoryGroups]);
const onSubmitInner = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (fromCategoryId) {
onSubmit(fromCategoryId);
}
onClose();
},
[fromCategoryId, onSubmit, onClose],
);
function submit() {
if (fromCategoryId) {
onSubmit(fromCategoryId);
}
onClose();
}
return (
<Form onSubmit={onSubmitInner}>
<View style={{ padding: 10 }}>
<View style={{ marginBottom: 5 }}>
<Trans>Cover from category:</Trans>
</View>
<CategoryAutocomplete
categoryGroups={filteredCategoryGroups}
value={null}
openOnFocus={true}
onSelect={(id: string | undefined) => setFromCategoryId(id || null)}
inputProps={{
placeholder: t('(none)'),
}}
showHiddenCategories={false}
autoFocus
/>
<View
style={{
alignItems: 'flex-end',
marginTop: 10,
}}
>
<Button
variant="primary"
type="submit"
style={{
fontSize: 12,
paddingTop: 3,
}}
>
<Trans>Transfer</Trans>
</Button>
</View>
<View style={{ padding: 10 }}>
<View style={{ marginBottom: 5 }}>
<Trans>Cover from category:</Trans>
</View>
</Form>
<InitialFocus>
{node => (
<CategoryAutocomplete
categoryGroups={filteredCategoryGroups}
value={null}
openOnFocus={true}
onSelect={(id: string | undefined) => setFromCategoryId(id || null)}
inputProps={{
inputRef: node,
onEnter: event => !event.defaultPrevented && submit(),
placeholder: t('(none)'),
}}
showHiddenCategories={false}
/>
)}
</InitialFocus>
<View
style={{
alignItems: 'flex-end',
marginTop: 10,
}}
>
<Button
variant="primary"
style={{
fontSize: 12,
paddingTop: 3,
}}
onPress={submit}
>
<Trans>Transfer</Trans>
</Button>
</View>
</View>
);
}

View File

@@ -14,7 +14,6 @@ import { evalArithmetic } from 'loot-core/src/shared/arithmetic';
import * as monthUtils from 'loot-core/src/shared/months';
import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util';
import { useFeatureFlag } from '../../../hooks/useFeatureFlag';
import { useUndo } from '../../../hooks/useUndo';
import { SvgCheveronDown } from '../../../icons/v1';
import { styles, theme } from '../../../style';
@@ -22,11 +21,7 @@ import { Button } from '../../common/Button2';
import { Popover } from '../../common/Popover';
import { Text } from '../../common/Text';
import { View } from '../../common/View';
import {
type SheetResult,
type Binding,
type SheetFields,
} from '../../spreadsheet';
import { type Binding, type SheetFields } from '../../spreadsheet';
import { CellValue, CellValueText } from '../../spreadsheet/CellValue';
import { useSheetName } from '../../spreadsheet/useSheetName';
import { useSheetValue } from '../../spreadsheet/useSheetValue';
@@ -45,11 +40,8 @@ export function useEnvelopeSheetName<
export function useEnvelopeSheetValue<
FieldName extends SheetFields<'envelope-budget'>,
>(
binding: Binding<'envelope-budget', FieldName>,
onChange?: (result: SheetResult<'envelope-budget', FieldName>) => void,
) {
return useSheetValue(binding, onChange);
>(binding: Binding<'envelope-budget', FieldName>) {
return useSheetValue(binding);
}
export const EnvelopeCellValue = <
@@ -223,7 +215,6 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
};
const { showUndoNotification } = useUndo();
const contextMenusEnabled = useFeatureFlag('contextMenus');
return (
<View
@@ -247,12 +238,6 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
flex: 1,
flexDirection: 'row',
}}
onContextMenu={e => {
if (!contextMenusEnabled) return;
if (editing) return;
e.preventDefault();
setBudgetMenuOpen(true);
}}
>
{!editing && (
<View
@@ -289,7 +274,6 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
isOpen={budgetMenuOpen}
onOpenChange={() => setBudgetMenuOpen(false)}
style={{ width: 200 }}
isNonModal
>
<BudgetMenu
onCopyLastMonthAverage={() => {
@@ -406,11 +390,6 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
<span
ref={balanceMenuTriggerRef}
onClick={() => setBalanceMenuOpen(true)}
onContextMenu={e => {
if (!contextMenusEnabled) return;
e.preventDefault();
setBalanceMenuOpen(true);
}}
>
<BalanceWithCarryover
carryover={envelopeBudget.catCarryover(category.id)}
@@ -427,7 +406,6 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
isOpen={balanceMenuOpen}
onOpenChange={() => setBalanceMenuOpen(false)}
style={{ width: 200 }}
isNonModal
>
<BalanceMovementMenu
categoryId={category.id}

View File

@@ -1,48 +1,45 @@
import React, { useState, useCallback, type FormEvent, useRef } from 'react';
import { Form } from 'react-aria-components';
import React, {
useState,
useContext,
useEffect,
type ChangeEvent,
} from 'react';
import { Trans } from 'react-i18next';
import { envelopeBudget } from 'loot-core/client/queries';
import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
import { evalArithmetic } from 'loot-core/src/shared/arithmetic';
import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util';
import { Button } from '../../common/Button2';
import { InitialFocus } from '../../common/InitialFocus';
import { Input } from '../../common/Input';
import { View } from '../../common/View';
import { useEnvelopeSheetValue } from './EnvelopeBudgetComponents';
import { NamespaceContext } from '../../spreadsheet/NamespaceContext';
type HoldMenuProps = {
onSubmit: (amount: number) => void;
onClose: () => void;
};
export function HoldMenu({ onSubmit, onClose }: HoldMenuProps) {
const [amount, setAmount] = useState<string>(
integerToCurrency(
useEnvelopeSheetValue(envelopeBudget.toBudget, result => {
setAmount(integerToCurrency(result?.value ?? 0));
}) ?? 0,
),
);
const inputRef = useRef<HTMLInputElement>(null);
const spreadsheet = useSpreadsheet();
const sheetName = useContext(NamespaceContext);
const onSubmitInner = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const [amount, setAmount] = useState<string | null>(null);
if (amount === '') {
inputRef.current?.focus();
return;
}
useEffect(() => {
(async () => {
const node = await spreadsheet.get(sheetName, 'to-budget');
setAmount(integerToCurrency(Math.max(node.value as number, 0)));
})();
}, []);
const parsedAmount = evalArithmetic(amount);
if (parsedAmount) {
onSubmit(amountToInteger(parsedAmount));
}
onClose();
},
[amount, onSubmit, onClose],
);
function submit(newAmount: string) {
const parsedAmount = evalArithmetic(newAmount);
if (parsedAmount) {
onSubmit(amountToInteger(parsedAmount));
}
onClose();
}
if (amount === null) {
// See `TransferMenu` for more info about this
@@ -50,37 +47,39 @@ export function HoldMenu({ onSubmit, onClose }: HoldMenuProps) {
}
return (
<Form onSubmit={onSubmitInner}>
<View style={{ padding: 10 }}>
<View style={{ marginBottom: 5 }}>
<Trans>Hold this amount:</Trans>
</View>
<Input
ref={inputRef}
value={amount}
onChangeValue={(value: string) => setAmount(value)}
autoFocus
autoSelect
/>
<View
style={{
alignItems: 'flex-end',
marginTop: 10,
}}
>
<Button
variant="primary"
type="submit"
style={{
fontSize: 12,
paddingTop: 3,
paddingBottom: 3,
}}
>
<Trans>Hold</Trans>
</Button>
</View>
<View style={{ padding: 10 }}>
<View style={{ marginBottom: 5 }}>
<Trans>Hold this amount:</Trans>
</View>
</Form>
<View>
<InitialFocus>
<Input
value={amount}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setAmount(e.target.value)
}
onEnter={() => submit(amount)}
/>
</InitialFocus>
</View>
<View
style={{
alignItems: 'flex-end',
marginTop: 10,
}}
>
<Button
variant="primary"
style={{
fontSize: 12,
paddingTop: 3,
paddingBottom: 3,
}}
onPress={() => submit(amount)}
>
<Trans>Hold</Trans>
</Button>
</View>
</View>
);
}

View File

@@ -1,5 +1,4 @@
import React, { type FormEvent, useCallback, useMemo, useState } from 'react';
import { Form } from 'react-aria-components';
import React, { useMemo, useState } from 'react';
import { Trans } from 'react-i18next';
import { evalArithmetic } from 'loot-core/src/shared/arithmetic';
@@ -9,6 +8,7 @@ import { type CategoryEntity } from 'loot-core/types/models';
import { useCategories } from '../../../hooks/useCategories';
import { CategoryAutocomplete } from '../../autocomplete/CategoryAutocomplete';
import { Button } from '../../common/Button2';
import { InitialFocus } from '../../common/InitialFocus';
import { Input } from '../../common/Input';
import { View } from '../../common/View';
import { addToBeBudgetedGroup, removeCategoriesFromGroups } from '../util';
@@ -42,67 +42,65 @@ export function TransferMenu({
}, [originalCategoryGroups, categoryId, showToBeBudgeted]);
const _initialAmount = integerToCurrency(Math.max(initialAmount, 0));
const [amount, setAmount] = useState<string>(_initialAmount);
const [amount, setAmount] = useState<string | null>(null);
const [toCategoryId, setToCategoryId] = useState<string | null>(null);
const _onSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const _onSubmit = (newAmount: string | null, categoryId: string | null) => {
const parsedAmount = evalArithmetic(newAmount || '');
if (parsedAmount && categoryId) {
onSubmit?.(amountToInteger(parsedAmount), categoryId);
}
const parsedAmount = evalArithmetic(amount || '');
if (parsedAmount && toCategoryId) {
onSubmit?.(amountToInteger(parsedAmount), toCategoryId);
}
onClose();
},
[amount, toCategoryId, onSubmit, onClose],
);
onClose();
};
return (
<Form onSubmit={_onSubmit}>
<View style={{ padding: 10 }}>
<View style={{ marginBottom: 5 }}>
<Trans>Transfer this amount:</Trans>
</View>
<Input
value={amount}
onChangeValue={value => setAmount(value)}
autoFocus
autoSelect
/>
<View style={{ margin: '10px 0 5px 0' }}>To:</View>
<CategoryAutocomplete
categoryGroups={filteredCategoryGroups}
value={null}
openOnFocus={true}
onSelect={(id: string | undefined) => setToCategoryId(id || null)}
inputProps={{
placeholder: '(none)',
}}
showHiddenCategories={true}
/>
<View
style={{
alignItems: 'flex-end',
marginTop: 10,
}}
>
<Button
variant="primary"
type="submit"
style={{
fontSize: 12,
paddingTop: 3,
paddingBottom: 3,
}}
>
<Trans>Transfer</Trans>
</Button>
</View>
<View style={{ padding: 10 }}>
<View style={{ marginBottom: 5 }}>
<Trans>Transfer this amount:</Trans>
</View>
</Form>
<View>
<InitialFocus>
<Input
defaultValue={_initialAmount}
onUpdate={value => setAmount(value)}
onEnter={() => _onSubmit(amount, toCategoryId)}
/>
</InitialFocus>
</View>
<View style={{ margin: '10px 0 5px 0' }}>To:</View>
<CategoryAutocomplete
categoryGroups={filteredCategoryGroups}
value={null}
openOnFocus={true}
onSelect={(id: string | undefined) => setToCategoryId(id || null)}
inputProps={{
onEnter: event =>
!event.defaultPrevented && _onSubmit(amount, toCategoryId),
placeholder: '(none)',
}}
showHiddenCategories={true}
/>
<View
style={{
alignItems: 'flex-end',
marginTop: 10,
}}
>
<Button
variant="primary"
style={{
fontSize: 12,
paddingTop: 3,
paddingBottom: 3,
}}
onPress={() => _onSubmit(amount, toCategoryId)}
>
<Trans>Transfer</Trans>
</Button>
</View>
</View>
);
}

View File

@@ -1,13 +1,7 @@
import React, {
useRef,
useState,
type CSSProperties,
useCallback,
} from 'react';
import React, { useRef, useState, type CSSProperties } from 'react';
import { envelopeBudget } from 'loot-core/src/client/queries';
import { useFeatureFlag } from '../../../../hooks/useFeatureFlag';
import { Popover } from '../../../common/Popover';
import { View } from '../../../common/View';
import { CoverMenu } from '../CoverMenu';
@@ -34,17 +28,8 @@ export function ToBudget({
amountStyle,
isCollapsed = false,
}: ToBudgetProps) {
const [menuOpen, _setMenuOpen] = useState<string | null>(null);
const [menuOpen, setMenuOpen] = useState<string | null>(null);
const triggerRef = useRef(null);
const ref = useRef<HTMLSpanElement>(null);
const setMenuOpen = useCallback(
(menu: string | null) => {
if (menu) ref.current?.focus();
_setMenuOpen(menu);
},
[ref],
);
const sheetValue = useEnvelopeSheetValue({
name: envelopeBudget.toBudget,
value: 0,
@@ -56,7 +41,6 @@ export function ToBudget({
);
}
const isMenuOpen = Boolean(menuOpen);
const contextMenusEnabled = useFeatureFlag('contextMenus');
return (
<>
@@ -67,11 +51,6 @@ export function ToBudget({
style={style}
amountStyle={amountStyle}
isTotalsListTooltipDisabled={!isCollapsed || isMenuOpen}
onContextMenu={e => {
if (!contextMenusEnabled) return;
e.preventDefault();
setMenuOpen('actions');
}}
/>
</View>
@@ -81,52 +60,49 @@ export function ToBudget({
isOpen={isMenuOpen}
onOpenChange={() => setMenuOpen(null)}
style={{ width: 200 }}
isNonModal
>
<span tabIndex={-1} ref={ref}>
{menuOpen === 'actions' && (
<ToBudgetMenu
onTransfer={() => setMenuOpen('transfer')}
onCover={() => setMenuOpen('cover')}
onHoldBuffer={() => setMenuOpen('buffer')}
onResetHoldBuffer={() => {
onBudgetAction(month, 'reset-hold');
setMenuOpen(null);
}}
/>
)}
{menuOpen === 'buffer' && (
<HoldMenu
onClose={() => setMenuOpen(null)}
onSubmit={amount => {
onBudgetAction(month, 'hold', { amount });
}}
/>
)}
{menuOpen === 'transfer' && (
<TransferMenu
initialAmount={availableValue ?? undefined}
onClose={() => setMenuOpen(null)}
onSubmit={(amount, categoryId) => {
onBudgetAction(month, 'transfer-available', {
amount,
category: categoryId,
});
}}
/>
)}
{menuOpen === 'cover' && (
<CoverMenu
showToBeBudgeted={false}
onClose={() => setMenuOpen(null)}
onSubmit={categoryId => {
onBudgetAction(month, 'cover-overbudgeted', {
category: categoryId,
});
}}
/>
)}
</span>
{menuOpen === 'actions' && (
<ToBudgetMenu
onTransfer={() => setMenuOpen('transfer')}
onCover={() => setMenuOpen('cover')}
onHoldBuffer={() => setMenuOpen('buffer')}
onResetHoldBuffer={() => {
onBudgetAction(month, 'reset-hold');
setMenuOpen(null);
}}
/>
)}
{menuOpen === 'buffer' && (
<HoldMenu
onClose={() => setMenuOpen(null)}
onSubmit={amount => {
onBudgetAction(month, 'hold', { amount });
}}
/>
)}
{menuOpen === 'transfer' && (
<TransferMenu
initialAmount={availableValue ?? undefined}
onClose={() => setMenuOpen(null)}
onSubmit={(amount, categoryId) => {
onBudgetAction(month, 'transfer-available', {
amount,
category: categoryId,
});
}}
/>
)}
{menuOpen === 'cover' && (
<CoverMenu
showToBeBudgeted={false}
onClose={() => setMenuOpen(null)}
onSubmit={categoryId => {
onBudgetAction(month, 'cover-overbudgeted', {
category: categoryId,
});
}}
/>
)}
</Popover>
</>
);

View File

@@ -1,4 +1,4 @@
import React, { type CSSProperties, type MouseEventHandler } from 'react';
import React, { type CSSProperties } from 'react';
import { css } from '@emotion/css';
@@ -22,7 +22,6 @@ type ToBudgetAmountProps = {
style?: CSSProperties;
amountStyle?: CSSProperties;
onClick: () => void;
onContextMenu?: MouseEventHandler;
isTotalsListTooltipDisabled?: boolean;
};
@@ -32,7 +31,6 @@ export function ToBudgetAmount({
amountStyle,
onClick,
isTotalsListTooltipDisabled = false,
onContextMenu,
}: ToBudgetAmountProps) {
const sheetName = useEnvelopeSheetName(envelopeBudget.toBudget);
const sheetValue = useEnvelopeSheetValue({
@@ -73,7 +71,6 @@ export function ToBudgetAmount({
>
<Block
onClick={onClick}
onContextMenu={onContextMenu}
data-cellname={sheetName}
className={css([
styles.veryLargeText,

View File

@@ -3,6 +3,15 @@ import React, { memo, useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
import { send, listen } from 'loot-core/src/platform/client/fetch';
import * as monthUtils from 'loot-core/src/shared/months';
import { useCategories } from '../../hooks/useCategories';
import { useGlobalPref } from '../../hooks/useGlobalPref';
import { useLocalPref } from '../../hooks/useLocalPref';
import { useNavigate } from '../../hooks/useNavigate';
import { useSyncedPref } from '../../hooks/useSyncedPref';
import {
addNotification,
applyBudgetAction,
@@ -16,16 +25,7 @@ import {
pushModal,
updateCategory,
updateGroup,
} from 'loot-core/src/client/actions';
import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
import { send, listen } from 'loot-core/src/platform/client/fetch';
import * as monthUtils from 'loot-core/src/shared/months';
import { useCategories } from '../../hooks/useCategories';
import { useGlobalPref } from '../../hooks/useGlobalPref';
import { useLocalPref } from '../../hooks/useLocalPref';
import { useNavigate } from '../../hooks/useNavigate';
import { useSyncedPref } from '../../hooks/useSyncedPref';
} from '../../state/actions';
import { styles } from '../../style';
import { View } from '../common/View';
import { NamespaceContext } from '../spreadsheet/NamespaceContext';
@@ -265,15 +265,6 @@ function BudgetInner(props: BudgetInnerProps) {
}
};
const onApplyBudgetTemplatesInGroup = async categories => {
dispatch(
applyBudgetAction(startMonth, 'apply-multiple-templates', {
month: startMonth,
categories,
}),
);
};
const onBudgetAction = (month, type, args) => {
dispatch(applyBudgetAction(month, type, args));
};
@@ -375,7 +366,6 @@ function BudgetInner(props: BudgetInnerProps) {
onMonthSelect={onMonthSelect}
onDeleteCategory={onDeleteCategory}
onDeleteGroup={onDeleteGroup}
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
onSaveCategory={onSaveCategory}
onSaveGroup={onSaveGroup}
onBudgetAction={onBudgetAction}

View File

@@ -22,11 +22,7 @@ import { Button } from '../../common/Button2';
import { Popover } from '../../common/Popover';
import { Text } from '../../common/Text';
import { View } from '../../common/View';
import {
type SheetResult,
type Binding,
type SheetFields,
} from '../../spreadsheet';
import { type Binding, type SheetFields } from '../../spreadsheet';
import { CellValue, CellValueText } from '../../spreadsheet/CellValue';
import { useSheetValue } from '../../spreadsheet/useSheetValue';
import { Field, SheetCell, type SheetCellProps } from '../../table';
@@ -40,9 +36,8 @@ export const useTrackingSheetValue = <
FieldName extends SheetFields<'tracking-budget'>,
>(
binding: Binding<'tracking-budget', FieldName>,
onChange?: (result: SheetResult<'tracking-budget', FieldName>) => void,
) => {
return useSheetValue(binding, onChange);
return useSheetValue(binding);
};
const TrackingCellValue = <FieldName extends SheetFields<'tracking-budget'>>(

View File

@@ -60,7 +60,7 @@ export function separateGroups(categoryGroups: CategoryGroupEntity[]) {
return [
categoryGroups.filter(g => !g.is_income),
categoryGroups.find(g => g.is_income),
] as const;
];
}
export function makeAmountGrey(value: number | string): CSSProperties {

View File

@@ -177,7 +177,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
className={
typeof className === 'function'
? renderProps =>
`${defaultButtonClassName} ${className(renderProps) || ''}`
`${defaultButtonClassName} ${className(renderProps)}`
: `${defaultButtonClassName} ${className || ''}`
}
>

View File

@@ -0,0 +1,36 @@
import {
type ReactElement,
type Ref,
cloneElement,
useEffect,
useRef,
} from 'react';
type InitialFocusProps = {
children:
| ReactElement<{ inputRef: Ref<HTMLInputElement> }>
| ((node: Ref<HTMLInputElement>) => ReactElement);
};
export function InitialFocus({ children }: InitialFocusProps) {
const node = useRef<HTMLInputElement>(null);
useEffect(() => {
if (node.current) {
// This is needed to avoid a strange interaction with
// `ScopeTab`, which doesn't allow it to be focused at first for
// some reason. Need to look into it.
setTimeout(() => {
if (node.current) {
node.current.focus();
node.current.setSelectionRange(0, 10000);
}
}, 0);
}
}, []);
if (typeof children === 'function') {
return children(node);
}
return cloneElement(children, { inputRef: node });
}

View File

@@ -1,138 +1,110 @@
import React, {
type InputHTMLAttributes,
type KeyboardEvent,
type ComponentPropsWithRef,
forwardRef,
useEffect,
type Ref,
type CSSProperties,
useRef,
useMemo,
} from 'react';
import { Input as ReactAriaInput } from 'react-aria-components';
import { css, cx } from '@emotion/css';
import { useMergedRefs } from '../../hooks/useMergedRefs';
import { useProperFocus } from '../../hooks/useProperFocus';
import { styles, theme } from '../../style';
type InputProps = ComponentPropsWithRef<typeof ReactAriaInput> & {
autoSelect?: boolean;
export const defaultInputStyle = {
outline: 0,
backgroundColor: theme.tableBackground,
color: theme.formInputText,
margin: 0,
padding: 5,
borderRadius: 4,
border: '1px solid ' + theme.formInputBorder,
};
type InputProps = InputHTMLAttributes<HTMLInputElement> & {
style?: CSSProperties;
inputRef?: Ref<HTMLInputElement>;
onEnter?: (event: KeyboardEvent<HTMLInputElement>) => void;
onEscape?: (event: KeyboardEvent<HTMLInputElement>) => void;
onChangeValue?: (newValue: string) => void;
onUpdate?: (newValue: string) => void;
focused?: boolean;
};
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
autoSelect,
className = '',
onEnter,
onEscape,
onChangeValue,
onUpdate,
...props
},
ref,
) => {
const inputRef = useRef<HTMLInputElement>(null);
const mergedRef = useMergedRefs<HTMLInputElement>(inputRef, ref);
export function Input({
style,
inputRef,
onEnter,
onEscape,
onChangeValue,
onUpdate,
focused,
className,
...nativeProps
}: InputProps) {
const ref = useRef<HTMLInputElement>(null);
useProperFocus(ref, focused);
useEffect(() => {
if (autoSelect) {
// Select on mount does not work properly for inputs that are inside a dialog.
// See https://github.com/facebook/react/issues/23301#issuecomment-1656908450
// for the reason why we need to use setTimeout here.
setTimeout(() => inputRef.current?.select());
}
}, [autoSelect]);
const mergedRef = useMergedRefs<HTMLInputElement>(ref, inputRef);
const defaultInputClassName = useMemo(
() =>
return (
<input
ref={mergedRef}
className={cx(
css(
defaultInputStyle,
{
outline: 0,
backgroundColor: theme.tableBackground,
color: theme.formInputText,
margin: 0,
padding: 5,
borderRadius: 4,
border: '1px solid ' + theme.formInputBorder,
whiteSpace: 'nowrap',
overflow: 'hidden',
flexShrink: 0,
'&[data-focused]': {
':focus': {
border: '1px solid ' + theme.formInputBorderSelected,
boxShadow: '0 1px 1px ' + theme.formInputShadowSelected,
},
'&::placeholder': { color: theme.formInputTextPlaceholder },
'::placeholder': { color: theme.formInputTextPlaceholder },
},
styles.smallText,
style,
),
[],
);
className,
)}
{...nativeProps}
onKeyDown={e => {
nativeProps.onKeyDown?.(e);
return (
<ReactAriaInput
ref={mergedRef}
{...props}
className={
typeof className === 'function'
? renderProps => cx(defaultInputClassName, className(renderProps))
: cx(defaultInputClassName, className)
if (e.key === 'Enter' && onEnter) {
onEnter(e);
}
onKeyDown={e => {
props.onKeyDown?.(e);
if (e.key === 'Enter' && onEnter) {
onEnter(e);
}
if (e.key === 'Escape' && onEscape) {
onEscape(e);
}
}}
onBlur={e => {
onUpdate?.(e.target.value);
props.onBlur?.(e);
}}
onChange={e => {
onChangeValue?.(e.target.value);
props.onChange?.(e);
}}
/>
);
},
);
Input.displayName = 'Input';
type BigInputProps = InputProps;
export const BigInput = forwardRef<HTMLInputElement, BigInputProps>(
({ className, ...props }, ref) => {
const defaultClassName = useMemo(
() =>
String(
css({
padding: 10,
fontSize: 15,
'&, &[data-focused]': { border: 'none', ...styles.shadow },
}),
),
[],
);
return (
<Input
ref={ref}
{...props}
className={renderProps =>
typeof className === 'function'
? cx(defaultClassName, className(renderProps))
: cx(defaultClassName, className)
if (e.key === 'Escape' && onEscape) {
onEscape(e);
}
/>
);
},
);
}}
onBlur={e => {
onUpdate?.(e.target.value);
nativeProps.onBlur?.(e);
}}
onChange={e => {
onChangeValue?.(e.target.value);
nativeProps.onChange?.(e);
}}
/>
);
}
BigInput.displayName = 'BigInput';
export function BigInput(props: InputProps) {
return (
<Input
{...props}
style={{
padding: 10,
fontSize: 15,
border: 'none',
...styles.shadow,
':focus': { border: 'none', ...styles.shadow },
...props.style,
}}
/>
);
}

View File

@@ -1,48 +1,74 @@
import { type ComponentPropsWithRef, type ReactNode, forwardRef } from 'react';
import { css, cx } from '@emotion/css';
import {
useState,
type ComponentProps,
type ReactNode,
type CSSProperties,
} from 'react';
import { theme } from '../../style';
import { Input } from './Input';
import { Input, defaultInputStyle } from './Input';
import { View } from './View';
type InputWithContentProps = ComponentPropsWithRef<typeof Input> & {
type InputWithContentProps = ComponentProps<typeof Input> & {
leftContent?: ReactNode;
rightContent?: ReactNode;
containerClassName?: string;
inputStyle?: CSSProperties;
focusStyle?: CSSProperties;
style?: CSSProperties;
getStyle?: (focused: boolean) => CSSProperties;
};
export const InputWithContent = forwardRef<
HTMLInputElement,
InputWithContentProps
>(({ leftContent, rightContent, containerClassName, ...props }, ref) => {
export function InputWithContent({
leftContent,
rightContent,
inputStyle,
focusStyle,
style,
getStyle,
...props
}: InputWithContentProps) {
const [focused, setFocused] = useState(props.focused ?? false);
return (
<View
className={cx(
css({
backgroundColor: theme.tableBackground,
color: theme.formInputText,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 4,
'&:focus-within': {
style={{
...defaultInputStyle,
padding: 0,
flexDirection: 'row',
alignItems: 'center',
...style,
...(focused &&
(focusStyle ?? {
boxShadow: '0 0 0 1px ' + theme.formInputShadowSelected,
},
'& input, input[data-focused], input[data-hovered]': {
})),
...getStyle?.(focused),
}}
>
{leftContent}
<Input
{...props}
focused={focused}
style={{
width: '100%',
...inputStyle,
flex: 1,
'&, &:focus, &:hover': {
border: 0,
backgroundColor: 'transparent',
boxShadow: 'none',
color: 'inherit',
},
}),
containerClassName,
)}
>
{leftContent}
<Input ref={ref} {...props} />
}}
onFocus={e => {
setFocused(true);
props.onFocus?.(e);
}}
onBlur={e => {
setFocused(false);
props.onBlur?.(e);
}}
/>
{rightContent}
</View>
);
});
InputWithContent.displayName = 'InputWithContent';
}

Some files were not shown because too many files have changed in this diff Show More