mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 20:44:32 -05:00
[Redux] Move tags states from queriesSlice to tagsSlice
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
165
packages/desktop-client/src/tags/tagsSlice.ts
Normal file
165
packages/desktop-client/src/tags/tagsSlice.ts
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface Tag {
|
||||
export interface TagEntity {
|
||||
id: string;
|
||||
tag: string;
|
||||
color?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user