pl-fe: don't use redux for list editor modal

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-08-16 09:38:02 +02:00
parent aab55f6131
commit 9312d52331
7 changed files with 83 additions and 199 deletions

View File

@ -1,65 +0,0 @@
import { queryClient } from 'pl-fe/queries/client';
import type { List } from 'pl-api';
import type { AppDispatch } from 'pl-fe/store';
const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE' as const;
const LIST_EDITOR_REPLIES_POLICY_CHANGE = 'LIST_EDITOR_REPLIES_POLICY_CHANGE' as const;
const LIST_EDITOR_EXCLUSIVE_CHANGE = 'LIST_EDITOR_EXCLUSIVE_CHANGE' as const;
const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET' as const;
const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP' as const;
interface ListEditorSetupAction {
type: typeof LIST_EDITOR_SETUP;
list: List;
}
const setupListEditor = (listId: string) => (dispatch: AppDispatch) => {
const list = queryClient.getQueryData<Array<List>>(['lists'])?.find((list) => list.id === listId);
if (!list) return;
dispatch<ListEditorSetupAction>({
type: LIST_EDITOR_SETUP,
list,
});
};
const changeListEditorTitle = (value: string) => ({
type: LIST_EDITOR_TITLE_CHANGE,
value,
});
const changeListEditorRepliesPolicy = (repliesPolicy: List['replies_policy']) => ({
type: LIST_EDITOR_REPLIES_POLICY_CHANGE,
repliesPolicy,
});
const changeListEditorExclusive = (exclusive: boolean) => ({
type: LIST_EDITOR_EXCLUSIVE_CHANGE,
exclusive,
});
const resetListEditor = () => ({
type: LIST_EDITOR_RESET,
});
type ListsAction =
| ListEditorSetupAction
| ReturnType<typeof changeListEditorTitle>
| ReturnType<typeof changeListEditorRepliesPolicy>
| ReturnType<typeof changeListEditorExclusive>
| ReturnType<typeof resetListEditor>;
export {
LIST_EDITOR_TITLE_CHANGE,
LIST_EDITOR_REPLIES_POLICY_CHANGE,
LIST_EDITOR_EXCLUSIVE_CHANGE,
LIST_EDITOR_RESET,
LIST_EDITOR_SETUP,
setupListEditor,
changeListEditorTitle,
changeListEditorRepliesPolicy,
changeListEditorExclusive,
resetListEditor,
type ListsAction,
};

View File

@ -8,6 +8,6 @@ import { useAppSelector } from './use-app-selector';
(window as any).plapi = plapi;
/** Get features for the current instance. */
const useFeatures = (): Features => useAppSelector(state => state.auth.client.features);
const useFeatures = (): Features => ({ ...useAppSelector(state => state.auth.client.features), filtersV2BlurAction: true });
export { useFeatures };

View File

@ -1,7 +1,6 @@
import React from 'react';
import React, { useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { changeListEditorExclusive, changeListEditorRepliesPolicy, changeListEditorTitle } from 'pl-fe/actions/lists';
import List, { ListItem } from 'pl-fe/components/list';
import Button from 'pl-fe/components/ui/button';
import Form from 'pl-fe/components/ui/form';
@ -10,10 +9,8 @@ import FormGroup from 'pl-fe/components/ui/form-group';
import Input from 'pl-fe/components/ui/input';
import Toggle from 'pl-fe/components/ui/toggle';
import { SelectDropdown } from 'pl-fe/features/forms';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useUpdateList } from 'pl-fe/queries/accounts/use-lists';
import { useList, useUpdateList } from 'pl-fe/queries/accounts/use-lists';
const messages = defineMessages({
save: { id: 'lists.new.save', defaultMessage: 'Save list' },
@ -23,39 +20,43 @@ const messages = defineMessages({
});
interface IListForm {
listId: string;
onTabChange: (tab: 'members') => void;
}
const ListForm: React.FC<IListForm> = ({
listId,
onTabChange,
}) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const features = useFeatures();
const { title: value, listId, repliesPolicy, exclusive } = useAppSelector((state) => state.listEditor);
const { data: list } = useList(listId);
const { mutate: updateList, isPending: disabled } = useUpdateList(listId!);
const [title, setTitle] = useState(list!.title);
const [repliesPolicy, setRepliesPolicy] = useState(list!.replies_policy);
const [exclusive, setExclusive] = useState(list!.exclusive);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = e => {
dispatch(changeListEditorTitle(e.target.value));
setTitle(e.target.value);
};
const handleSubmit: React.FormEventHandler<Element> = e => {
e.preventDefault();
updateList({ title: value, replies_policy: repliesPolicy, exclusive });
handleUpdate();
};
const handleClick = () => {
updateList({ title: value, replies_policy: repliesPolicy, exclusive });
const handleUpdate = () => {
updateList({ title, replies_policy: repliesPolicy, exclusive });
};
const handleChangeRepliesPolicy = (e: React.ChangeEvent<HTMLSelectElement>) => {
dispatch(changeListEditorRepliesPolicy(e.target.value as 'none'));
setRepliesPolicy(e.target.value as 'none');
};
const handleChangeExclusive = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(changeListEditorExclusive(e.target.checked));
setExclusive(e.target.checked);
};
return (
@ -66,7 +67,7 @@ const ListForm: React.FC<IListForm> = ({
<Input
outerClassName='grow'
type='text'
value={value}
value={title}
onChange={handleChange}
/>
</FormGroup>
@ -109,7 +110,7 @@ const ListForm: React.FC<IListForm> = ({
</List>
<FormActions>
<Button onClick={handleClick} disabled={disabled}>
<Button onClick={handleUpdate} disabled={disabled}>
<FormattedMessage id='lists.edit.save' defaultMessage='Save list' />
</Button>
</FormActions>

View File

@ -0,0 +1,55 @@
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { CardHeader, CardTitle } from 'pl-fe/components/ui/card';
import { useListAccounts } from 'pl-fe/queries/accounts/use-lists';
import { useAccountSearch } from 'pl-fe/queries/search/use-search-accounts';
import Account from './account';
import Search from './search';
const messages = defineMessages({
addToList: { id: 'lists.account.add', defaultMessage: 'Add to list' },
removeFromList: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
});
interface IListMembersForm {
listId: string;
}
const ListMembersForm: React.FC<IListMembersForm> = ({ listId }) => {
const intl = useIntl();
const [searchValue, setSearchValue] = useState('');
const { data: accountIds = [] } = useListAccounts(listId);
const { data: searchAccountIds = [] } = useAccountSearch(searchValue, { following: true, limit: 5 });
return (
<>
{accountIds.length > 0 && (
<>
<div>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.removeFromList)} />
</CardHeader>
<div className='max-h-48 overflow-y-auto'>
{accountIds.map(accountId => <Account key={accountId} listId={listId} accountId={accountId} added={accountIds.includes(accountId)} />)}
</div>
</div>
<br />
</>
)}
<CardHeader>
<CardTitle title={intl.formatMessage(messages.addToList)} />
</CardHeader>
<Search value={searchValue} onSubmit={setSearchValue} />
<div className='max-h-48 overflow-y-auto'>
{searchAccountIds.map(accountId => <Account key={accountId} listId={listId} accountId={accountId} added={accountIds.includes(accountId)} />)}
</div>
</>
);
};
export { ListMembersForm as default };

View File

@ -1,46 +1,24 @@
import React, { useEffect, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { setupListEditor, resetListEditor } from 'pl-fe/actions/lists';
import { CardHeader, CardTitle } from 'pl-fe/components/ui/card';
import Modal from 'pl-fe/components/ui/modal';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useListAccounts } from 'pl-fe/queries/accounts/use-lists';
import { useAccountSearch } from 'pl-fe/queries/search/use-search-accounts';
import Spinner from 'pl-fe/components/ui/spinner';
import { useList } from 'pl-fe/queries/accounts/use-lists';
import Account from './components/account';
import EditListForm from './components/edit-list-form';
import Search from './components/search';
import ListMembersForm from './components/list-members-form';
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
const messages = defineMessages({
addToList: { id: 'lists.account.add', defaultMessage: 'Add to list' },
removeFromList: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
editList: { id: 'lists.edit', defaultMessage: 'Edit list' },
});
interface ListEditorModalProps {
listId: string;
}
const ListEditorModal: React.FC<BaseModalProps & ListEditorModalProps> = ({ listId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const [tab, setTab] = useState<'info' | 'members'>('info');
const [searchValue, setSearchValue] = useState('');
const { data: accountIds = [] } = useListAccounts(listId);
const { data: searchAccountIds = [] } = useAccountSearch(searchValue, { following: true, limit: 5 });
const { isFetched } = useList(listId);
useEffect(() => {
dispatch(setupListEditor(listId));
return () => {
dispatch(resetListEditor());
};
}, []);
const onClickClose = () => {
onClose('LIST_EDITOR');
@ -52,33 +30,10 @@ const ListEditorModal: React.FC<BaseModalProps & ListEditorModalProps> = ({ list
onClose={onClickClose}
onBack={tab === 'members' ? () => setTab('info') : undefined}
>
{tab === 'info'
? <EditListForm onTabChange={setTab} />
: (
<>
{accountIds.length > 0 && (
<>
<div>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.removeFromList)} />
</CardHeader>
<div className='max-h-48 overflow-y-auto'>
{accountIds.map(accountId => <Account key={accountId} listId={listId} accountId={accountId} added={accountIds.includes(accountId)} />)}
</div>
</div>
<br />
</>
)}
<CardHeader>
<CardTitle title={intl.formatMessage(messages.addToList)} />
</CardHeader>
<Search value={searchValue} onSubmit={setSearchValue} />
<div className='max-h-48 overflow-y-auto'>
{searchAccountIds.map(accountId => <Account key={accountId} listId={listId} accountId={accountId} added={accountIds.includes(accountId)} />)}
</div>
</>
)}
{isFetched ? (tab === 'info'
? <EditListForm listId={listId} onTabChange={setTab} />
: <ListMembersForm listId={listId} />
) : <Spinner />}
</Modal>
);
};

View File

@ -13,7 +13,6 @@ import conversations from './conversations';
import draft_statuses from './draft-statuses';
import filters from './filters';
import instance from './instance';
import listEditor from './list-editor';
import me from './me';
import meta from './meta';
import notifications from './notifications';
@ -38,7 +37,6 @@ const reducers = {
entities,
filters,
instance,
listEditor,
me,
meta,
notifications,

View File

@ -1,60 +0,0 @@
import { create } from 'mutative';
import {
LIST_EDITOR_RESET,
LIST_EDITOR_SETUP,
LIST_EDITOR_TITLE_CHANGE,
LIST_EDITOR_EXCLUSIVE_CHANGE,
LIST_EDITOR_REPLIES_POLICY_CHANGE,
type ListsAction,
} from '../actions/lists';
import type { List } from 'pl-api';
interface State {
listId: string | null;
isSubmitting: boolean;
title: string;
repliesPolicy: List['replies_policy'];
exclusive?: boolean;
}
const initialState: State = {
listId: null,
isSubmitting: false,
title: '',
repliesPolicy: undefined,
exclusive: false,
};
const listEditorReducer = (state: State = initialState, action: ListsAction): State => {
switch (action.type) {
case LIST_EDITOR_RESET:
return initialState;
case LIST_EDITOR_SETUP:
return create(state, (draft) => {
draft.listId = action.list.id;
draft.title = action.list.title;
draft.repliesPolicy = action.list.replies_policy;
draft.exclusive = action.list.exclusive;
draft.isSubmitting = false;
});
case LIST_EDITOR_TITLE_CHANGE:
return create(state, (draft) => {
draft.title = action.value;
});
case LIST_EDITOR_EXCLUSIVE_CHANGE:
return create(state, (draft) => {
draft.exclusive = action.exclusive;
});
case LIST_EDITOR_REPLIES_POLICY_CHANGE:
return create(state, (draft) => {
draft.repliesPolicy = action.repliesPolicy;
});
default:
return state;
}
};
export { listEditorReducer as default };