Add ability to update Group tags

This commit is contained in:
Chewbacca 2023-04-14 15:15:34 -04:00
parent 2d52c8c3e4
commit c5c5bd0d62
5 changed files with 105 additions and 59 deletions

View file

@ -31,10 +31,14 @@ function useEntityActions<TEntity extends Entity = Entity, Data = any>(
const { createEntity, isSubmitting: createSubmitting } = const { createEntity, isSubmitting: createSubmitting } =
useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts); useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts);
const { createEntity: updateEntity, isSubmitting: updateSubmitting } =
useCreateEntity<TEntity, Data>(path, (data) => api.patch(endpoints.patch!, data), opts);
return { return {
createEntity, createEntity,
deleteEntity, deleteEntity,
isSubmitting: createSubmitting || deleteSubmitting, updateEntity,
isSubmitting: createSubmitting || deleteSubmitting || updateSubmitting,
}; };
} }

View file

@ -3,7 +3,11 @@ import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { HStack, IconButton, Stack, Text, Tooltip } from 'soapbox/components/ui'; import { HStack, IconButton, Stack, Text, Tooltip } from 'soapbox/components/ui';
import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities';
import { useAppDispatch } from 'soapbox/hooks';
import { useUpdateGroupTag } from 'soapbox/hooks/api'; import { useUpdateGroupTag } from 'soapbox/hooks/api';
import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast'; import toast from 'soapbox/toast';
import { shortNumberFormat } from 'soapbox/utils/numbers'; import { shortNumberFormat } from 'soapbox/utils/numbers';
@ -29,15 +33,26 @@ interface IGroupMemberListItem {
const GroupTagListItem = (props: IGroupMemberListItem) => { const GroupTagListItem = (props: IGroupMemberListItem) => {
const { group, tag, isPinnable } = props; const { group, tag, isPinnable } = props;
const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const updateGroupTag = useUpdateGroupTag(group.id, tag.id); const { updateGroupTag } = useUpdateGroupTag(group.id, tag.id);
const isOwner = group.relationship?.role === GroupRoles.OWNER;
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
const canEdit = isOwner || isAdmin;
const toggleVisibility = () => { const toggleVisibility = () => {
updateGroupTag({ updateGroupTag({
pinned: !tag.visible, group_tag_type: tag.visible ? 'hidden' : 'normal',
}, { }, {
onSuccess(entity: GroupTag) { onSuccess() {
const entity = {
...tag,
visible: !tag.visible,
};
dispatch(importEntities([entity], Entities.GROUP_TAGS));
toast.success( toast.success(
entity.visible ? entity.visible ?
intl.formatMessage(messages.visibleSuccess) : intl.formatMessage(messages.visibleSuccess) :
@ -49,9 +64,15 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
const togglePin = () => { const togglePin = () => {
updateGroupTag({ updateGroupTag({
pinned: !tag.pinned, group_tag_type: tag.pinned ? 'normal' : 'pinned',
}, { }, {
onSuccess(entity: GroupTag) { onSuccess() {
const entity = {
...tag,
pinned: !tag.pinned,
};
dispatch(importEntities([entity], Entities.GROUP_TAGS));
toast.success( toast.success(
entity.pinned ? entity.pinned ?
intl.formatMessage(messages.pinSuccess) : intl.formatMessage(messages.pinSuccess) :
@ -73,7 +94,7 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
> >
<IconButton <IconButton
onClick={togglePin} onClick={togglePin}
transparent theme='transparent'
src={ src={
tag.pinned ? tag.pinned ?
require('@tabler/icons/pin-filled.svg') : require('@tabler/icons/pin-filled.svg') :
@ -90,7 +111,7 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
<Tooltip text={intl.formatMessage(messages.unpinTag)}> <Tooltip text={intl.formatMessage(messages.unpinTag)}>
<IconButton <IconButton
onClick={togglePin} onClick={togglePin}
transparent theme='transparent'
src={require('@tabler/icons/pin-filled.svg')} src={require('@tabler/icons/pin-filled.svg')}
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue' iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
/> />
@ -106,12 +127,12 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
<Stack> <Stack>
<Text <Text
weight='bold' weight='bold'
theme={tag.visible ? 'default' : 'subtle'} theme={(tag.visible || !canEdit) ? 'default' : 'subtle'}
className='group-hover:underline' className='group-hover:underline'
> >
#{tag.name} #{tag.name}
</Text> </Text>
<Text size='sm' theme={tag.visible ? 'muted' : 'subtle'}> <Text size='sm' theme={(tag.visible || !canEdit) ? 'muted' : 'subtle'}>
{intl.formatMessage(messages.total)}: {intl.formatMessage(messages.total)}:
{' '} {' '}
<Text size='sm' theme='inherit' weight='semibold' tag='span'> <Text size='sm' theme='inherit' weight='semibold' tag='span'>
@ -121,30 +142,32 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
</Stack> </Stack>
</Link> </Link>
<HStack alignItems='center' space={2}> {canEdit ? (
{tag.visible ? ( <HStack alignItems='center' space={2}>
renderPinIcon() {tag.visible ? (
) : null} renderPinIcon()
) : null}
<Tooltip <Tooltip
text={ text={
tag.visible ?
intl.formatMessage(messages.hideTag) :
intl.formatMessage(messages.showTag)
}
>
<IconButton
onClick={toggleVisibility}
transparent
src={
tag.visible ? tag.visible ?
require('@tabler/icons/eye.svg') : intl.formatMessage(messages.hideTag) :
require('@tabler/icons/eye-off.svg') intl.formatMessage(messages.showTag)
} }
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue' >
/> <IconButton
</Tooltip> onClick={toggleVisibility}
</HStack> theme='transparent'
src={
tag.visible ?
require('@tabler/icons/eye.svg') :
require('@tabler/icons/eye-off.svg')
}
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
/>
</Tooltip>
</HStack>
) : null}
</HStack> </HStack>
); );
}; };

View file

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl';
import ScrollableList from 'soapbox/components/scrollable-list'; import ScrollableList from 'soapbox/components/scrollable-list';
import { Icon, Stack, Text } from 'soapbox/components/ui';
import { useGroupTags } from 'soapbox/hooks/api'; import { useGroupTags } from 'soapbox/hooks/api';
import { useGroup } from 'soapbox/queries/groups'; import { useGroup } from 'soapbox/queries/groups';
@ -26,28 +28,41 @@ const GroupTopics: React.FC<IGroupTopics> = (props) => {
const isPinnable = pinnedTags.length < 3; const isPinnable = pinnedTags.length < 3;
return ( return (
<> <ScrollableList
<ScrollableList scrollKey='group-tags'
scrollKey='group-tags' hasMore={hasNextPage}
hasMore={hasNextPage} onLoadMore={fetchNextPage}
onLoadMore={fetchNextPage} isLoading={isLoading || !group}
isLoading={isLoading || !group} showLoading={!group || isLoading && tags.length === 0}
showLoading={!group || isLoading && tags.length === 0} placeholderComponent={PlaceholderAccount}
placeholderComponent={PlaceholderAccount} placeholderCount={3}
placeholderCount={3} className='divide-y divide-solid divide-gray-300'
className='divide-y divide-solid divide-gray-300' itemClassName='py-3 last:pb-0'
itemClassName='py-3 last:pb-0' emptyMessage={
> <Stack space={4} className='pt-6' justifyContent='center' alignItems='center'>
{tags.map((tag) => ( <div className='rounded-full bg-gray-200 p-4 dark:bg-gray-800'>
<GroupTagListItem <Icon
key={tag.id} src={require('@tabler/icons/hash.svg')}
group={group as Group} className='h-6 w-6 text-gray-600'
isPinnable={isPinnable} />
tag={tag} </div>
/>
))} <Text theme='muted'>
</ScrollableList> <FormattedMessage id='group.tags.empty' defaultMessage='There are no topics in this group yet.' />
</> </Text>
</Stack>
}
emptyMessageCard={false}
>
{tags.map((tag) => (
<GroupTagListItem
key={tag.id}
group={group as Group}
isPinnable={isPinnable}
tag={tag}
/>
))}
</ScrollableList>
); );
}; };

View file

@ -1,13 +1,16 @@
import { Entities } from 'soapbox/entity-store/entities'; import { Entities } from 'soapbox/entity-store/entities';
import { useEntities } from 'soapbox/entity-store/hooks'; import { useEntities } from 'soapbox/entity-store/hooks';
import { useApi } from 'soapbox/hooks/useApi';
import { groupTagSchema } from 'soapbox/schemas'; import { groupTagSchema } from 'soapbox/schemas';
import type { GroupTag } from 'soapbox/schemas'; import type { GroupTag } from 'soapbox/schemas';
function useGroupTags(groupId: string) { function useGroupTags(groupId: string) {
const api = useApi();
const { entities, ...result } = useEntities<GroupTag>( const { entities, ...result } = useEntities<GroupTag>(
[Entities.GROUP_TAGS, groupId], [Entities.GROUP_TAGS, groupId],
'/api/mock/groups/tags', // `api/v1/groups/${groupId}/tags` () => api.get(`api/v1/truth/trends/groups/${groupId}/tags`),
{ schema: groupTagSchema }, { schema: groupTagSchema },
); );

View file

@ -1,17 +1,18 @@
import { Entities } from 'soapbox/entity-store/entities'; import { Entities } from 'soapbox/entity-store/entities';
import { useEntityActions } from 'soapbox/entity-store/hooks'; import { useEntityActions } from 'soapbox/entity-store/hooks';
import { groupTagSchema } from 'soapbox/schemas';
import type { GroupTag } from 'soapbox/schemas'; import type { GroupTag } from 'soapbox/schemas';
function useUpdateGroupTag(groupId: string, tagId: string) { function useUpdateGroupTag(groupId: string, tagId: string) {
const { updateEntity } = useEntityActions<GroupTag>( const { updateEntity, ...rest } = useEntityActions<GroupTag>(
[Entities.GROUP_TAGS, groupId, tagId], [Entities.GROUP_TAGS, groupId, tagId],
{ patch: `/api/mock/truth/groups/${groupId}/tags/${tagId}` }, { patch: `/api/v1/groups/${groupId}/tags/${tagId}` },
{ schema: groupTagSchema },
); );
return updateEntity; return {
updateGroupTag: updateEntity,
...rest,
};
} }
export { useUpdateGroupTag }; export { useUpdateGroupTag };