diff --git a/app/javascript/mastodon/api/collections.ts b/app/javascript/mastodon/api/collections.ts index 142e303422..8e3ceb7389 100644 --- a/app/javascript/mastodon/api/collections.ts +++ b/app/javascript/mastodon/api/collections.ts @@ -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( `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( + apiRequestGet( `v1_alpha/collections/${collectionId}`, ); diff --git a/app/javascript/mastodon/api_types/collections.ts b/app/javascript/mastodon/api_types/collections.ts index 61ed9d9439..c1a17b5dc2 100644 --- a/app/javascript/mastodon/api_types/collections.ts +++ b/app/javascript/mastodon/api_types/collections.ts @@ -73,7 +73,7 @@ type CommonPayloadFields = Pick< tag_name?: string; }; -export interface ApiPatchCollectionPayload extends Partial { +export interface ApiUpdateCollectionPayload extends Partial { id: string; } diff --git a/app/javascript/mastodon/features/collections/editor.tsx b/app/javascript/mastodon/features/collections/editor.tsx index 29659edf9e..138e7764e5 100644 --- a/app/javascript/mastodon/features/collections/editor.tsx +++ b/app/javascript/mastodon/features/collections/editor.tsx @@ -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={ } 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); diff --git a/app/javascript/mastodon/features/collections/index.tsx b/app/javascript/mastodon/features/collections/index.tsx index 1afa43ebb8..bd1c4f790b 100644 --- a/app/javascript/mastodon/features/collections/index.tsx +++ b/app/javascript/mastodon/features/collections/index.tsx @@ -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( () => [ diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/delete_collection.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/delete_collection.tsx new file mode 100644 index 0000000000..4bc2374603 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/delete_collection.tsx @@ -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 ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts index 9aff30eeac..389ad7ea83 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts @@ -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, diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 0458bac93c..30d7578c55 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -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 }), diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index ecd35d3b19..49448c4f33 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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?", diff --git a/app/javascript/mastodon/reducers/slices/collections.ts b/app/javascript/mastodon/reducers/slices/collections.ts index 0eb7bfbbcf..6f8637bb2c 100644 --- a/app/javascript/mastodon/reducers/slices/collections.ts +++ b/app/javascript/mastodon/reducers/slices/collections.ts @@ -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 = {}; + const collectionsMap: Record = + 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; },