pl-fe: allow searching folders in SelectBookmarkFolderModal

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-01-06 13:37:48 +01:00
parent f84cfd3c5f
commit 01eb94872b
6 changed files with 58 additions and 27 deletions

View File

@ -17,7 +17,7 @@ const messages = defineMessages({
/** Possible theme names for an Input. */
type InputThemes = 'normal' | 'search' | 'transparent'
interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'maxLength' | 'onChange' | 'onBlur' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required' | 'disabled' | 'onClick' | 'readOnly' | 'min' | 'pattern' | 'onKeyDown' | 'onKeyUp' | 'onFocus' | 'onMouseDown' | 'style' | 'id' | 'lang'> {
interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'maxLength' | 'onChange' | 'onBlur' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required' | 'disabled' | 'onClick' | 'readOnly' | 'min' | 'pattern' | 'onKeyDown' | 'onKeyUp' | 'onFocus' | 'onMouseDown' | 'style' | 'id' | 'lang' | 'title'> {
/** Put the cursor into the input on mount. */
autoFocus?: boolean;
/** The initial text in the input. */

View File

@ -143,7 +143,7 @@ const getLanguageDropdown = (composeId: string): React.FC<ILanguageDropdown> =>
}, [node.current]);
const isSearching = searchValue !== '';
const results = search();
const results = useMemo(search, [searchValue]);
return (
<>

View File

@ -289,6 +289,7 @@
"bookmark_folders.edit.success": "Bookmark folder edited successfully",
"bookmark_folders.new.create_title": "Add folder",
"bookmark_folders.new.title_placeholder": "New folder title",
"bookmark_folders.new.title_with_search_placeholder": "Search or create new folder",
"bookmarks.delete_folder": "Delete folder",
"bookmarks.delete_folder.fail": "Failed to delete folder",
"bookmarks.delete_folder.success": "Folder deleted",

View File

@ -151,16 +151,15 @@ const EditBookmarkFolderModal: React.FC<BaseModalProps & EditBookmarkFolderModal
onPickEmoji={handleEmojiPick}
/>
)}
<label className='grow'>
<span style={{ display: 'none' }}>{label}</span>
<Input
type='text'
placeholder={label}
disabled={isPending}
{...name}
/>
</label>
<Input
outerClassName='grow'
type='text'
placeholder={label}
title={label}
disabled={isPending}
{...name}
/>
</HStack>
</Modal>
);

View File

@ -1,4 +1,6 @@
import React, { useCallback, useState } from 'react';
import fuzzysort from 'fuzzysort';
import { BookmarkFolder } from 'pl-api';
import React, { useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { ListItem } from 'pl-fe/components/list';
@ -23,12 +25,23 @@ interface SelectBookmarkFolderModalProps {
statusId: string;
}
const search = (bookmarkFolders: Array<BookmarkFolder>, term: string) => {
if (!term) return bookmarkFolders;
return fuzzysort.go(term, bookmarkFolders, { key: 'name' }).map(result => result.obj);
};
const SelectBookmarkFolderModal: React.FC<SelectBookmarkFolderModalProps & BaseModalProps> = ({ statusId, onClose }) => {
const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector(state => getStatus(state, { id: statusId }))!;
const features = useFeatures();
const [selectedFolder, setSelectedFolder] = useState(status.bookmark_folder);
const [searchTerm, setSearchTerm] = useState('');
const handleSearchChange: React.ChangeEventHandler<HTMLInputElement> = e => {
setSearchTerm(e.target.value);
};
const { isFetching, data: bookmarkFolders } = useBookmarkFolders(data => data);
const { data: selectedBookmarkFolders, isPending: fetchingSelectedBookmarkFolders } = useStatusBookmarkFolders(statusId);
@ -57,10 +70,18 @@ const SelectBookmarkFolderModal: React.FC<SelectBookmarkFolderModalProps & BaseM
}
};
const filteredFolders = useMemo(() => {
if (!bookmarkFolders) return [];
const filtered = search(bookmarkFolders, searchTerm);
return filtered;
}, [bookmarkFolders, searchTerm]);
let items;
if (features.bookmarkFoldersMultiple) {
items = (bookmarkFolders || []).map((folder) => (
items = (filteredFolders).map((folder) => (
<ListItem
key={folder.id}
label={
@ -99,7 +120,7 @@ const SelectBookmarkFolderModal: React.FC<SelectBookmarkFolderModalProps & BaseM
];
if (!isFetching) {
items.push(...((bookmarkFolders || []).map((folder) => (
items.push(...((filteredFolders).map((folder) => (
<RadioItem
key={folder.id}
label={
@ -123,7 +144,7 @@ const SelectBookmarkFolderModal: React.FC<SelectBookmarkFolderModalProps & BaseM
const body = isFetching ? <Spinner /> : (
<Stack space={4}>
<NewFolderForm />
<NewFolderForm search onChange={handleSearchChange} />
<RadioGroup onChange={onChange}>
{items}

View File

@ -20,17 +20,28 @@ import toast from 'pl-fe/toast';
const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
label: { id: 'bookmark_folders.new.title_placeholder', defaultMessage: 'New folder title' },
labelWithSearch: { id: 'bookmark_folders.new.title_with_search_placeholder', defaultMessage: 'Search or create new folder' },
createSuccess: { id: 'bookmark_folders.add.success', defaultMessage: 'Bookmark folder created successfully' },
createFail: { id: 'bookmark_folders.add.fail', defaultMessage: 'Failed to create bookmark folder' },
});
const NewFolderForm: React.FC = () => {
interface INewFolderForm {
search?: boolean;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
}
const NewFolderForm: React.FC<INewFolderForm> = ({ search, onChange }) => {
const intl = useIntl();
const name = useTextField();
const { mutate: createBookmarkFolder, isPending } = useCreateBookmarkFolder();
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
name.onChange(e);
if (onChange) onChange(e);
};
const handleSubmit = (e: React.FormEvent<Element>) => {
e.preventDefault();
createBookmarkFolder({
@ -45,21 +56,20 @@ const NewFolderForm: React.FC = () => {
});
};
const label = intl.formatMessage(messages.label);
const label = intl.formatMessage(search ? messages.labelWithSearch : messages.label);
return (
<Form onSubmit={handleSubmit}>
<HStack space={2} alignItems='center'>
<label className='grow'>
<span style={{ display: 'none' }}>{label}</span>
<Input
type='text'
placeholder={label}
disabled={isPending}
{...name}
/>
</label>
<Input
outerClassName='grow'
type='text'
placeholder={label}
title={label}
disabled={isPending}
{...name}
onChange={handleChange}
/>
<Button
disabled={isPending}