pl-fe: migrate account notes

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-10-23 18:45:07 +02:00
parent 2f1a9eb2d0
commit 1b85f50605
8 changed files with 87 additions and 86 deletions

View File

@ -65,6 +65,7 @@
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tanstack/react-pacer": "^0.16.4",
"@tanstack/react-query": "^5.62.11", "@tanstack/react-query": "^5.62.11",
"@transfem-org/sfm-js": "^0.24.6", "@transfem-org/sfm-js": "^0.24.6",
"@twemoji/svg": "^15.0.0", "@twemoji/svg": "^15.0.0",

View File

@ -1,53 +0,0 @@
import { __stub } from 'pl-fe/api';
import { mockStore, rootState } from 'pl-fe/jest/test-helpers';
import { submitAccountNote } from './account-notes';
describe('submitAccountNote()', () => {
let store: ReturnType<typeof mockStore>;
beforeEach(() => {
store = mockStore(rootState);
});
describe('with a successful API request', () => {
beforeEach(() => {
__stub((mock) => {
mock.onPost('/api/v1/accounts/1/note').reply(200, {});
});
});
it('post the note to the API', async() => {
const expectedActions = [
{ type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' },
{ type: 'ACCOUNT_NOTE_SUBMIT_SUCCESS', relationship: {} },
];
await store.dispatch(submitAccountNote('1', 'hello'));
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
describe('with an unsuccessful API request', () => {
beforeEach(() => {
__stub((mock) => {
mock.onPost('/api/v1/accounts/1/note').networkError();
});
});
it('should dispatch failed action', async() => {
const expectedActions = [
{ type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' },
{
type: 'ACCOUNT_NOTE_SUBMIT_FAIL',
error: new Error('Network Error'),
},
];
await store.dispatch(submitAccountNote('1', 'hello'));
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
});

View File

@ -1,12 +0,0 @@
import { importEntities } from 'pl-fe/actions/importer';
import { getClient } from '../api';
import type { AppDispatch, RootState } from 'pl-fe/store';
const submitAccountNote = (accountId: string, value: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState).accounts.updateAccountNote(accountId, value)
.then(response => dispatch(importEntities({ relationships: [response] })));
export { submitAccountNote };

View File

@ -3,7 +3,7 @@ import { useSettingsStore } from 'pl-fe/stores/settings';
import type { AppDispatch, RootState } from 'pl-fe/store'; import type { AppDispatch, RootState } from 'pl-fe/store';
const toggleMainWindow = () => const toggleChatPane = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const main = useSettingsStore.getState().settings.chats.mainWindow; const main = useSettingsStore.getState().settings.chats.mainWindow;
const state = main === 'minimized' ? 'open' : 'minimized'; const state = main === 'minimized' ? 'open' : 'minimized';
@ -11,5 +11,5 @@ const toggleMainWindow = () =>
}; };
export { export {
toggleMainWindow, toggleChatPane,
}; };

View File

@ -1,7 +1,7 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { toggleMainWindow } from 'pl-fe/actions/chats'; import { toggleChatPane } from 'pl-fe/actions/chats';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useChat } from 'pl-fe/queries/chats'; import { useChat } from 'pl-fe/queries/chats';
import { useSettings } from 'pl-fe/stores/settings'; import { useSettings } from 'pl-fe/stores/settings';
@ -45,13 +45,13 @@ const ChatProvider: React.FC<IChatProvider> = ({ children }) => {
setScreen(screen); setScreen(screen);
}; };
const toggleChatPane = () => dispatch(toggleMainWindow()); const handleChatPaneToggle = () => dispatch(toggleChatPane());
const value = useMemo(() => ({ const value = useMemo(() => ({
chat, chat,
isOpen, isOpen,
isUsingMainChatPage, isUsingMainChatPage,
toggleChatPane, toggleChatPane: handleChatPaneToggle,
screen, screen,
changeScreen, changeScreen,
currentChatId, currentChatId,

View File

@ -1,23 +1,15 @@
import debounce from 'lodash/debounce'; import { debounce } from '@tanstack/react-pacer/debouncer';
import React, { useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { submitAccountNote } from 'pl-fe/actions/account-notes';
import HStack from 'pl-fe/components/ui/hstack'; import HStack from 'pl-fe/components/ui/hstack';
import Text from 'pl-fe/components/ui/text'; import Text from 'pl-fe/components/ui/text';
import Textarea from 'pl-fe/components/ui/textarea'; import Textarea from 'pl-fe/components/ui/textarea';
import Widget from 'pl-fe/components/ui/widget'; import Widget from 'pl-fe/components/ui/widget';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useUpdateAccountNoteMutation } from 'pl-fe/queries/accounts/use-relationship';
import type { Account as AccountEntity } from 'pl-fe/normalizers/account'; import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
import type { AppDispatch } from 'pl-fe/store';
const onSave = debounce(
(dispatch: AppDispatch, accountId: string, value: string, callback: () => void) =>
dispatch(submitAccountNote(accountId, value)).then(() => callback()),
900,
);
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' }, placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
@ -30,9 +22,12 @@ interface IAccountNotePanel {
const AccountNotePanel: React.FC<IAccountNotePanel> = ({ account }) => { const AccountNotePanel: React.FC<IAccountNotePanel> = ({ account }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch();
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
const { mutate: updateAccountNote } = useUpdateAccountNoteMutation(account.id);
const debouncedUpdateAccountNote = useCallback(debounce(updateAccountNote, { wait: 900 }), []);
const textarea = useRef<HTMLTextAreaElement>(null); const textarea = useRef<HTMLTextAreaElement>(null);
const [value, setValue] = useState<string | undefined>(account.relationship?.note); const [value, setValue] = useState<string | undefined>(account.relationship?.note);
@ -41,9 +36,11 @@ const AccountNotePanel: React.FC<IAccountNotePanel> = ({ account }) => {
const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = e => { const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = e => {
setValue(e.target.value); setValue(e.target.value);
onSave(dispatch, account.id, e.target.value, () => { debouncedUpdateAccountNote(e.target.value, {
setSaved(true); onSuccess: () => {
setTimeout(() => setSaved(false), 2000); setSaved(true);
setTimeout(() => setSaved(false), 2000);
},
}); });
}; };

View File

@ -236,6 +236,23 @@ const useRemoveAccountFromFollowersMutation = (accountId: string) => {
}); });
}; };
const useUpdateAccountNoteMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['accountNote', accountId],
mutationFn: (note: string) => client.accounts.updateAccountNote(accountId, note),
onMutate: (note) => updateRelationship(accountId, {
note,
}, queryClient),
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
},
});
};
export { export {
useRelationshipQuery, useRelationshipQuery,
useFollowAccountMutation, useFollowAccountMutation,
@ -247,4 +264,5 @@ export {
usePinAccountMutation, usePinAccountMutation,
useUnpinAccountMutation, useUnpinAccountMutation,
useRemoveAccountFromFollowersMutation, useRemoveAccountFromFollowersMutation,
useUpdateAccountNoteMutation,
}; };

52
pnpm-lock.yaml generated
View File

@ -196,6 +196,9 @@ importers:
'@tailwindcss/typography': '@tailwindcss/typography':
specifier: ^0.5.16 specifier: ^0.5.16
version: 0.5.16(tailwindcss@3.4.17) version: 0.5.16(tailwindcss@3.4.17)
'@tanstack/react-pacer':
specifier: ^0.16.4
version: 0.16.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.62.11 specifier: ^5.62.11
version: 5.84.1(react@18.3.1) version: 5.84.1(react@18.3.1)
@ -2322,14 +2325,38 @@ packages:
peerDependencies: peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tanstack/devtools-event-client@0.3.3':
resolution: {integrity: sha512-RfV+OPV/M3CGryYqTue684u10jUt55PEqeBOnOtCe6tAmHI9Iqyc8nHeDhWPEV9715gShuauFVaMc9RiUVNdwg==}
engines: {node: '>=18'}
'@tanstack/pacer@0.15.4':
resolution: {integrity: sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==}
engines: {node: '>=18'}
'@tanstack/query-core@5.83.1': '@tanstack/query-core@5.83.1':
resolution: {integrity: sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==} resolution: {integrity: sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==}
'@tanstack/react-pacer@0.16.4':
resolution: {integrity: sha512-nuQLE8bx0rYMiJau4jOTPZFp3XC/GnIHDKfKVVWeKUHNF4grRdVHPgTlJ8EV/nt/HJxSUnIcy+IIKX+Bj0bLSw==}
engines: {node: '>=18'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/react-query@5.84.1': '@tanstack/react-query@5.84.1':
resolution: {integrity: sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==} resolution: {integrity: sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==}
peerDependencies: peerDependencies:
react: ^18 || ^19 react: ^18 || ^19
'@tanstack/react-store@0.7.7':
resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/store@0.7.7':
resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==}
'@testing-library/dom@10.4.1': '@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -8625,13 +8652,36 @@ snapshots:
postcss-selector-parser: 6.0.10 postcss-selector-parser: 6.0.10
tailwindcss: 3.4.17 tailwindcss: 3.4.17
'@tanstack/devtools-event-client@0.3.3': {}
'@tanstack/pacer@0.15.4':
dependencies:
'@tanstack/devtools-event-client': 0.3.3
'@tanstack/store': 0.7.7
'@tanstack/query-core@5.83.1': {} '@tanstack/query-core@5.83.1': {}
'@tanstack/react-pacer@0.16.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/pacer': 0.15.4
'@tanstack/react-store': 0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@tanstack/react-query@5.84.1(react@18.3.1)': '@tanstack/react-query@5.84.1(react@18.3.1)':
dependencies: dependencies:
'@tanstack/query-core': 5.83.1 '@tanstack/query-core': 5.83.1
react: 18.3.1 react: 18.3.1
'@tanstack/react-store@0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/store': 0.7.7
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
use-sync-external-store: 1.5.0(react@18.3.1)
'@tanstack/store@0.7.7': {}
'@testing-library/dom@10.4.1': '@testing-library/dom@10.4.1':
dependencies: dependencies:
'@babel/code-frame': 7.27.1 '@babel/code-frame': 7.27.1
@ -10299,7 +10349,7 @@ snapshots:
tinyglobby: 0.2.14 tinyglobby: 0.2.14
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color