[Glitch] Add initial collections editor page

Port 2427e14446cf340c77daa791b32217c4e4e28c85 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion 2026-01-29 10:06:49 +01:00 committed by Claire
parent a434d73a08
commit 60314c1077
6 changed files with 282 additions and 22 deletions

View File

@ -70,7 +70,7 @@ type CommonPayloadFields = Pick<
ApiCollectionJSON,
'name' | 'description' | 'sensitive' | 'discoverable'
> & {
tag?: string;
tag_name?: string;
};
export interface ApiPatchCollectionPayload extends Partial<CommonPayloadFields> {

View File

@ -1,3 +1,4 @@
export { TextInputField } from './text_input_field';
export { TextAreaField } from './text_area_field';
export { ToggleField, PlainToggleField } from './toggle_field';
export { SelectField } from './select_field';

View 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 'flavours/glitch/api_types/collections';
import { Button } from 'flavours/glitch/components/button';
import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { TextAreaField, ToggleField } from 'flavours/glitch/components/form_fields';
import { TextInputField } from 'flavours/glitch/components/form_fields/text_input_field';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { createCollection } from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/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>
);
};

View File

@ -16,7 +16,6 @@ import { Dropdown } from 'flavours/glitch/components/dropdown_menu';
import { Icon } from 'flavours/glitch/components/icon';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import {
createCollection,
fetchAccountCollections,
selectMyCollections,
} from 'flavours/glitch/reducers/slices/collections';
@ -67,7 +66,7 @@ const ListItem: React.FC<{
return (
<div className='lists__item'>
<Link to={`/collections/${id}`} className='lists__item__title'>
<Link to={`/collections/${id}/edit`} className='lists__item__title'>
<span>{name}</span>
</Link>
@ -94,24 +93,6 @@ export const Collections: React.FC<{
void dispatch(fetchAccountCollections({ accountId: 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 =
status === 'error' ? (
<FormattedMessage
@ -152,7 +133,6 @@ export const Collections: React.FC<{
className='column-header__button'
title={intl.formatMessage(messages.create)}
aria-label={intl.formatMessage(messages.create)}
onClick={addDummyCollection}
>
<Icon id='plus' icon={AddIcon} />
</Link>

View File

@ -67,6 +67,7 @@ import {
ListEdit,
ListMembers,
Collections,
CollectionsEditor,
Blocks,
DomainBlocks,
Mutes,
@ -237,6 +238,12 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
<WrappedRoute path='/mutes' component={Mutes} 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() &&
<WrappedRoute path='/collections' component={Collections} content={children} />
}

View File

@ -50,6 +50,12 @@ export function Collections () {
);
}
export function CollectionsEditor () {
return import('../../collections/editor').then(
module => ({default: module.CollectionEditorPage})
);
}
export function Status () {
return import('../../status');
}