Compare commits

...

9 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
8c8ee889e6 Enable Make Transfer menu for child transactions 2025-08-21 15:09:32 -07:00
Joel Jeremy Marquez
82cbb9bffe Fix import 2025-08-21 15:09:32 -07:00
Joel Jeremy Marquez
d53b0499cf [Redux] Rename queriesSlice to transactionsSlice 2025-08-21 15:09:32 -07:00
Joel Jeremy Marquez
c512bed718 Fix lint + release notes 2025-08-21 15:09:32 -07:00
Joel Jeremy Marquez
b6b19fe52c [Redux] Move tags states from queriesSlice to tagsSlice 2025-08-21 15:09:32 -07:00
autofix-ci[bot]
d108b0f3c5 [autofix.ci] apply automated fixes 2025-08-21 15:09:32 -07:00
Joel Jeremy Marquez
c2602bd11a Move getAccountsById to accountsSlice 2025-08-21 15:09:32 -07:00
autofix-ci[bot]
e04d6e5716 [autofix.ci] apply automated fixes 2025-08-21 15:09:32 -07:00
Joel Jeremy Marquez
3fc4d1f85e Update packages/desktop-client/src/queries/queriesSlice.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-21 15:09:32 -07:00
22 changed files with 409 additions and 369 deletions

View File

@@ -16,9 +16,9 @@ import {
import { resetApp } from '@desktop-client/app/appSlice';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { markPayeesDirty } from '@desktop-client/payees/payeesSlice';
import { setNewTransactions } from '@desktop-client/queries/queriesSlice';
import { createAppAsyncThunk } from '@desktop-client/redux';
import { type AppDispatch } from '@desktop-client/redux/store';
import { setNewTransactions } from '@desktop-client/transactions/transactionsSlice';
const sliceName = 'account';
@@ -520,6 +520,97 @@ export const moveAccount = createAppAsyncThunk(
},
);
type ImportPreviewTransactionsPayload = {
accountId: string;
transactions: TransactionEntity[];
};
export const importPreviewTransactions = createAppAsyncThunk(
`${sliceName}/importPreviewTransactions`,
async (
{ accountId, transactions }: ImportPreviewTransactionsPayload,
{ dispatch },
) => {
const { errors = [], updatedPreview } = await send('transactions-import', {
accountId,
transactions,
isPreview: true,
});
errors.forEach(error => {
dispatch(
addNotification({
notification: {
type: 'error',
message: error.message,
},
}),
);
});
return updatedPreview;
},
);
type ImportTransactionsPayload = {
accountId: string;
transactions: TransactionEntity[];
reconcile: boolean;
};
export const importTransactions = createAppAsyncThunk(
`${sliceName}/importTransactions`,
async (
{ accountId, transactions, reconcile }: ImportTransactionsPayload,
{ dispatch },
) => {
if (!reconcile) {
await send('api/transactions-add', {
accountId,
transactions,
});
return true;
}
const {
errors = [],
added,
updated,
} = await send('transactions-import', {
accountId,
transactions,
isPreview: false,
});
errors.forEach(error => {
dispatch(
addNotification({
notification: {
type: 'error',
message: error.message,
},
}),
);
});
dispatch(
setNewTransactions({
newTransactions: added,
matchedTransactions: updated,
}),
);
dispatch(
markUpdatedAccounts({
ids: added.length > 0 ? [accountId] : [],
}),
);
return added.length > 0 || updated.length > 0;
},
);
export const getAccountsById = memoizeOne(
(accounts: AccountEntity[] | null | undefined) => groupById(accounts),
);

View File

@@ -85,9 +85,9 @@ import {
pagedQuery,
type PagedQuery,
} from '@desktop-client/queries/pagedQuery';
import { updateNewTransactions } from '@desktop-client/queries/queriesSlice';
import { useSelector, useDispatch } from '@desktop-client/redux';
import { type AppDispatch } from '@desktop-client/redux/store';
import { updateNewTransactions } from '@desktop-client/transactions/transactionsSlice';
type ConditionEntity = Partial<RuleConditionEntity> | TransactionFilterEntity;
@@ -1932,9 +1932,11 @@ export function Account() {
const location = useLocation();
const { grouped: categoryGroups } = useCategories();
const newTransactions = useSelector(state => state.queries.newTransactions);
const newTransactions = useSelector(
state => state.transactions.newTransactions,
);
const matchedTransactions = useSelector(
state => state.queries.matchedTransactions,
state => state.transactions.matchedTransactions,
);
const accounts = useAccounts();
const payees = usePayees();

View File

@@ -79,8 +79,8 @@ import {
} from '@desktop-client/hooks/useSingleActiveEditForm';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { aqlQuery } from '@desktop-client/queries/aqlQuery';
import { setLastTransaction } from '@desktop-client/queries/queriesSlice';
import { useSelector, useDispatch } from '@desktop-client/redux';
import { setLastTransaction } from '@desktop-client/transactions/transactionsSlice';
function getFieldName(transactionId, field) {
return `${field}-${transactionId}`;

View File

@@ -95,7 +95,9 @@ export function TransactionListItem({
const transferAccount = useAccount(payee?.transfer_acct || '');
const isPreview = isPreviewId(transaction?.id || '');
const newTransactions = useSelector(state => state.queries.newTransactions);
const newTransactions = useSelector(
state => state.transactions.newTransactions,
);
const { longPressProps } = useLongPress({
accessibilityDescription: 'Long press to select multiple transactions',

View File

@@ -27,6 +27,10 @@ import {
stripCsvImportTransaction,
} from './utils';
import {
importPreviewTransactions,
importTransactions,
} from '@desktop-client/accounts/accountsSlice';
import {
Modal,
ModalCloseButton,
@@ -41,10 +45,6 @@ import { useCategories } from '@desktop-client/hooks/useCategories';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useSyncedPrefs } from '@desktop-client/hooks/useSyncedPrefs';
import { reloadPayees } from '@desktop-client/payees/payeesSlice';
import {
importPreviewTransactions,
importTransactions,
} from '@desktop-client/queries/queriesSlice';
import { useDispatch } from '@desktop-client/redux';
function getFileType(filepath) {

View File

@@ -21,8 +21,8 @@ import {
useSelected,
} from '@desktop-client/hooks/useSelected';
import { useTags } from '@desktop-client/hooks/useTags';
import { deleteAllTags, findTags } from '@desktop-client/queries/queriesSlice';
import { useDispatch } from '@desktop-client/redux';
import { deleteAllTags, findTags } from '@desktop-client/tags/tagsSlice';
export function ManageTags() {
const { t } = useTranslation();

View File

@@ -14,7 +14,7 @@ import { Stack } from '@actual-app/components/stack';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { type Tag } from 'loot-core/types/models';
import { type TagEntity } from 'loot-core/types/models';
import {
InputCell,
@@ -23,11 +23,11 @@ import {
} from '@desktop-client/components/table';
import { useProperFocus } from '@desktop-client/hooks/useProperFocus';
import { useTagCSS } from '@desktop-client/hooks/useTagCSS';
import { createTag } from '@desktop-client/queries/queriesSlice';
import { useDispatch } from '@desktop-client/redux';
import { createTag } from '@desktop-client/tags/tagsSlice';
type TagCreationRowProps = {
tags: Tag[];
tags: TagEntity[];
onClose: () => void;
};

View File

@@ -3,14 +3,14 @@ import { type RefObject } from 'react';
import { Button } from '@actual-app/components/button';
import { ColorPicker } from '@actual-app/components/color-picker';
import { type Tag } from 'loot-core/types/models';
import { type TagEntity } from 'loot-core/types/models';
import { useTagCSS } from '@desktop-client/hooks/useTagCSS';
import { updateTag } from '@desktop-client/queries/queriesSlice';
import { useDispatch } from '@desktop-client/redux';
import { updateTag } from '@desktop-client/tags/tagsSlice';
type TagEditorProps = {
tag: Tag;
tag: TagEntity;
ref: RefObject<HTMLButtonElement | null>;
};

View File

@@ -7,7 +7,7 @@ import { Popover } from '@actual-app/components/popover';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { type Tag } from 'loot-core/types/models';
import { type TagEntity } from 'loot-core/types/models';
import { TagEditor } from './TagEditor';
@@ -22,11 +22,11 @@ import { useContextMenu } from '@desktop-client/hooks/useContextMenu';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useProperFocus } from '@desktop-client/hooks/useProperFocus';
import { useSelectedDispatch } from '@desktop-client/hooks/useSelected';
import { deleteTag, updateTag } from '@desktop-client/queries/queriesSlice';
import { useDispatch } from '@desktop-client/redux';
import { deleteTag, updateTag } from '@desktop-client/tags/tagsSlice';
type TagRowProps = {
tag: Tag;
tag: TagEntity;
hovered?: boolean;
selected?: boolean;
onHover: (id?: string) => void;

View File

@@ -1,13 +1,13 @@
import React from 'react';
import { type Tag } from 'loot-core/types/models';
import { type TagEntity } from 'loot-core/types/models';
import { TagRow } from './TagRow';
import { Table, useTableNavigator } from '@desktop-client/components/table';
type TagsListProps = {
tags: Tag[];
tags: TagEntity[];
selectedItems: Set<string>;
hoveredTag?: string;
onHover: (id?: string) => void;

View File

@@ -2,13 +2,13 @@ import { useEffect } from 'react';
import { useInitialMount } from './useInitialMount';
import { getTags } from '@desktop-client/queries/queriesSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
import { getTags } from '@desktop-client/tags/tagsSlice';
export function useTags() {
const dispatch = useDispatch();
const isInitialMount = useInitialMount();
const isTagsDirty = useSelector(state => state.queries.isTagsDirty);
const isTagsDirty = useSelector(state => state.tags.isTagsDirty);
useEffect(() => {
if (isInitialMount || isTagsDirty) {
@@ -16,5 +16,5 @@ export function useTags() {
}
}, [dispatch, isInitialMount, isTagsDirty]);
return useSelector(state => state.queries.tags);
return useSelector(state => state.tags.tags);
}

View File

@@ -31,8 +31,9 @@ import * as notificationsSlice from './notifications/notificationsSlice';
import * as payeesSlice from './payees/payeesSlice';
import * as prefsSlice from './prefs/prefsSlice';
import { aqlQuery } from './queries/aqlQuery';
import * as queriesSlice from './queries/queriesSlice';
import { store } from './redux/store';
import * as tagsSlice from './tags/tagsSlice';
import * as transactionsSlice from './transactions/transactionsSlice';
import { redo, undo } from './undo';
import * as usersSlice from './users/usersSlice';
@@ -46,7 +47,8 @@ const boundActions = bindActionCreators(
...notificationsSlice.actions,
...payeesSlice.actions,
...prefsSlice.actions,
...queriesSlice.actions,
...transactionsSlice.actions,
...tagsSlice.actions,
...usersSlice.actions,
},
store.dispatch,

View File

@@ -1,324 +0,0 @@
// @ts-strict-ignore
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { send } from 'loot-core/platform/client/fetch';
import { type TransactionEntity, type Tag } from 'loot-core/types/models';
import { markUpdatedAccounts } from '@desktop-client/accounts/accountsSlice';
import { resetApp } from '@desktop-client/app/appSlice';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { createAppAsyncThunk } from '@desktop-client/redux';
const sliceName = 'queries';
type QueriesState = {
newTransactions: Array<TransactionEntity['id']>;
matchedTransactions: Array<TransactionEntity['id']>;
lastTransaction: TransactionEntity | null;
tags: Tag[];
isTagsLoading: boolean;
isTagsLoaded: boolean;
isTagsDirty: boolean;
};
const initialState: QueriesState = {
newTransactions: [],
matchedTransactions: [],
lastTransaction: null,
tags: [],
isTagsLoading: false,
isTagsLoaded: false,
isTagsDirty: false,
};
type SetNewTransactionsPayload = {
newTransactions: Array<TransactionEntity['id']>;
matchedTransactions: Array<TransactionEntity['id']>;
};
type UpdateNewTransactionsPayload = {
id: TransactionEntity['id'];
};
type SetLastTransactionPayload = {
transaction: TransactionEntity;
};
const queriesSlice = createSlice({
name: sliceName,
initialState,
reducers: {
setNewTransactions(
state,
action: PayloadAction<SetNewTransactionsPayload>,
) {
state.newTransactions = action.payload.newTransactions
? [...state.newTransactions, ...action.payload.newTransactions]
: state.newTransactions;
state.matchedTransactions = action.payload.matchedTransactions
? [...state.matchedTransactions, ...action.payload.matchedTransactions]
: state.matchedTransactions;
},
updateNewTransactions(
state,
action: PayloadAction<UpdateNewTransactionsPayload>,
) {
state.newTransactions = state.newTransactions.filter(
id => id !== action.payload.id,
);
state.matchedTransactions = state.matchedTransactions.filter(
id => id !== action.payload.id,
);
},
setLastTransaction(
state,
action: PayloadAction<SetLastTransactionPayload>,
) {
state.lastTransaction = action.payload.transaction;
},
markTagsDirty(state) {
_markTagsDirty(state);
},
},
extraReducers: builder => {
// App
builder.addCase(resetApp, () => initialState);
// Tags
builder.addCase(createTag.fulfilled, _markTagsDirty);
builder.addCase(deleteTag.fulfilled, _markTagsDirty);
builder.addCase(deleteAllTags.fulfilled, _markTagsDirty);
builder.addCase(updateTag.fulfilled, _markTagsDirty);
builder.addCase(reloadTags.fulfilled, (state, action) => {
_loadTags(state, action.payload);
});
builder.addCase(reloadTags.rejected, state => {
state.isTagsLoading = false;
});
builder.addCase(reloadTags.pending, state => {
state.isTagsLoading = true;
});
builder.addCase(getTags.fulfilled, (state, action) => {
_loadTags(state, action.payload);
});
builder.addCase(getTags.rejected, state => {
state.isTagsLoading = false;
});
builder.addCase(getTags.pending, state => {
state.isTagsLoading = true;
});
builder.addCase(findTags.fulfilled, (state, action) => {
_loadTags(state, action.payload);
});
builder.addCase(findTags.rejected, state => {
state.isTagsLoading = false;
});
builder.addCase(findTags.pending, state => {
state.isTagsLoading = true;
});
},
});
export const getTags = createAppAsyncThunk(
`${sliceName}/getTags`,
async () => {
const tags: Tag[] = await send('tags-get');
return tags;
},
{
condition: (_, { getState }) => {
const { queries } = getState();
return (
!queries.isTagsLoading && (queries.isTagsDirty || !queries.isTagsLoaded)
);
},
},
);
export const reloadTags = createAppAsyncThunk(
`${sliceName}/reloadTags`,
async () => {
const tags: Tag[] = await send('tags-get');
return tags;
},
);
export const createTag = createAppAsyncThunk(
`${sliceName}/createTag`,
async ({ tag, color, description }: Omit<Tag, 'id'>) => {
const id = await send('tags-create', { tag, color, description });
return id;
},
);
export const deleteTag = createAppAsyncThunk(
`${sliceName}/deleteTag`,
async (tag: Tag) => {
const id = await send('tags-delete', tag);
return id;
},
);
export const deleteAllTags = createAppAsyncThunk(
`${sliceName}/deleteAllTags`,
async (ids: Array<Tag['id']>) => {
const id = await send('tags-delete-all', ids);
return id;
},
);
export const updateTag = createAppAsyncThunk(
`${sliceName}/updateTag`,
async (tag: Tag) => {
const id = await send('tags-update', tag);
return id;
},
);
export const findTags = createAppAsyncThunk(
`${sliceName}/findTags`,
async () => {
const tags: Tag[] = await send('tags-find');
return tags;
},
);
// Transaction actions
type ImportPreviewTransactionsPayload = {
accountId: string;
transactions: TransactionEntity[];
};
export const importPreviewTransactions = createAppAsyncThunk(
`${sliceName}/importPreviewTransactions`,
async (
{ accountId, transactions }: ImportPreviewTransactionsPayload,
{ dispatch },
) => {
const { errors = [], updatedPreview } = await send('transactions-import', {
accountId,
transactions,
isPreview: true,
});
errors.forEach(error => {
dispatch(
addNotification({
notification: {
type: 'error',
message: error.message,
},
}),
);
});
return updatedPreview;
},
);
type ImportTransactionsPayload = {
accountId: string;
transactions: TransactionEntity[];
reconcile: boolean;
};
export const importTransactions = createAppAsyncThunk(
`${sliceName}/importTransactions`,
async (
{ accountId, transactions, reconcile }: ImportTransactionsPayload,
{ dispatch },
) => {
if (!reconcile) {
await send('api/transactions-add', {
accountId,
transactions,
});
return true;
}
const {
errors = [],
added,
updated,
} = await send('transactions-import', {
accountId,
transactions,
isPreview: false,
});
errors.forEach(error => {
dispatch(
addNotification({
notification: {
type: 'error',
message: error.message,
},
}),
);
});
const { setNewTransactions } = queriesSlice.actions;
dispatch(
setNewTransactions({
newTransactions: added,
matchedTransactions: updated,
}),
);
dispatch(
markUpdatedAccounts({
ids: added.length > 0 ? [accountId] : [],
}),
);
return added.length > 0 || updated.length > 0;
},
);
// Slice exports
export const { name, reducer, getInitialState } = queriesSlice;
export const actions = {
...queriesSlice.actions,
importPreviewTransactions,
importTransactions,
getTags,
createTag,
updateTag,
deleteTag,
deleteAllTags,
findTags,
};
export const {
setNewTransactions,
updateNewTransactions,
setLastTransaction,
markTagsDirty,
} = queriesSlice.actions;
function _loadTags(state: QueriesState, tags: QueriesState['tags']) {
state.tags = tags;
state.isTagsLoading = false;
state.isTagsLoaded = true;
state.isTagsDirty = false;
}
function _markTagsDirty(state: QueriesState) {
state.isTagsDirty = true;
}

View File

@@ -38,9 +38,13 @@ import {
reducer as prefsSliceReducer,
} from '@desktop-client/prefs/prefsSlice';
import {
name as queriesSliceName,
reducer as queriesSliceReducer,
} from '@desktop-client/queries/queriesSlice';
name as tagsSliceName,
reducer as tagsSliceReducer,
} from '@desktop-client/tags/tagsSlice';
import {
name as transactionsSliceName,
reducer as transactionsSliceReducer,
} from '@desktop-client/transactions/transactionsSlice';
import {
name as usersSliceName,
reducer as usersSliceReducer,
@@ -55,7 +59,8 @@ const appReducer = combineReducers({
[notificationsSliceName]: notificationsSliceReducer,
[payeesSliceName]: payeesSliceReducer,
[prefsSliceName]: prefsSliceReducer,
[queriesSliceName]: queriesSliceReducer,
[transactionsSliceName]: transactionsSliceReducer,
[tagsSliceName]: tagsSliceReducer,
[usersSliceName]: usersSliceReducer,
});

View File

@@ -39,9 +39,13 @@ import {
reducer as prefsSliceReducer,
} from '@desktop-client/prefs/prefsSlice';
import {
name as queriesSliceName,
reducer as queriesSliceReducer,
} from '@desktop-client/queries/queriesSlice';
name as tagsSliceName,
reducer as tagsSliceReducer,
} from '@desktop-client/tags/tagsSlice';
import {
name as transactionsSliceName,
reducer as transactionsSliceReducer,
} from '@desktop-client/transactions/transactionsSlice';
import {
name as usersSliceName,
reducer as usersSliceReducer,
@@ -56,7 +60,8 @@ const rootReducer = combineReducers({
[notificationsSliceName]: notificationsSliceReducer,
[payeesSliceName]: payeesSliceReducer,
[prefsSliceName]: prefsSliceReducer,
[queriesSliceName]: queriesSliceReducer,
[transactionsSliceName]: transactionsSliceReducer,
[tagsSliceName]: tagsSliceReducer,
[usersSliceName]: usersSliceReducer,
});

View File

@@ -0,0 +1,165 @@
import { createSlice } from '@reduxjs/toolkit';
import { send } from 'loot-core/platform/client/fetch';
import { type TagEntity } from 'loot-core/types/models';
import { resetApp } from '@desktop-client/app/appSlice';
import { createAppAsyncThunk } from '@desktop-client/redux';
const sliceName = 'tags';
type TagsState = {
tags: TagEntity[];
isTagsLoading: boolean;
isTagsLoaded: boolean;
isTagsDirty: boolean;
};
const initialState: TagsState = {
tags: [],
isTagsLoading: false,
isTagsLoaded: false,
isTagsDirty: false,
};
const tagSlice = createSlice({
name: sliceName,
initialState,
reducers: {
markTagsDirty(state) {
_markTagsDirty(state);
},
},
extraReducers: builder => {
builder.addCase(resetApp, () => initialState);
builder.addCase(createTag.fulfilled, _markTagsDirty);
builder.addCase(deleteTag.fulfilled, _markTagsDirty);
builder.addCase(deleteAllTags.fulfilled, _markTagsDirty);
builder.addCase(updateTag.fulfilled, _markTagsDirty);
builder.addCase(reloadTags.fulfilled, (state, action) => {
_loadTags(state, action.payload);
});
builder.addCase(reloadTags.rejected, state => {
state.isTagsLoading = false;
});
builder.addCase(reloadTags.pending, state => {
state.isTagsLoading = true;
});
builder.addCase(getTags.fulfilled, (state, action) => {
_loadTags(state, action.payload);
});
builder.addCase(getTags.rejected, state => {
state.isTagsLoading = false;
});
builder.addCase(getTags.pending, state => {
state.isTagsLoading = true;
});
builder.addCase(findTags.fulfilled, (state, action) => {
_loadTags(state, action.payload);
});
builder.addCase(findTags.rejected, state => {
state.isTagsLoading = false;
});
builder.addCase(findTags.pending, state => {
state.isTagsLoading = true;
});
},
});
export const getTags = createAppAsyncThunk(
`${sliceName}/getTags`,
async () => {
const tags: TagEntity[] = await send('tags-get');
return tags;
},
{
condition: (_, { getState }) => {
const { tags } = getState();
return !tags.isTagsLoading && (tags.isTagsDirty || !tags.isTagsLoaded);
},
},
);
export const reloadTags = createAppAsyncThunk(
`${sliceName}/reloadTags`,
async () => {
const tags: TagEntity[] = await send('tags-get');
return tags;
},
);
export const createTag = createAppAsyncThunk(
`${sliceName}/createTag`,
async ({ tag, color, description }: Omit<TagEntity, 'id'>) => {
const id = await send('tags-create', { tag, color, description });
return id;
},
);
export const deleteTag = createAppAsyncThunk(
`${sliceName}/deleteTag`,
async (tag: TagEntity) => {
const id = await send('tags-delete', tag);
return id;
},
);
export const deleteAllTags = createAppAsyncThunk(
`${sliceName}/deleteAllTags`,
async (ids: Array<TagEntity['id']>) => {
const id = await send('tags-delete-all', ids);
return id;
},
);
export const updateTag = createAppAsyncThunk(
`${sliceName}/updateTag`,
async (tag: TagEntity) => {
const id = await send('tags-update', tag);
return id;
},
);
export const findTags = createAppAsyncThunk(
`${sliceName}/findTags`,
async () => {
const tags: TagEntity[] = await send('tags-find');
return tags;
},
);
export const { name, reducer, getInitialState } = tagSlice;
export const actions = {
...tagSlice.actions,
getTags,
reloadTags,
createTag,
deleteTag,
deleteAllTags,
updateTag,
findTags,
};
export const { markTagsDirty } = tagSlice.actions;
function _loadTags(state: TagsState, tags: TagsState['tags']) {
state.tags = tags;
state.isTagsLoading = false;
state.isTagsLoaded = true;
state.isTagsDirty = false;
}
function _markTagsDirty(state: TagsState) {
state.isTagsDirty = true;
}

View File

@@ -0,0 +1,79 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { type TransactionEntity } from 'loot-core/types/models';
import { resetApp } from '@desktop-client/app/appSlice';
const sliceName = 'transactions';
type TransactionsState = {
newTransactions: Array<TransactionEntity['id']>;
matchedTransactions: Array<TransactionEntity['id']>;
lastTransaction: TransactionEntity | null;
};
const initialState: TransactionsState = {
newTransactions: [],
matchedTransactions: [],
lastTransaction: null,
};
type SetNewTransactionsPayload = {
newTransactions: Array<TransactionEntity['id']>;
matchedTransactions: Array<TransactionEntity['id']>;
};
type UpdateNewTransactionsPayload = {
id: TransactionEntity['id'];
};
type SetLastTransactionPayload = {
transaction: TransactionEntity;
};
const transactionsSlice = createSlice({
name: sliceName,
initialState,
reducers: {
setNewTransactions(
state,
action: PayloadAction<SetNewTransactionsPayload>,
) {
state.newTransactions = action.payload.newTransactions
? [...state.newTransactions, ...action.payload.newTransactions]
: state.newTransactions;
state.matchedTransactions = action.payload.matchedTransactions
? [...state.matchedTransactions, ...action.payload.matchedTransactions]
: state.matchedTransactions;
},
updateNewTransactions(
state,
action: PayloadAction<UpdateNewTransactionsPayload>,
) {
state.newTransactions = state.newTransactions.filter(
id => id !== action.payload.id,
);
state.matchedTransactions = state.matchedTransactions.filter(
id => id !== action.payload.id,
);
},
setLastTransaction(
state,
action: PayloadAction<SetLastTransactionPayload>,
) {
state.lastTransaction = action.payload.transaction;
},
},
extraReducers: builder => {
builder.addCase(resetApp, () => initialState);
},
});
export const { name, reducer, getInitialState } = transactionsSlice;
export const actions = {
...transactionsSlice.actions,
};
export const { setNewTransactions, updateNewTransactions, setLastTransaction } =
transactionsSlice.actions;

View File

@@ -1,4 +1,4 @@
import { Tag } from '../../types/models';
import { TagEntity } from '../../types/models';
import { createApp } from '../app';
import * as db from '../db';
import { mutator } from '../mutators';
@@ -22,7 +22,7 @@ app.method('tags-delete-all', mutator(deleteAllTags));
app.method('tags-update', mutator(undoable(updateTag)));
app.method('tags-find', mutator(findTags));
async function getTags(): Promise<Tag[]> {
async function getTags(): Promise<TagEntity[]> {
return await db.getTags();
}
@@ -30,7 +30,7 @@ async function createTag({
tag,
color = null,
description = null,
}: Omit<Tag, 'id'>): Promise<Tag> {
}: Omit<TagEntity, 'id'>): Promise<TagEntity> {
const allTags = await db.getAllTags();
const { id: tagId = null } = allTags.find(t => t.tag === tag) || {};
@@ -54,12 +54,14 @@ async function createTag({
return { id, tag, color, description };
}
async function deleteTag(tag: Tag): Promise<Tag['id']> {
async function deleteTag(tag: TagEntity): Promise<TagEntity['id']> {
await db.deleteTag(tag);
return tag.id;
}
async function deleteAllTags(ids: Array<Tag['id']>): Promise<Array<Tag['id']>> {
async function deleteAllTags(
ids: Array<TagEntity['id']>,
): Promise<Array<TagEntity['id']>> {
await batchMessages(async () => {
for (const id of ids) {
await db.deleteTag({ id });
@@ -68,12 +70,12 @@ async function deleteAllTags(ids: Array<Tag['id']>): Promise<Array<Tag['id']>> {
return ids;
}
async function updateTag(tag: Tag): Promise<Tag> {
async function updateTag(tag: TagEntity): Promise<TagEntity> {
await db.updateTag(tag);
return tag;
}
async function findTags(): Promise<Tag[]> {
async function findTags(): Promise<TagEntity[]> {
const taggedNotes = await db.findTags();
const tags = await getTags();

View File

@@ -5,10 +5,9 @@ export function validForTransfer(
toTransaction: TransactionEntity,
) {
if (
// no subtransactions
// not already a transfer
[fromTransaction, toTransaction].every(tran => {
return tran.transfer_id == null && tran.is_child === false;
return tran.transfer_id == null;
}) &&
fromTransaction.account !== toTransaction.account && // belong to different accounts
fromTransaction.amount + toTransaction.amount === 0 // amount must zero each other out

View File

@@ -1,4 +1,4 @@
export interface Tag {
export interface TagEntity {
id: string;
tag: string;
color?: string | null;

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [joel-jeremy]
---
[Redux] Move tags states from queriesSlice to tagsSlice

View File

@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [joel-jeremy]
---
Enable Make Transfer menu for child transactions