Add initial collections editor page (#37643)
This commit is contained in:
parent
8a42689268
commit
2427e14446
@ -70,7 +70,7 @@ type CommonPayloadFields = Pick<
|
|||||||
ApiCollectionJSON,
|
ApiCollectionJSON,
|
||||||
'name' | 'description' | 'sensitive' | 'discoverable'
|
'name' | 'description' | 'sensitive' | 'discoverable'
|
||||||
> & {
|
> & {
|
||||||
tag?: string;
|
tag_name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ApiPatchCollectionPayload extends Partial<CommonPayloadFields> {
|
export interface ApiPatchCollectionPayload extends Partial<CommonPayloadFields> {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export { TextInputField } from './text_input_field';
|
export { TextInputField } from './text_input_field';
|
||||||
export { TextAreaField } from './text_area_field';
|
export { TextAreaField } from './text_area_field';
|
||||||
|
export { ToggleField, PlainToggleField } from './toggle_field';
|
||||||
export { SelectField } from './select_field';
|
export { SelectField } from './select_field';
|
||||||
|
|||||||
266
app/javascript/mastodon/features/collections/editor.tsx
Normal file
266
app/javascript/mastodon/features/collections/editor.tsx
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { useParams, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
|
import type {
|
||||||
|
ApiCollectionJSON,
|
||||||
|
ApiCreateCollectionPayload,
|
||||||
|
} from 'mastodon/api_types/collections';
|
||||||
|
import { Button } from 'mastodon/components/button';
|
||||||
|
import { Column } from 'mastodon/components/column';
|
||||||
|
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 { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
edit: { id: 'column.edit_collection', defaultMessage: 'Edit collection' },
|
||||||
|
create: {
|
||||||
|
id: 'column.create_collection',
|
||||||
|
defaultMessage: 'Create collection',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const CollectionSettings: React.FC<{
|
||||||
|
collection?: ApiCollectionJSON | null;
|
||||||
|
}> = ({ collection }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name: initialName = '',
|
||||||
|
description: initialDescription = '',
|
||||||
|
tag,
|
||||||
|
discoverable: initialDiscoverable = true,
|
||||||
|
sensitive: initialSensitive = false,
|
||||||
|
} = collection ?? {};
|
||||||
|
|
||||||
|
const [name, setName] = useState(initialName);
|
||||||
|
const [description, setDescription] = useState(initialDescription);
|
||||||
|
const [topic, setTopic] = useState(tag?.name ?? '');
|
||||||
|
const [discoverable] = useState(initialDiscoverable);
|
||||||
|
const [sensitive, setSensitive] = useState(initialSensitive);
|
||||||
|
|
||||||
|
const handleNameChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setName(event.target.value);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDescriptionChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setDescription(event.target.value);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTopicChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTopic(event.target.value);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSensitiveChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSensitive(event.target.checked);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
(e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
// void dispatch(
|
||||||
|
// updateList({
|
||||||
|
// id,
|
||||||
|
// title,
|
||||||
|
// exclusive,
|
||||||
|
// replies_policy: repliesPolicy,
|
||||||
|
// }),
|
||||||
|
// ).then(() => {
|
||||||
|
// return '';
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
const payload: ApiCreateCollectionPayload = {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
discoverable,
|
||||||
|
sensitive,
|
||||||
|
};
|
||||||
|
if (topic) {
|
||||||
|
payload.tag_name = topic;
|
||||||
|
}
|
||||||
|
void dispatch(
|
||||||
|
createCollection({
|
||||||
|
payload,
|
||||||
|
}),
|
||||||
|
).then((result) => {
|
||||||
|
if (isFulfilled(result)) {
|
||||||
|
history.replace(
|
||||||
|
`/collections/${result.payload.collection.id}/edit`,
|
||||||
|
);
|
||||||
|
history.push(`/collections`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[id, dispatch, name, description, topic, discoverable, sensitive, history],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className='simple_form app-form' onSubmit={handleSubmit}>
|
||||||
|
<div className='fields-group'>
|
||||||
|
<TextInputField
|
||||||
|
required
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.collection_name'
|
||||||
|
defaultMessage='Name'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.name_length_hint'
|
||||||
|
defaultMessage='40 characters limit'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={name}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
maxLength={40}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='fields-group'>
|
||||||
|
<TextAreaField
|
||||||
|
required
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.collection_description'
|
||||||
|
defaultMessage='Description'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.description_length_hint'
|
||||||
|
defaultMessage='100 characters limit'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={description}
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
maxLength={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='fields-group'>
|
||||||
|
<TextInputField
|
||||||
|
required={false}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.collection_topic'
|
||||||
|
defaultMessage='Topic'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.topic_hint'
|
||||||
|
defaultMessage='Add a hashtag that helps others understand the main topic of this collection.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={topic}
|
||||||
|
onChange={handleTopicChange}
|
||||||
|
maxLength={40}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='fields-group'>
|
||||||
|
<ToggleField
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.mark_as_sensitive'
|
||||||
|
defaultMessage='Mark as sensitive'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
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."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
checked={sensitive}
|
||||||
|
onChange={handleSensitiveChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='actions'>
|
||||||
|
<Button type='submit'>
|
||||||
|
{id ? (
|
||||||
|
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='lists.create' defaultMessage='Create' />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CollectionEditorPage: React.FC<{
|
||||||
|
multiColumn?: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { id } = useParams<{ id?: string }>();
|
||||||
|
const collection = useAppSelector((state) =>
|
||||||
|
id ? state.collections.collections[id] : undefined,
|
||||||
|
);
|
||||||
|
const isEditMode = !!id;
|
||||||
|
const isLoading = isEditMode && !collection;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if (id) {
|
||||||
|
// dispatch(fetchCollection(id));
|
||||||
|
// }
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const pageTitle = intl.formatMessage(id ? messages.edit : messages.create);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} label={pageTitle}>
|
||||||
|
<ColumnHeader
|
||||||
|
title={pageTitle}
|
||||||
|
icon='list-ul'
|
||||||
|
iconComponent={ListAltIcon}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='scrollable'>
|
||||||
|
{isLoading ? (
|
||||||
|
<LoadingIndicator />
|
||||||
|
) : (
|
||||||
|
<CollectionSettings collection={collection} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{pageTitle}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -16,7 +16,6 @@ import { Dropdown } from 'mastodon/components/dropdown_menu';
|
|||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
import {
|
import {
|
||||||
createCollection,
|
|
||||||
fetchAccountCollections,
|
fetchAccountCollections,
|
||||||
selectMyCollections,
|
selectMyCollections,
|
||||||
} from 'mastodon/reducers/slices/collections';
|
} from 'mastodon/reducers/slices/collections';
|
||||||
@ -67,7 +66,7 @@ const ListItem: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='lists__item'>
|
<div className='lists__item'>
|
||||||
<Link to={`/collections/${id}`} className='lists__item__title'>
|
<Link to={`/collections/${id}/edit`} className='lists__item__title'>
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -94,24 +93,6 @@ export const Collections: React.FC<{
|
|||||||
void dispatch(fetchAccountCollections({ accountId: me }));
|
void dispatch(fetchAccountCollections({ accountId: me }));
|
||||||
}, [dispatch, me]);
|
}, [dispatch, me]);
|
||||||
|
|
||||||
const addDummyCollection = useCallback(
|
|
||||||
(event: React.MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
void dispatch(
|
|
||||||
createCollection({
|
|
||||||
payload: {
|
|
||||||
name: 'Test Collection',
|
|
||||||
description: 'A useful test collection',
|
|
||||||
discoverable: true,
|
|
||||||
sensitive: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
const emptyMessage =
|
const emptyMessage =
|
||||||
status === 'error' ? (
|
status === 'error' ? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@ -152,7 +133,6 @@ export const Collections: React.FC<{
|
|||||||
className='column-header__button'
|
className='column-header__button'
|
||||||
title={intl.formatMessage(messages.create)}
|
title={intl.formatMessage(messages.create)}
|
||||||
aria-label={intl.formatMessage(messages.create)}
|
aria-label={intl.formatMessage(messages.create)}
|
||||||
onClick={addDummyCollection}
|
|
||||||
>
|
>
|
||||||
<Icon id='plus' icon={AddIcon} />
|
<Icon id='plus' icon={AddIcon} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -64,6 +64,7 @@ import {
|
|||||||
ListEdit,
|
ListEdit,
|
||||||
ListMembers,
|
ListMembers,
|
||||||
Collections,
|
Collections,
|
||||||
|
CollectionsEditor,
|
||||||
Blocks,
|
Blocks,
|
||||||
DomainBlocks,
|
DomainBlocks,
|
||||||
Mutes,
|
Mutes,
|
||||||
@ -229,6 +230,12 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
||||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||||
|
{areCollectionsEnabled() &&
|
||||||
|
<WrappedRoute path='/collections/new' component={CollectionsEditor} content={children} />
|
||||||
|
}
|
||||||
|
{areCollectionsEnabled() &&
|
||||||
|
<WrappedRoute path='/collections/:id/edit' component={CollectionsEditor} content={children} />
|
||||||
|
}
|
||||||
{areCollectionsEnabled() &&
|
{areCollectionsEnabled() &&
|
||||||
<WrappedRoute path='/collections' component={Collections} content={children} />
|
<WrappedRoute path='/collections' component={Collections} content={children} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,12 @@ export function Collections () {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CollectionsEditor () {
|
||||||
|
return import('../../collections/editor').then(
|
||||||
|
module => ({default: module.CollectionEditorPage})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Status () {
|
export function Status () {
|
||||||
return import('../../status');
|
return import('../../status');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -212,21 +212,31 @@
|
|||||||
"closed_registrations_modal.find_another_server": "Find another server",
|
"closed_registrations_modal.find_another_server": "Find another server",
|
||||||
"closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
|
"closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
|
||||||
"closed_registrations_modal.title": "Signing up on Mastodon",
|
"closed_registrations_modal.title": "Signing up on Mastodon",
|
||||||
|
"collections.collection_description": "Description",
|
||||||
|
"collections.collection_name": "Name",
|
||||||
|
"collections.collection_topic": "Topic",
|
||||||
"collections.create_a_collection_hint": "Create a collection to recommend or share your favourite accounts with others.",
|
"collections.create_a_collection_hint": "Create a collection to recommend or share your favourite accounts with others.",
|
||||||
"collections.create_collection": "Create collection",
|
"collections.create_collection": "Create collection",
|
||||||
"collections.delete_collection": "Delete collection",
|
"collections.delete_collection": "Delete collection",
|
||||||
|
"collections.description_length_hint": "100 characters limit",
|
||||||
"collections.error_loading_collections": "There was an error when trying to load your collections.",
|
"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.name_length_hint": "100 characters limit",
|
||||||
"collections.no_collections_yet": "No collections yet.",
|
"collections.no_collections_yet": "No collections yet.",
|
||||||
|
"collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.",
|
||||||
"collections.view_collection": "View collection",
|
"collections.view_collection": "View collection",
|
||||||
"column.about": "About",
|
"column.about": "About",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
"column.bookmarks": "Bookmarks",
|
"column.bookmarks": "Bookmarks",
|
||||||
"column.collections": "My collections",
|
"column.collections": "My collections",
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
|
"column.create_collection": "Create collection",
|
||||||
"column.create_list": "Create list",
|
"column.create_list": "Create list",
|
||||||
"column.direct": "Private mentions",
|
"column.direct": "Private mentions",
|
||||||
"column.directory": "Browse profiles",
|
"column.directory": "Browse profiles",
|
||||||
"column.domain_blocks": "Blocked domains",
|
"column.domain_blocks": "Blocked domains",
|
||||||
|
"column.edit_collection": "Edit collection",
|
||||||
"column.edit_list": "Edit list",
|
"column.edit_list": "Edit list",
|
||||||
"column.favourites": "Favorites",
|
"column.favourites": "Favorites",
|
||||||
"column.firehose": "Live feeds",
|
"column.firehose": "Live feeds",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user