mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 18:40:34 -05:00
Apply Template to All Categories in Group for Web (#3666)
* add function to apply template to multiple category and add button to group sidebar * add function to apply template to multiple category and add button to group sidebar * add correct month * clean up code * clean up code * clean up code * clean up code * add notification and clean up * add notification and clean up * add notification and clean up * add notification and clean up * add notification and clean up * add release note * excluded hidden categories * removed unused method from api * adjust template to run on already budgeted categories * fix typecheck * add apply multiple as budget action and remove from api * lint clean up * fix notification and remove log --------- Co-authored-by: dreptschar <dreptschar@gmail.com>
This commit is contained in:
@@ -28,6 +28,7 @@ export const BudgetCategories = memo(
|
||||
onSaveGroup,
|
||||
onDeleteCategory,
|
||||
onDeleteGroup,
|
||||
onApplyBudgetTemplatesInGroup,
|
||||
onReorderCategory,
|
||||
onReorderGroup,
|
||||
}) => {
|
||||
@@ -245,6 +246,7 @@ export const BudgetCategories = memo(
|
||||
onReorderCategory={onReorderCategory}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
onShowNewCategory={onShowNewCategory}
|
||||
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -28,6 +28,7 @@ export function BudgetTable(props) {
|
||||
onDeleteCategory,
|
||||
onSaveGroup,
|
||||
onDeleteGroup,
|
||||
onApplyBudgetTemplatesInGroup,
|
||||
onReorderCategory,
|
||||
onReorderGroup,
|
||||
onShowActivity,
|
||||
@@ -235,6 +236,7 @@ export function BudgetTable(props) {
|
||||
onReorderGroup={_onReorderGroup}
|
||||
onBudgetAction={onBudgetAction}
|
||||
onShowActivity={onShowActivity}
|
||||
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -25,6 +25,9 @@ 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']
|
||||
>;
|
||||
@@ -43,6 +46,7 @@ export function ExpenseGroup({
|
||||
onEditName,
|
||||
onSave,
|
||||
onDelete,
|
||||
onApplyBudgetTemplatesInGroup,
|
||||
onDragChange,
|
||||
onReorderGroup,
|
||||
onReorderCategory,
|
||||
@@ -125,6 +129,7 @@ export function ExpenseGroup({
|
||||
onEdit={onEditName}
|
||||
onSave={onSave}
|
||||
onDelete={onDelete}
|
||||
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
|
||||
onShowNewCategory={onShowNewCategory}
|
||||
/>
|
||||
<RenderMonths component={MonthComponent} args={{ group }} />
|
||||
|
||||
@@ -33,6 +33,7 @@ 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;
|
||||
@@ -48,11 +49,13 @@ 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);
|
||||
@@ -132,6 +135,12 @@ 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);
|
||||
}}
|
||||
@@ -143,6 +152,14 @@ 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>
|
||||
|
||||
@@ -265,6 +265,15 @@ 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));
|
||||
};
|
||||
@@ -366,6 +375,7 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
onMonthSelect={onMonthSelect}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onSaveGroup={onSaveGroup}
|
||||
onBudgetAction={onBudgetAction}
|
||||
|
||||
@@ -102,6 +102,16 @@ export function applyBudgetAction(month, type, args) {
|
||||
category: args.category,
|
||||
});
|
||||
break;
|
||||
case 'apply-multiple-templates':
|
||||
dispatch(
|
||||
addNotification(
|
||||
await send('budget/apply-multiple-templates', {
|
||||
month,
|
||||
categoryIds: args.categories,
|
||||
}),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'set-single-3-avg':
|
||||
await send('budget/set-n-month-avg', {
|
||||
month,
|
||||
|
||||
@@ -29,6 +29,10 @@ app.method(
|
||||
'budget/apply-goal-template',
|
||||
mutator(undoable(goalActions.applyTemplate)),
|
||||
);
|
||||
app.method(
|
||||
'budget/apply-multiple-templates',
|
||||
mutator(undoable(goalActions.applyMultipleCategoryTemplates)),
|
||||
);
|
||||
app.method(
|
||||
'budget/overwrite-goal-template',
|
||||
mutator(undoable(goalActions.overwriteTemplate)),
|
||||
|
||||
@@ -37,6 +37,23 @@ export async function overwriteTemplate({ month }) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function applyMultipleCategoryTemplates({ month, categoryIds }) {
|
||||
const placeholders = categoryIds.map(() => '?').join(', ');
|
||||
const query = `SELECT * FROM v_categories WHERE id IN (${placeholders})`;
|
||||
const categories = await db.all(query, categoryIds);
|
||||
await storeTemplates();
|
||||
const category_templates = await getTemplates(categories, 'template');
|
||||
const category_goals = await getTemplates(categories, 'goal');
|
||||
const ret = await processTemplate(
|
||||
month,
|
||||
true,
|
||||
category_templates,
|
||||
categories,
|
||||
);
|
||||
await processGoals(category_goals, month);
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function applySingleCategoryTemplate({ month, category }) {
|
||||
const categories = await db.all(`SELECT * FROM v_categories WHERE id = ?`, [
|
||||
category,
|
||||
@@ -48,7 +65,7 @@ export async function applySingleCategoryTemplate({ month, category }) {
|
||||
month,
|
||||
true,
|
||||
category_templates,
|
||||
categories[0],
|
||||
categories,
|
||||
);
|
||||
await processGoals(category_goals, month, categories[0]);
|
||||
return ret;
|
||||
@@ -135,7 +152,19 @@ async function getTemplates(category, directive: string) {
|
||||
for (let ll = 0; ll < goal_def.length; ll++) {
|
||||
templates[goal_def[ll].id] = JSON.parse(goal_def[ll].goal_def);
|
||||
}
|
||||
if (category) {
|
||||
if (Array.isArray(category)) {
|
||||
const multipleCategoryTemplates = [];
|
||||
for (let dd = 0; dd < category.length; dd++) {
|
||||
const categoryId = category[dd].id;
|
||||
if (templates[categoryId] !== undefined) {
|
||||
multipleCategoryTemplates[categoryId] = templates[categoryId];
|
||||
multipleCategoryTemplates[categoryId] = multipleCategoryTemplates[
|
||||
categoryId
|
||||
].filter(t => t.directive === directive);
|
||||
}
|
||||
}
|
||||
return multipleCategoryTemplates;
|
||||
} else if (category) {
|
||||
const singleCategoryTemplate = [];
|
||||
if (templates[category.id] !== undefined) {
|
||||
singleCategoryTemplate[category.id] = templates[category.id].filter(
|
||||
@@ -174,11 +203,10 @@ async function processTemplate(
|
||||
let categories = [];
|
||||
const categories_remove = [];
|
||||
if (category) {
|
||||
categories[0] = category;
|
||||
categories = category;
|
||||
} else {
|
||||
categories = await getCategories();
|
||||
}
|
||||
|
||||
//clears templated categories
|
||||
for (let c = 0; c < categories.length; c++) {
|
||||
const category = categories[c];
|
||||
|
||||
@@ -79,4 +79,9 @@ export interface BudgetHandlers {
|
||||
month: string;
|
||||
category: string; //category id
|
||||
}) => Promise<void>;
|
||||
|
||||
'budget/apply-multiple-templates': (arg: {
|
||||
month: string;
|
||||
categoryIds: string[];
|
||||
}) => Promise<Notification>;
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/3666.md
Normal file
6
upcoming-release-notes/3666.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [Dreptschar]
|
||||
---
|
||||
|
||||
Adds a Button to Group Menu that allows users to apply all Budget Templates in this Group
|
||||
Reference in New Issue
Block a user