diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index e5e995e706..db6716d910 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -102,7 +102,9 @@ export function Modals() { return budgetId ? : null; case 'category-automations-edit': - return budgetId ? : null; + return budgetId ? ( + + ) : null; case 'keyboard-shortcuts': // don't show the hotkey help modal when a budget is not open diff --git a/packages/desktop-client/src/components/NotesButton.tsx b/packages/desktop-client/src/components/NotesButton.tsx index d42753d839..3aaf11b26e 100644 --- a/packages/desktop-client/src/components/NotesButton.tsx +++ b/packages/desktop-client/src/components/NotesButton.tsx @@ -14,6 +14,7 @@ import { Popover } from '@actual-app/components/popover'; import { theme } from '@actual-app/components/theme'; import { Tooltip } from '@actual-app/components/tooltip'; import { View } from '@actual-app/components/view'; +import { css, cx } from '@emotion/css'; import { send } from 'loot-core/platform/client/fetch'; @@ -27,6 +28,7 @@ type NotesButtonProps = { height?: number; defaultColor?: string; tooltipPosition?: ComponentProps['placement']; + showPlaceholder?: boolean; style?: CSSProperties; }; export function NotesButton({ @@ -35,6 +37,7 @@ export function NotesButton({ height = 12, defaultColor = theme.buttonNormalText, tooltipPosition = 'bottom start', + showPlaceholder = false, style, }: NotesButtonProps) { const { t } = useTranslation(); @@ -73,13 +76,19 @@ export function NotesButton({ ref={triggerRef} variant="bare" aria-label={t('View notes')} - className={!hasNotes && !isOpen ? 'hover-visible' : ''} - style={{ - color: defaultColor, - ...style, - ...(hasNotes && { display: 'flex !important' }), - ...(isOpen && { color: theme.buttonNormalText }), - }} + className={cx( + css({ + color: defaultColor, + ...style, + ...(showPlaceholder && { + opacity: hasNotes || isOpen ? 1 : 0.3, + }), + ...(isOpen && { color: theme.buttonNormalText }), + '&:hover': { opacity: 1 }, + }), + !hasNotes && !isOpen && !showPlaceholder ? 'hover-visible' : '', + )} + data-placeholder={showPlaceholder} onPress={() => { setIsOpen(true); }} diff --git a/packages/desktop-client/src/components/budget/SidebarCategory.tsx b/packages/desktop-client/src/components/budget/SidebarCategory.tsx index c9c08ec8bb..0998a01734 100644 --- a/packages/desktop-client/src/components/budget/SidebarCategory.tsx +++ b/packages/desktop-client/src/components/budget/SidebarCategory.tsx @@ -15,12 +15,10 @@ import { type CategoryEntity, } from 'loot-core/types/models'; -import { CategoryAutomationButton } from './goals/CategoryAutomationButton'; +import { SidebarCategoryButtons } from './SidebarCategoryButtons'; -import { NotesButton } from '@desktop-client/components/NotesButton'; import { InputCell } from '@desktop-client/components/table'; import { useContextMenu } from '@desktop-client/hooks/useContextMenu'; -import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag'; import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref'; type SidebarCategoryProps = { @@ -56,7 +54,6 @@ export function SidebarCategory({ onHideNewCategory, }: SidebarCategoryProps) { const { t } = useTranslation(); - const isGoalTemplatesUIEnabled = useFeatureFlag('goalTemplatesUIEnabled'); const [categoryExpandedStatePref] = useGlobalPref('categoryExpandedState'); const categoryExpandedState = categoryExpandedStatePref ?? 0; @@ -128,22 +125,11 @@ export function SidebarCategory({ /> - - {!goalsShown && isGoalTemplatesUIEnabled && ( - - - - )} - - - + ); diff --git a/packages/desktop-client/src/components/budget/SidebarCategoryButtons.tsx b/packages/desktop-client/src/components/budget/SidebarCategoryButtons.tsx new file mode 100644 index 0000000000..8b549c09f1 --- /dev/null +++ b/packages/desktop-client/src/components/budget/SidebarCategoryButtons.tsx @@ -0,0 +1,53 @@ +import { theme } from '@actual-app/components/theme'; +import { View } from '@actual-app/components/view'; + +import { type CategoryEntity } from 'loot-core/types/models/category'; + +import { CategoryAutomationButton } from './goals/CategoryAutomationButton'; + +import { NotesButton } from '@desktop-client/components/NotesButton'; +import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag'; +import { useNotes } from '@desktop-client/hooks/useNotes'; + +type SidebarCategoryButtonsProps = { + category: CategoryEntity; + dragging: boolean; + goalsShown: boolean; +}; + +export const SidebarCategoryButtons = ({ + category, + dragging, + goalsShown, +}: SidebarCategoryButtonsProps) => { + const isGoalTemplatesUIEnabled = useFeatureFlag('goalTemplatesUIEnabled'); + const notes = useNotes(category.id) || ''; + + return ( + <> + + {!goalsShown && isGoalTemplatesUIEnabled && ( + + + + )} + + + + + ); +}; diff --git a/packages/desktop-client/src/components/budget/goals/BudgetAutomationEditor.tsx b/packages/desktop-client/src/components/budget/goals/BudgetAutomationEditor.tsx index 67d4860ddf..1d744ddb6c 100644 --- a/packages/desktop-client/src/components/budget/goals/BudgetAutomationEditor.tsx +++ b/packages/desktop-client/src/components/budget/goals/BudgetAutomationEditor.tsx @@ -1,3 +1,4 @@ +import { type ReactNode } from 'react'; import { useTranslation, Trans } from 'react-i18next'; import { InitialFocus } from '@actual-app/components/initial-focus'; @@ -20,7 +21,11 @@ import { ScheduleAutomation } from './editor/ScheduleAutomation'; import { SimpleAutomation } from './editor/SimpleAutomation'; import { WeekAutomation } from './editor/WeekAutomation'; -import { FormField, FormLabel } from '@desktop-client/components/forms'; +import { + FormField, + FormLabel, + FormTextLabel, +} from '@desktop-client/components/forms'; type BudgetAutomationEditorProps = { inline: boolean; @@ -73,7 +78,7 @@ export function BudgetAutomationEditor({ }: BudgetAutomationEditorProps) { const { t } = useTranslation(); - let automationEditor; + let automationEditor: ReactNode; switch (state.displayType) { case 'simple': automationEditor = ( @@ -144,7 +149,7 @@ export function BudgetAutomationEditor({ - + {displayTypeToDescription[state.displayType] ?? ( No description available diff --git a/packages/desktop-client/src/components/budget/goals/CategoryAutomationButton.tsx b/packages/desktop-client/src/components/budget/goals/CategoryAutomationButton.tsx index 078fefb23d..8a3c9e290a 100644 --- a/packages/desktop-client/src/components/budget/goals/CategoryAutomationButton.tsx +++ b/packages/desktop-client/src/components/budget/goals/CategoryAutomationButton.tsx @@ -4,34 +4,36 @@ import { useTranslation } from 'react-i18next'; import { Button } from '@actual-app/components/button'; import { SvgChartPie } from '@actual-app/components/icons/v1'; import { theme } from '@actual-app/components/theme'; +import { cx, css } from '@emotion/css'; -import { type Template } from 'loot-core/types/models/templates'; +import { type CategoryEntity } from 'loot-core/types/models'; import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag'; import { pushModal } from '@desktop-client/modals/modalsSlice'; import { useDispatch } from '@desktop-client/redux'; type CategoryAutomationButtonProps = { + category: CategoryEntity; width?: number; height?: number; defaultColor?: string; style?: CSSProperties; + showPlaceholder?: boolean; }; export function CategoryAutomationButton({ + category, width = 12, height = 12, defaultColor = theme.buttonNormalText, style, + showPlaceholder = false, }: CategoryAutomationButtonProps) { const { t } = useTranslation(); - - const automations: Template[] = []; - const hasAutomations = !!automations.length; - const dispatch = useDispatch(); const goalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); const goalTemplatesUIEnabled = useFeatureFlag('goalTemplatesUIEnabled'); + const hasAutomations = !!category.goal_def?.length; if (!goalTemplatesEnabled || !goalTemplatesUIEnabled) { return null; @@ -41,14 +43,26 @@ export function CategoryAutomationButton({ + + ); +} + +export function BudgetAutomationsModal({ categoryId }: { categoryId: string }) { const { t } = useTranslation(); - // HACK: This is a placeholder for the actual data. - // We should eventually load it using a data hook. - const [templates, setTemplates] = useState([ - { - type: 'average', - numMonths: 3, - directive: 'template', - priority: DEFAULT_PRIORITY, - id: uniqueId(), - }, - ]); + const [automations, setAutomations] = useState>( + {}, + ); + const { loading } = useBudgetAutomations({ + categoryId, + onLoaded: setAutomations, + }); const schedulesQuery = useMemo(() => q('schedules').select('*'), []); const { schedules } = useSchedules({ @@ -43,21 +113,22 @@ export function BudgetAutomationsModal() { const categories = useBudgetAutomationCategories(); - const onAdd = () => { - setTemplates([ - ...templates, - { - type: 'average', - numMonths: 3, - directive: 'template', - priority: DEFAULT_PRIORITY, - id: uniqueId(), - }, - ]); - }; - const onSave = () => {}; - const onDelete = (index: number) => () => { - setTemplates([...templates.slice(0, index), ...templates.slice(index + 1)]); + const onSave = async (close: () => void) => { + if (!automations[categoryId]) { + close(); + return; + } + + await send('budget/set-category-automations', { + categoriesWithTemplates: [ + { + id: categoryId, + templates: automations[categoryId], + }, + ], + source: 'ui', + }); + close(); }; return ( @@ -68,40 +139,48 @@ export function BudgetAutomationsModal() { }} > {({ state: { close } }) => ( - <> + } /> - - {templates.map((template, index) => ( - - ))} - + - + )} ); diff --git a/packages/desktop-client/src/hooks/useBudgetAutomations.ts b/packages/desktop-client/src/hooks/useBudgetAutomations.ts new file mode 100644 index 0000000000..532e67ada5 --- /dev/null +++ b/packages/desktop-client/src/hooks/useBudgetAutomations.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react'; + +import { send } from 'loot-core/platform/client/fetch'; +import type { Template } from 'loot-core/types/models/templates'; + +export function useBudgetAutomations({ + categoryId, + onLoaded, +}: { + categoryId: string; + onLoaded: (automations: Record) => void; +}) { + const [automations, setAutomations] = useState>( + {}, + ); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let mounted = true; + async function fetchAutomations() { + setLoading(true); + const result = await send('budget/get-category-automations', categoryId); + if (mounted) { + setAutomations(result); + onLoaded(result); + setLoading(false); + } + } + fetchAutomations(); + return () => { + mounted = false; + }; + }, [categoryId, onLoaded]); + + return { automations, loading }; +} diff --git a/packages/desktop-client/src/modals/modalsSlice.ts b/packages/desktop-client/src/modals/modalsSlice.ts index bc7f584bb2..82d0bfa5c9 100644 --- a/packages/desktop-client/src/modals/modalsSlice.ts +++ b/packages/desktop-client/src/modals/modalsSlice.ts @@ -542,6 +542,9 @@ export type Modal = } | { name: 'category-automations-edit'; + options: { + categoryId: CategoryEntity['id']; + }; }; type OpenAccountCloseModalPayload = { diff --git a/upcoming-release-notes/5533.md b/upcoming-release-notes/5533.md new file mode 100644 index 0000000000..0d81a791b0 --- /dev/null +++ b/upcoming-release-notes/5533.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [jfdoming] +--- + +Connect automations UI to backend