Refactor collection editor state handling (#38133)
This commit is contained in:
parent
12c6c6dcf9
commit
20932752fe
@ -3,7 +3,7 @@ import { useCallback, useEffect } from 'react';
|
|||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useLocation, useParams } from 'react-router';
|
import { useHistory, useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
@ -84,6 +84,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { name, description, tag, account_id } = collection;
|
const { name, description, tag, account_id } = collection;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const handleShare = useCallback(() => {
|
const handleShare = useCallback(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -97,12 +98,14 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
|
|||||||
}, [collection, dispatch]);
|
}, [collection, dispatch]);
|
||||||
|
|
||||||
const location = useLocation<{ newCollection?: boolean } | undefined>();
|
const location = useLocation<{ newCollection?: boolean } | undefined>();
|
||||||
const wasJustCreated = location.state?.newCollection;
|
const isNewCollection = location.state?.newCollection;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (wasJustCreated) {
|
if (isNewCollection) {
|
||||||
|
// Replace with current pathname to clear `newCollection` state
|
||||||
|
history.replace(location.pathname);
|
||||||
handleShare();
|
handleShare();
|
||||||
}
|
}
|
||||||
}, [handleShare, wasJustCreated]);
|
}, [history, handleShare, isNewCollection, location.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export const CollectionShareModal: React.FC<{
|
|||||||
onClose();
|
onClose();
|
||||||
dispatch(changeCompose(shareMessage));
|
dispatch(changeCompose(shareMessage));
|
||||||
dispatch(focusCompose());
|
dispatch(focusCompose());
|
||||||
}, [collectionLink, dispatch, intl, isOwnCollection, onClose]);
|
}, [onClose, collectionLink, dispatch, intl, isOwnCollection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalShell>
|
<ModalShell>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useCallback, useId, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
@ -30,12 +30,12 @@ import { useAccount } from 'mastodon/hooks/useAccount';
|
|||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
import {
|
import {
|
||||||
addCollectionItem,
|
addCollectionItem,
|
||||||
|
getCollectionItemIds,
|
||||||
removeCollectionItem,
|
removeCollectionItem,
|
||||||
|
updateCollectionEditorField,
|
||||||
} from 'mastodon/reducers/slices/collections';
|
} from 'mastodon/reducers/slices/collections';
|
||||||
import { store, useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { store, useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import type { TempCollectionState } from './state';
|
|
||||||
import { getCollectionEditorState } from './state';
|
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
import { WizardStepHeader } from './wizard_step_header';
|
import { WizardStepHeader } from './wizard_step_header';
|
||||||
|
|
||||||
@ -52,9 +52,8 @@ function isOlderThanAWeek(date?: string): boolean {
|
|||||||
|
|
||||||
const AddedAccountItem: React.FC<{
|
const AddedAccountItem: React.FC<{
|
||||||
accountId: string;
|
accountId: string;
|
||||||
isRemovable: boolean;
|
|
||||||
onRemove: (id: string) => void;
|
onRemove: (id: string) => void;
|
||||||
}> = ({ accountId, isRemovable, onRemove }) => {
|
}> = ({ accountId, onRemove }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const account = useAccount(accountId);
|
const account = useAccount(accountId);
|
||||||
|
|
||||||
@ -86,17 +85,15 @@ const AddedAccountItem: React.FC<{
|
|||||||
id={accountId}
|
id={accountId}
|
||||||
extraAccountInfo={lastPostHint}
|
extraAccountInfo={lastPostHint}
|
||||||
>
|
>
|
||||||
{isRemovable && (
|
<IconButton
|
||||||
<IconButton
|
title={intl.formatMessage({
|
||||||
title={intl.formatMessage({
|
id: 'collections.remove_account',
|
||||||
id: 'collections.remove_account',
|
defaultMessage: 'Remove this account',
|
||||||
defaultMessage: 'Remove this account',
|
})}
|
||||||
})}
|
icon='remove'
|
||||||
icon='remove'
|
iconComponent={CancelIcon}
|
||||||
iconComponent={CancelIcon}
|
onClick={handleRemoveAccount}
|
||||||
onClick={handleRemoveAccount}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Account>
|
</Account>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -139,28 +136,25 @@ export const CollectionAccounts: React.FC<{
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation<TempCollectionState>();
|
|
||||||
const { id, initialItemIds } = getCollectionEditorState(
|
|
||||||
collection,
|
|
||||||
location.state,
|
|
||||||
);
|
|
||||||
const isEditMode = !!id;
|
|
||||||
const collectionItems = collection?.items;
|
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const { id, items } = collection ?? {};
|
||||||
// This state is only used when creating a new collection.
|
const isEditMode = !!id;
|
||||||
// In edit mode, the collection will be updated instantly
|
const collectionItems = items;
|
||||||
const [addedAccountIds, setAccountIds] = useState(initialItemIds);
|
|
||||||
|
const addedAccountIds = useAppSelector(
|
||||||
|
(state) => state.collections.editor.accountIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
// In edit mode, we're bypassing state and just return collection items directly,
|
||||||
|
// since they're edited "live", saving after each addition/deletion
|
||||||
const accountIds = useMemo(
|
const accountIds = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isEditMode
|
isEditMode ? getCollectionItemIds(collectionItems) : addedAccountIds,
|
||||||
? (collectionItems
|
|
||||||
?.map((item) => item.account_id)
|
|
||||||
.filter((id): id is string => !!id) ?? [])
|
|
||||||
: addedAccountIds,
|
|
||||||
[isEditMode, collectionItems, addedAccountIds],
|
[isEditMode, collectionItems, addedAccountIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT;
|
const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -233,28 +227,41 @@ export const CollectionAccounts: React.FC<{
|
|||||||
[dispatch, relationships],
|
[dispatch, relationships],
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeAccountItem = useCallback((accountId: string) => {
|
const removeAccountItem = useCallback(
|
||||||
setAccountIds((ids) => ids.filter((id) => id !== accountId));
|
(accountId: string) => {
|
||||||
}, []);
|
dispatch(
|
||||||
|
updateCollectionEditorField({
|
||||||
|
field: 'accountIds',
|
||||||
|
value: accountIds.filter((id) => id !== accountId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[accountIds, dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
const addAccountItem = useCallback(
|
const addAccountItem = useCallback(
|
||||||
(accountId: string) => {
|
(accountId: string) => {
|
||||||
confirmFollowStatus(accountId, () => {
|
confirmFollowStatus(accountId, () => {
|
||||||
setAccountIds((ids) => [...ids, accountId]);
|
dispatch(
|
||||||
|
updateCollectionEditorField({
|
||||||
|
field: 'accountIds',
|
||||||
|
value: [...accountIds, accountId],
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[confirmFollowStatus],
|
[accountIds, confirmFollowStatus, dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleAccountItem = useCallback(
|
const toggleAccountItem = useCallback(
|
||||||
(item: SuggestionItem) => {
|
(item: SuggestionItem) => {
|
||||||
if (addedAccountIds.includes(item.id)) {
|
if (accountIds.includes(item.id)) {
|
||||||
removeAccountItem(item.id);
|
removeAccountItem(item.id);
|
||||||
} else {
|
} else {
|
||||||
addAccountItem(item.id);
|
addAccountItem(item.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[addAccountItem, addedAccountIds, removeAccountItem],
|
[accountIds, addAccountItem, removeAccountItem],
|
||||||
);
|
);
|
||||||
|
|
||||||
const instantRemoveAccountItem = useCallback(
|
const instantRemoveAccountItem = useCallback(
|
||||||
@ -406,7 +413,6 @@ export const CollectionAccounts: React.FC<{
|
|||||||
>
|
>
|
||||||
<AddedAccountItem
|
<AddedAccountItem
|
||||||
accountId={accountId}
|
accountId={accountId}
|
||||||
isRemovable={!isEditMode}
|
|
||||||
onRemove={handleRemoveAccountItem}
|
onRemove={handleRemoveAccountItem}
|
||||||
/>
|
/>
|
||||||
</Article>
|
</Article>
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { isFulfilled } from '@reduxjs/toolkit';
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiCollectionJSON,
|
|
||||||
ApiCreateCollectionPayload,
|
ApiCreateCollectionPayload,
|
||||||
ApiUpdateCollectionPayload,
|
ApiUpdateCollectionPayload,
|
||||||
} from 'mastodon/api_types/collections';
|
} from 'mastodon/api_types/collections';
|
||||||
@ -23,70 +22,77 @@ import { TextInputField } from 'mastodon/components/form_fields/text_input_field
|
|||||||
import {
|
import {
|
||||||
createCollection,
|
createCollection,
|
||||||
updateCollection,
|
updateCollection,
|
||||||
|
updateCollectionEditorField,
|
||||||
} from 'mastodon/reducers/slices/collections';
|
} from 'mastodon/reducers/slices/collections';
|
||||||
import { useAppDispatch } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import type { TempCollectionState } from './state';
|
|
||||||
import { getCollectionEditorState } from './state';
|
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
import { WizardStepHeader } from './wizard_step_header';
|
import { WizardStepHeader } from './wizard_step_header';
|
||||||
|
|
||||||
export const CollectionDetails: React.FC<{
|
export const CollectionDetails: React.FC = () => {
|
||||||
collection?: ApiCollectionJSON | null;
|
|
||||||
}> = ({ collection }) => {
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation<TempCollectionState>();
|
const { id, name, description, topic, discoverable, sensitive, accountIds } =
|
||||||
|
useAppSelector((state) => state.collections.editor);
|
||||||
const {
|
|
||||||
id,
|
|
||||||
initialName,
|
|
||||||
initialDescription,
|
|
||||||
initialTopic,
|
|
||||||
initialItemIds,
|
|
||||||
initialDiscoverable,
|
|
||||||
initialSensitive,
|
|
||||||
} = getCollectionEditorState(collection, location.state);
|
|
||||||
|
|
||||||
const [name, setName] = useState(initialName);
|
|
||||||
const [description, setDescription] = useState(initialDescription);
|
|
||||||
const [topic, setTopic] = useState(initialTopic);
|
|
||||||
const [discoverable, setDiscoverable] = useState(initialDiscoverable);
|
|
||||||
const [sensitive, setSensitive] = useState(initialSensitive);
|
|
||||||
|
|
||||||
const handleNameChange = useCallback(
|
const handleNameChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setName(event.target.value);
|
dispatch(
|
||||||
|
updateCollectionEditorField({
|
||||||
|
field: 'name',
|
||||||
|
value: event.target.value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDescriptionChange = useCallback(
|
const handleDescriptionChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setDescription(event.target.value);
|
dispatch(
|
||||||
|
updateCollectionEditorField({
|
||||||
|
field: 'description',
|
||||||
|
value: event.target.value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTopicChange = useCallback(
|
const handleTopicChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setTopic(event.target.value);
|
dispatch(
|
||||||
|
updateCollectionEditorField({
|
||||||
|
field: 'topic',
|
||||||
|
value: event.target.value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDiscoverableChange = useCallback(
|
const handleDiscoverableChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setDiscoverable(event.target.value === 'public');
|
dispatch(
|
||||||
|
updateCollectionEditorField({
|
||||||
|
field: 'discoverable',
|
||||||
|
value: event.target.value === 'public',
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSensitiveChange = useCallback(
|
const handleSensitiveChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setSensitive(event.target.checked);
|
dispatch(
|
||||||
|
updateCollectionEditorField({
|
||||||
|
field: 'sensitive',
|
||||||
|
value: event.target.checked,
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
@ -112,7 +118,7 @@ export const CollectionDetails: React.FC<{
|
|||||||
description,
|
description,
|
||||||
discoverable,
|
discoverable,
|
||||||
sensitive,
|
sensitive,
|
||||||
account_ids: initialItemIds,
|
account_ids: accountIds,
|
||||||
};
|
};
|
||||||
if (topic) {
|
if (topic) {
|
||||||
payload.tag_name = topic;
|
payload.tag_name = topic;
|
||||||
@ -124,9 +130,7 @@ export const CollectionDetails: React.FC<{
|
|||||||
}),
|
}),
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
if (isFulfilled(result)) {
|
if (isFulfilled(result)) {
|
||||||
history.replace(
|
history.replace(`/collections`);
|
||||||
`/collections/${result.payload.collection.id}/edit/details`,
|
|
||||||
);
|
|
||||||
history.push(`/collections/${result.payload.collection.id}`, {
|
history.push(`/collections/${result.payload.collection.id}`, {
|
||||||
newCollection: true,
|
newCollection: true,
|
||||||
});
|
});
|
||||||
@ -143,7 +147,7 @@ export const CollectionDetails: React.FC<{
|
|||||||
sensitive,
|
sensitive,
|
||||||
dispatch,
|
dispatch,
|
||||||
history,
|
history,
|
||||||
initialItemIds,
|
accountIds,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,10 @@ import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
|||||||
import { Column } from 'mastodon/components/column';
|
import { Column } from 'mastodon/components/column';
|
||||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
import { fetchCollection } from 'mastodon/reducers/slices/collections';
|
import {
|
||||||
|
collectionEditorActions,
|
||||||
|
fetchCollection,
|
||||||
|
} from 'mastodon/reducers/slices/collections';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import { CollectionAccounts } from './accounts';
|
import { CollectionAccounts } from './accounts';
|
||||||
@ -68,6 +71,7 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
const collection = useAppSelector((state) =>
|
const collection = useAppSelector((state) =>
|
||||||
id ? state.collections.collections[id] : undefined,
|
id ? state.collections.collections[id] : undefined,
|
||||||
);
|
);
|
||||||
|
const editorStateId = useAppSelector((state) => state.collections.editor.id);
|
||||||
const isEditMode = !!id;
|
const isEditMode = !!id;
|
||||||
const isLoading = isEditMode && !collection;
|
const isLoading = isEditMode && !collection;
|
||||||
|
|
||||||
@ -77,6 +81,18 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
}
|
}
|
||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id !== editorStateId) {
|
||||||
|
void dispatch(collectionEditorActions.reset());
|
||||||
|
}
|
||||||
|
}, [dispatch, editorStateId, id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (collection) {
|
||||||
|
void dispatch(collectionEditorActions.init(collection));
|
||||||
|
}
|
||||||
|
}, [dispatch, collection]);
|
||||||
|
|
||||||
const pageTitle = intl.formatMessage(usePageTitle(id));
|
const pageTitle = intl.formatMessage(usePageTitle(id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -104,7 +120,7 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
exact
|
exact
|
||||||
path={`${path}/details`}
|
path={`${path}/details`}
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
render={() => <CollectionDetails collection={collection} />}
|
render={() => <CollectionDetails />}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
import type {
|
|
||||||
ApiCollectionJSON,
|
|
||||||
ApiCreateCollectionPayload,
|
|
||||||
} from '@/mastodon/api_types/collections';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporary editor state across creation steps,
|
|
||||||
* kept in location state
|
|
||||||
*/
|
|
||||||
export type TempCollectionState =
|
|
||||||
| Partial<ApiCreateCollectionPayload>
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve initial editor state. Temporary location state
|
|
||||||
* trumps stored data, otherwise initial values are returned.
|
|
||||||
*/
|
|
||||||
export function getCollectionEditorState(
|
|
||||||
collection: ApiCollectionJSON | null | undefined,
|
|
||||||
locationState: TempCollectionState,
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name = '',
|
|
||||||
description = '',
|
|
||||||
tag,
|
|
||||||
language = '',
|
|
||||||
discoverable = true,
|
|
||||||
sensitive = false,
|
|
||||||
items,
|
|
||||||
} = collection ?? {};
|
|
||||||
|
|
||||||
const collectionItemIds =
|
|
||||||
items?.map((item) => item.account_id).filter(onlyExistingIds) ?? [];
|
|
||||||
|
|
||||||
const initialItemIds = (
|
|
||||||
locationState?.account_ids ?? collectionItemIds
|
|
||||||
).filter(onlyExistingIds);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
initialItemIds,
|
|
||||||
initialName: locationState?.name ?? name,
|
|
||||||
initialDescription: locationState?.description ?? description,
|
|
||||||
initialTopic: locationState?.tag_name ?? tag?.name ?? '',
|
|
||||||
initialLanguage: locationState?.language ?? language,
|
|
||||||
initialDiscoverable: locationState?.discoverable ?? discoverable,
|
|
||||||
initialSensitive: locationState?.sensitive ?? sensitive,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const onlyExistingIds = (id?: string): id is string => !!id;
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { importFetchedAccounts } from '@/mastodon/actions/importer';
|
import { importFetchedAccounts } from '@/mastodon/actions/importer';
|
||||||
@ -36,17 +37,69 @@ interface CollectionState {
|
|||||||
status: QueryStatus;
|
status: QueryStatus;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
editor: {
|
||||||
|
id: string | undefined;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
topic: string;
|
||||||
|
language: string | null;
|
||||||
|
discoverable: boolean;
|
||||||
|
sensitive: boolean;
|
||||||
|
accountIds: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditorField = CollectionState['editor'];
|
||||||
|
|
||||||
|
interface UpdateEditorFieldPayload<K extends keyof EditorField> {
|
||||||
|
field: K;
|
||||||
|
value: EditorField[K];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: CollectionState = {
|
const initialState: CollectionState = {
|
||||||
collections: {},
|
collections: {},
|
||||||
accountCollections: {},
|
accountCollections: {},
|
||||||
|
editor: {
|
||||||
|
id: undefined,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
topic: '',
|
||||||
|
language: null,
|
||||||
|
discoverable: true,
|
||||||
|
sensitive: false,
|
||||||
|
accountIds: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const collectionSlice = createSlice({
|
const collectionSlice = createSlice({
|
||||||
name: 'collections',
|
name: 'collections',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {
|
||||||
|
init(state, action: PayloadAction<ApiCollectionJSON | null>) {
|
||||||
|
const collection = action.payload;
|
||||||
|
|
||||||
|
state.editor = {
|
||||||
|
id: collection?.id,
|
||||||
|
name: collection?.name ?? '',
|
||||||
|
description: collection?.description ?? '',
|
||||||
|
topic: collection?.tag?.name ?? '',
|
||||||
|
language: collection?.language ?? '',
|
||||||
|
discoverable: collection?.discoverable ?? true,
|
||||||
|
sensitive: collection?.sensitive ?? false,
|
||||||
|
accountIds: getCollectionItemIds(collection?.items ?? []),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
reset(state) {
|
||||||
|
state.editor = initialState.editor;
|
||||||
|
},
|
||||||
|
updateEditorField<K extends keyof EditorField>(
|
||||||
|
state: CollectionState,
|
||||||
|
action: PayloadAction<UpdateEditorFieldPayload<K>>,
|
||||||
|
) {
|
||||||
|
const { field, value } = action.payload;
|
||||||
|
state.editor[field] = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
extraReducers(builder) {
|
extraReducers(builder) {
|
||||||
/**
|
/**
|
||||||
* Fetching account collections
|
* Fetching account collections
|
||||||
@ -104,6 +157,7 @@ const collectionSlice = createSlice({
|
|||||||
builder.addCase(updateCollection.fulfilled, (state, action) => {
|
builder.addCase(updateCollection.fulfilled, (state, action) => {
|
||||||
const { collection } = action.payload;
|
const { collection } = action.payload;
|
||||||
state.collections[collection.id] = collection;
|
state.collections[collection.id] = collection;
|
||||||
|
state.editor = initialState.editor;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,6 +186,7 @@ const collectionSlice = createSlice({
|
|||||||
const { collection } = actions.payload;
|
const { collection } = actions.payload;
|
||||||
|
|
||||||
state.collections[collection.id] = collection;
|
state.collections[collection.id] = collection;
|
||||||
|
state.editor = initialState.editor;
|
||||||
|
|
||||||
if (state.accountCollections[collection.account_id]) {
|
if (state.accountCollections[collection.account_id]) {
|
||||||
state.accountCollections[collection.account_id]?.collectionIds.unshift(
|
state.accountCollections[collection.account_id]?.collectionIds.unshift(
|
||||||
@ -240,6 +295,9 @@ export const revokeCollectionInclusion = createAppAsyncThunk(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const collections = collectionSlice.reducer;
|
export const collections = collectionSlice.reducer;
|
||||||
|
export const collectionEditorActions = collectionSlice.actions;
|
||||||
|
export const updateCollectionEditorField =
|
||||||
|
collectionSlice.actions.updateEditorField;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selectors
|
* Selectors
|
||||||
@ -278,3 +336,8 @@ export const selectAccountCollections = createAppSelector(
|
|||||||
} satisfies AccountCollectionQuery;
|
} satisfies AccountCollectionQuery;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onlyExistingIds = (id?: string): id is string => !!id;
|
||||||
|
|
||||||
|
export const getCollectionItemIds = (items?: ApiCollectionJSON['items']) =>
|
||||||
|
items?.map((item) => item.account_id).filter(onlyExistingIds) ?? [];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user