Implement editing collection settings and deleting collections (#37658)

This commit is contained in:
diondiondion
2026-01-29 12:01:40 +01:00
committed by GitHub
parent 21f8fc808e
commit 6f53b0b634
9 changed files with 156 additions and 39 deletions

View File

@@ -9,7 +9,7 @@ import type {
ApiWrappedCollectionJSON,
ApiCollectionWithAccountsJSON,
ApiCreateCollectionPayload,
ApiPatchCollectionPayload,
ApiUpdateCollectionPayload,
ApiCollectionsJSON,
} from '../api_types/collections';
@@ -19,7 +19,7 @@ export const apiCreateCollection = (collection: ApiCreateCollectionPayload) =>
export const apiUpdateCollection = ({
id,
...collection
}: ApiPatchCollectionPayload) =>
}: ApiUpdateCollectionPayload) =>
apiRequestPut<ApiWrappedCollectionJSON>(
`v1_alpha/collections/${id}`,
collection,
@@ -29,7 +29,7 @@ export const apiDeleteCollection = (collectionId: string) =>
apiRequestDelete(`v1_alpha/collections/${collectionId}`);
export const apiGetCollection = (collectionId: string) =>
apiRequestGet<ApiCollectionWithAccountsJSON[]>(
apiRequestGet<ApiCollectionWithAccountsJSON>(
`v1_alpha/collections/${collectionId}`,
);

View File

@@ -73,7 +73,7 @@ type CommonPayloadFields = Pick<
tag_name?: string;
};
export interface ApiPatchCollectionPayload extends Partial<CommonPayloadFields> {
export interface ApiUpdateCollectionPayload extends Partial<CommonPayloadFields> {
id: string;
}

View File

@@ -11,6 +11,7 @@ import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import type {
ApiCollectionJSON,
ApiCreateCollectionPayload,
ApiUpdateCollectionPayload,
} from 'mastodon/api_types/collections';
import { Button } from 'mastodon/components/button';
import { Column } from 'mastodon/components/column';
@@ -18,7 +19,11 @@ import { ColumnHeader } from 'mastodon/components/column_header';
import { TextAreaField, ToggleField } from 'mastodon/components/form_fields';
import { TextInputField } from 'mastodon/components/form_fields/text_input_field';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { createCollection } from 'mastodon/reducers/slices/collections';
import {
createCollection,
fetchCollection,
updateCollection,
} from 'mastodon/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
@@ -83,16 +88,18 @@ const CollectionSettings: React.FC<{
e.preventDefault();
if (id) {
// void dispatch(
// updateList({
// id,
// title,
// exclusive,
// replies_policy: repliesPolicy,
// }),
// ).then(() => {
// return '';
// });
const payload: ApiUpdateCollectionPayload = {
id,
name,
description,
tag_name: topic,
discoverable,
sensitive,
};
void dispatch(updateCollection({ payload })).then(() => {
history.push(`/collections`);
});
} else {
const payload: ApiCreateCollectionPayload = {
name,
@@ -103,6 +110,7 @@ const CollectionSettings: React.FC<{
if (topic) {
payload.tag_name = topic;
}
void dispatch(
createCollection({
payload,
@@ -114,8 +122,6 @@ const CollectionSettings: React.FC<{
);
history.push(`/collections`);
}
return '';
});
}
},
@@ -198,7 +204,7 @@ const CollectionSettings: React.FC<{
hint={
<FormattedMessage
id='collections.mark_as_sensitive_hint'
defaultMessage="Hides the collection's description and accounts behind a content warning. The title will still be visible."
defaultMessage="Hides the collection's description and accounts behind a content warning. The collection name will still be visible."
/>
}
checked={sensitive}
@@ -232,9 +238,9 @@ export const CollectionEditorPage: React.FC<{
const isLoading = isEditMode && !collection;
useEffect(() => {
// if (id) {
// dispatch(fetchCollection(id));
// }
if (id) {
void dispatch(fetchCollection({ collectionId: id }));
}
}, [dispatch, id]);
const pageTitle = intl.formatMessage(id ? messages.edit : messages.create);

View File

@@ -48,13 +48,14 @@ const ListItem: React.FC<{
const handleDeleteClick = useCallback(() => {
dispatch(
openModal({
modalType: 'CONFIRM_DELETE_LIST',
modalType: 'CONFIRM_DELETE_COLLECTION',
modalProps: {
listId: id,
name,
id,
},
}),
);
}, [dispatch, id]);
}, [dispatch, id, name]);
const menu = useMemo(
() => [

View File

@@ -0,0 +1,54 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router';
import { deleteCollection } from 'mastodon/reducers/slices/collections';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
deleteListTitle: {
id: 'confirmations.delete_collection.title',
defaultMessage: 'Delete "{name}"?',
},
deleteListMessage: {
id: 'confirmations.delete_collection.message',
defaultMessage: 'This action cannot be undone.',
},
deleteListConfirm: {
id: 'confirmations.delete_collection.confirm',
defaultMessage: 'Delete',
},
});
export const ConfirmDeleteCollectionModal: React.FC<
{
id: string;
name: string;
} & BaseConfirmationModalProps
> = ({ id, name, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const history = useHistory();
const onConfirm = useCallback(() => {
void dispatch(deleteCollection({ collectionId: id }));
history.push('/collections');
}, [dispatch, history, id]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.deleteListTitle, {
name,
})}
message={intl.formatMessage(messages.deleteListMessage)}
confirm={intl.formatMessage(messages.deleteListConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@@ -1,6 +1,7 @@
export { ConfirmationModal } from './confirmation_modal';
export { ConfirmDeleteStatusModal } from './delete_status';
export { ConfirmDeleteListModal } from './delete_list';
export { ConfirmDeleteCollectionModal } from './delete_collection';
export {
ConfirmReplyModal,
ConfirmEditStatusModal,

View File

@@ -29,6 +29,7 @@ import {
ConfirmationModal,
ConfirmDeleteStatusModal,
ConfirmDeleteListModal,
ConfirmDeleteCollectionModal,
ConfirmReplyModal,
ConfirmEditStatusModal,
ConfirmUnblockModal,
@@ -57,6 +58,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'CONFIRM_DELETE_STATUS': () => Promise.resolve({ default: ConfirmDeleteStatusModal }),
'CONFIRM_DELETE_LIST': () => Promise.resolve({ default: ConfirmDeleteListModal }),
'CONFIRM_DELETE_COLLECTION': () => Promise.resolve({ default: ConfirmDeleteCollectionModal }),
'CONFIRM_REPLY': () => Promise.resolve({ default: ConfirmReplyModal }),
'CONFIRM_EDIT_STATUS': () => Promise.resolve({ default: ConfirmEditStatusModal }),
'CONFIRM_UNBLOCK': () => Promise.resolve({ default: ConfirmUnblockModal }),

View File

@@ -221,7 +221,7 @@
"collections.description_length_hint": "100 characters limit",
"collections.error_loading_collections": "There was an error when trying to load your collections.",
"collections.mark_as_sensitive": "Mark as sensitive",
"collections.mark_as_sensitive_hint": "Hides the collection's description and accounts behind a content warning. The title will still be visible.",
"collections.mark_as_sensitive_hint": "Hides the collection's description and accounts behind a content warning. The collection name will still be visible.",
"collections.name_length_hint": "100 characters limit",
"collections.no_collections_yet": "No collections yet.",
"collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.",
@@ -291,6 +291,9 @@
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this post?",
"confirmations.delete.title": "Delete post?",
"confirmations.delete_collection.confirm": "Delete",
"confirmations.delete_collection.message": "This action cannot be undone.",
"confirmations.delete_collection.title": "Delete \"{name}\"?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.delete_list.title": "Delete list?",

View File

@@ -1,13 +1,17 @@
import { createSlice } from '@reduxjs/toolkit';
import { importFetchedAccounts } from '@/mastodon/actions/importer';
import {
apiCreateCollection,
apiGetAccountCollections,
// apiGetCollection,
apiUpdateCollection,
apiGetCollection,
apiDeleteCollection,
} from '@/mastodon/api/collections';
import type {
ApiCollectionJSON,
ApiCreateCollectionPayload,
ApiUpdateCollectionPayload,
} from '@/mastodon/api_types/collections';
import {
createAppSelector,
@@ -59,10 +63,11 @@ const collectionSlice = createSlice({
};
});
builder.addCase(fetchAccountCollections.fulfilled, (state, actions) => {
const { collections } = actions.payload;
builder.addCase(fetchAccountCollections.fulfilled, (state, action) => {
const { collections } = action.payload;
const collectionsMap: Record<string, ApiCollectionJSON> = {};
const collectionsMap: Record<string, ApiCollectionJSON> =
state.collections;
const collectionIds: string[] = [];
collections.forEach((collection) => {
@@ -72,12 +77,40 @@ const collectionSlice = createSlice({
});
state.collections = collectionsMap;
state.accountCollections[actions.meta.arg.accountId] = {
state.accountCollections[action.meta.arg.accountId] = {
collectionIds,
status: 'idle',
};
});
/**
* Fetching a single collection
*/
builder.addCase(fetchCollection.fulfilled, (state, action) => {
const { collection } = action.payload;
state.collections[collection.id] = collection;
});
/**
* Updating a collection
*/
builder.addCase(updateCollection.fulfilled, (state, action) => {
const { collection } = action.payload;
state.collections[collection.id] = collection;
});
/**
* Deleting a collection
*/
builder.addCase(deleteCollection.fulfilled, (state, action) => {
const { collectionId } = action.meta.arg;
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete state.collections[collectionId];
});
/**
* Creating a collection
*/
@@ -86,6 +119,7 @@ const collectionSlice = createSlice({
const { collection } = actions.payload;
state.collections[collection.id] = collection;
if (state.accountCollections[collection.account_id]) {
state.accountCollections[collection.account_id]?.collectionIds.unshift(
collection.id,
@@ -105,13 +139,17 @@ export const fetchAccountCollections = createDataLoadingThunk(
({ accountId }: { accountId: string }) => apiGetAccountCollections(accountId),
);
// To be added soon…
//
// export const fetchCollection = createDataLoadingThunk(
// `${collectionSlice.name}/fetchCollection`,
// ({ collectionId }: { collectionId: string }) =>
// apiGetCollection(collectionId),
// );
export const fetchCollection = createDataLoadingThunk(
`${collectionSlice.name}/fetchCollection`,
({ collectionId }: { collectionId: string }) =>
apiGetCollection(collectionId),
(payload, { dispatch }) => {
if (payload.accounts.length > 0) {
dispatch(importFetchedAccounts(payload.accounts));
}
return payload;
},
);
export const createCollection = createDataLoadingThunk(
`${collectionSlice.name}/createCollection`,
@@ -119,6 +157,18 @@ export const createCollection = createDataLoadingThunk(
apiCreateCollection(payload),
);
export const updateCollection = createDataLoadingThunk(
`${collectionSlice.name}/updateCollection`,
({ payload }: { payload: ApiUpdateCollectionPayload }) =>
apiUpdateCollection(payload),
);
export const deleteCollection = createDataLoadingThunk(
`${collectionSlice.name}/deleteCollection`,
({ collectionId }: { collectionId: string }) =>
apiDeleteCollection(collectionId),
);
export const collections = collectionSlice.reducer;
/**
@@ -136,7 +186,7 @@ export const selectMyCollections = createAppSelector(
(state) => state.collections.accountCollections,
(state) => state.collections.collections,
],
(me, collectionsByAccountId, collectionsById) => {
(me, collectionsByAccountId, collectionsMap) => {
const myCollectionsQuery = collectionsByAccountId[me];
if (!myCollectionsQuery) {
@@ -151,7 +201,7 @@ export const selectMyCollections = createAppSelector(
return {
status,
collections: collectionIds
.map((id) => collectionsById[id])
.map((id) => collectionsMap[id])
.filter((c) => !!c),
} satisfies AccountCollectionQuery;
},