[Redux] Move tags states from queriesSlice to tagsSlice

This commit is contained in:
Joel Jeremy Marquez
2025-08-21 08:52:24 -07:00
parent d108b0f3c5
commit b6b19fe52c
12 changed files with 203 additions and 167 deletions

View File

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

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 { createTag } from '@desktop-client/tags/tagsSlice';
import { useDispatch } from '@desktop-client/redux';
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 { updateTag } from '@desktop-client/tags/tagsSlice';
import { useDispatch } from '@desktop-client/redux';
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 { deleteTag, updateTag } from '@desktop-client/tags/tagsSlice';
import { useDispatch } from '@desktop-client/redux';
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 { getTags } from '@desktop-client/tags/tagsSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
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

@@ -2,7 +2,7 @@
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 { type TransactionEntity } from 'loot-core/types/models';
import { markUpdatedAccounts } from '@desktop-client/accounts/accountsSlice';
import { resetApp } from '@desktop-client/app/appSlice';
@@ -15,20 +15,12 @@ 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 = {
@@ -77,124 +69,12 @@ const queriesSlice = createSlice({
) {
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 = {
@@ -297,28 +177,7 @@ 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;
}
export const { setNewTransactions, updateNewTransactions, setLastTransaction } =
queriesSlice.actions;

View File

@@ -41,6 +41,10 @@ import {
name as queriesSliceName,
reducer as queriesSliceReducer,
} from '@desktop-client/queries/queriesSlice';
import {
name as tagsSliceName,
reducer as tagsSliceReducer,
} from '@desktop-client/tags/tagsSlice';
import {
name as usersSliceName,
reducer as usersSliceReducer,
@@ -56,6 +60,7 @@ const appReducer = combineReducers({
[payeesSliceName]: payeesSliceReducer,
[prefsSliceName]: prefsSliceReducer,
[queriesSliceName]: queriesSliceReducer,
[tagsSliceName]: tagsSliceReducer,
[usersSliceName]: usersSliceReducer,
});

View File

@@ -42,6 +42,10 @@ import {
name as queriesSliceName,
reducer as queriesSliceReducer,
} from '@desktop-client/queries/queriesSlice';
import {
name as tagsSliceName,
reducer as tagsSliceReducer,
} from '@desktop-client/tags/tagsSlice';
import {
name as usersSliceName,
reducer as usersSliceReducer,
@@ -57,6 +61,7 @@ const rootReducer = combineReducers({
[payeesSliceName]: payeesSliceReducer,
[prefsSliceName]: prefsSliceReducer,
[queriesSliceName]: queriesSliceReducer,
[tagsSliceName]: tagsSliceReducer,
[usersSliceName]: usersSliceReducer,
});

View File

@@ -0,0 +1,165 @@
import { createSlice } from '@reduxjs/toolkit';
import { send } from 'loot-core/platform/client/fetch';
import { 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

@@ -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

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