diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index d70a6eb34..532cdea99 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -1170,6 +1170,30 @@ class PlApiClient { return v.parse(bookmarkFolderSchema, response.json); }, + + /** + * Requires features{@link Features.bookmarkFoldersMultiple}. + */ + addBookmarkToFolder: async (statusId: string, folderId: string) => { + const response = await this.request <{}>( + `/api/v1/bookmark_categories/${folderId}/statuses`, + { method: 'POST', params: { status_ids: [statusId] } }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.bookmarkFoldersMultiple}. + */ + removeBookmarkFromFolder: async (statusId: string, folderId: string) => { + const response = await this.request<{}>( + `/api/v1/bookmark_categories/${folderId}/statuses`, + { method: 'DELETE', params: { status_ids: [statusId] } }, + ); + + return response.json; + }, }; public readonly settings = { @@ -2593,7 +2617,7 @@ class PlApiClient { * Requires features{@link Features.bookmarkFoldersMultiple}. */ getStatusBookmarkFolders: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/categories`, { method: 'GET' }); + const response = await this.request(`/api/v1/statuses/${statusId}/bookmark_categories`, { method: 'GET' }); return v.parse(filteredArray(bookmarkFolderSchema), response.json); }, diff --git a/packages/pl-fe/src/features/ui/components/modals/select-bookmark-folder-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/select-bookmark-folder-modal.tsx index fe91de942..4e718cb36 100644 --- a/packages/pl-fe/src/features/ui/components/modals/select-bookmark-folder-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/select-bookmark-folder-modal.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { bookmark } from 'pl-fe/actions/interactions'; +import { ListItem } from 'pl-fe/components/list'; import { RadioGroup, RadioItem } from 'pl-fe/components/radio'; import Emoji from 'pl-fe/components/ui/emoji'; import HStack from 'pl-fe/components/ui/hstack'; @@ -9,10 +10,12 @@ import Icon from 'pl-fe/components/ui/icon'; import Modal from 'pl-fe/components/ui/modal'; import Spinner from 'pl-fe/components/ui/spinner'; import Stack from 'pl-fe/components/ui/stack'; +import Toggle from 'pl-fe/components/ui/toggle'; import NewFolderForm from 'pl-fe/features/bookmark-folders/components/new-folder-form'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; -import { useBookmarkFolders } from 'pl-fe/queries/statuses/use-bookmark-folders'; +import { useFeatures } from 'pl-fe/hooks/use-features'; +import { useAddBookmarkToFolder, useBookmarkFolders, useRemoveBookmarkFromFolder, useStatusBookmarkFolders } from 'pl-fe/queries/statuses/use-bookmark-folders'; import { makeGetStatus } from 'pl-fe/selectors'; import type { BaseModalProps } from '../modal-root'; @@ -25,10 +28,14 @@ const SelectBookmarkFolderModal: React.FC getStatus(state, { id: statusId }))!; const dispatch = useAppDispatch(); + const features = useFeatures(); const [selectedFolder, setSelectedFolder] = useState(status.bookmark_folder); const { isFetching, data: bookmarkFolders } = useBookmarkFolders(data => data); + const { data: selectedBookmarkFolders, isPending: fetchingSelectedBookmarkFolders } = useStatusBookmarkFolders(statusId); + const { mutate: addBookmarkToFolder, isPending: addingBookmarkToFolder } = useAddBookmarkToFolder(statusId); + const { mutate: removeBookmarkFromFolder, isPending: removingBookmarkFromFolder } = useRemoveBookmarkFromFolder(statusId); const onChange: React.ChangeEventHandler = e => { const folderId = e.target.value; @@ -43,23 +50,19 @@ const SelectBookmarkFolderModal: React.FC - - - - } - checked={selectedFolder === null} - value='' - />, - ]; + const toggleBookmarkFolder = (folderId: string) => { + if (selectedBookmarkFolders?.includes(folderId)) { + removeBookmarkFromFolder(folderId); + } else { + addBookmarkToFolder(folderId); + } + }; - if (!isFetching) { - items.push(...((bookmarkFolders || []).map((folder) => ( - ( + @@ -73,10 +76,50 @@ const SelectBookmarkFolderModal: React.FC{folder.name} } - checked={selectedFolder === folder.id} - value={folder.id} - /> - )))); + > + toggleBookmarkFolder(folder.id)} + disabled={fetchingSelectedBookmarkFolders || addingBookmarkToFolder || removingBookmarkFromFolder} + /> + + )); + } else { + items = [ + + + + + } + checked={selectedFolder === null} + value='' + />, + ]; + + if (!isFetching) { + items.push(...((bookmarkFolders || []).map((folder) => ( + + {folder.emoji ? ( + + ) : } + {folder.name} + + } + checked={selectedFolder === folder.id} + value={folder.id} + /> + )))); + } } const body = isFetching ? : ( diff --git a/packages/pl-fe/src/queries/statuses/use-bookmark-folders.ts b/packages/pl-fe/src/queries/statuses/use-bookmark-folders.ts index e75e1f259..635598d9f 100644 --- a/packages/pl-fe/src/queries/statuses/use-bookmark-folders.ts +++ b/packages/pl-fe/src/queries/statuses/use-bookmark-folders.ts @@ -63,4 +63,44 @@ const useUpdateBookmarkFolder = (folderId: string) => { }); }; -export { useBookmarkFolders, useBookmarkFolder, useCreateBookmarkFolder, useDeleteBookmarkFolder, useUpdateBookmarkFolder }; +const useStatusBookmarkFolders = (statusId: string) => { + const client = useClient(); + const features = useFeatures(); + + return useQuery({ + queryKey: ['bookmarkFolders', 'status', statusId], + queryFn: () => client.statuses.getStatusBookmarkFolders(statusId).then(response => response.map((folder) => folder.id)), + enabled: features.bookmarkFoldersMultiple, + }); +}; + +const useAddBookmarkToFolder = (statusId: string) => { + const client = useClient(); + + return useMutation({ + mutationKey: ['bookmarkFolders', 'add', statusId], + mutationFn: (folderId: string) => client.myAccount.addBookmarkToFolder(statusId, folderId), + onSettled: () => queryClient.invalidateQueries({ queryKey: ['bookmarkFolders', 'status', statusId] }), + }); +}; + +const useRemoveBookmarkFromFolder = (statusId: string) => { + const client = useClient(); + + return useMutation({ + mutationKey: ['bookmarkFolders', 'remove', statusId], + mutationFn: (folderId: string) => client.myAccount.removeBookmarkFromFolder(statusId, folderId), + onSettled: () => queryClient.invalidateQueries({ queryKey: ['bookmarkFolders', 'status', statusId] }), + }); +}; + +export { + useBookmarkFolders, + useBookmarkFolder, + useCreateBookmarkFolder, + useDeleteBookmarkFolder, + useUpdateBookmarkFolder, + useStatusBookmarkFolders, + useAddBookmarkToFolder, + useRemoveBookmarkFromFolder, +};