Merge branch 'update-group-header' into 'develop'

Allow Admins to manage/leave Group

See merge request soapbox-pub/soapbox!2410
This commit is contained in:
Chewbacca 2023-04-04 16:35:52 +00:00
commit b704e476eb
9 changed files with 72 additions and 31 deletions

View File

@ -11,24 +11,22 @@ interface IIconButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
src: string src: string
/** Text to display next ot the button. */ /** Text to display next ot the button. */
text?: string text?: string
/** Don't render a background behind the icon. */
transparent?: boolean
/** Predefined styles to display for the button. */ /** Predefined styles to display for the button. */
theme?: 'seamless' | 'outlined' | 'secondary' theme?: 'seamless' | 'outlined' | 'secondary' | 'transparent'
/** Override the data-testid */ /** Override the data-testid */
'data-testid'?: string 'data-testid'?: string
} }
/** A clickable icon. */ /** A clickable icon. */
const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element => { const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element => {
const { src, className, iconClassName, text, transparent = false, theme = 'seamless', ...filteredProps } = props; const { src, className, iconClassName, text, theme = 'seamless', ...filteredProps } = props;
return ( return (
<button <button
ref={ref} ref={ref}
type='button' type='button'
className={clsx('flex items-center space-x-2 rounded-full p-1 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0', { className={clsx('flex items-center space-x-2 rounded-full p-1 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0', {
'bg-white dark:bg-transparent': !transparent, 'bg-white dark:bg-transparent': theme === 'seamless',
'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500': theme === 'outlined', 'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500': theme === 'outlined',
'border-transparent bg-primary-100 dark:bg-primary-800 hover:bg-primary-50 dark:hover:bg-primary-700 focus:bg-primary-100 dark:focus:bg-primary-800 text-primary-500 dark:text-primary-200': theme === 'secondary', 'border-transparent bg-primary-100 dark:bg-primary-800 hover:bg-primary-50 dark:hover:bg-primary-700 focus:bg-primary-100 dark:focus:bg-primary-800 text-primary-500 dark:text-primary-200': theme === 'secondary',
'opacity-50': filteredProps.disabled, 'opacity-50': filteredProps.disabled,

View File

@ -20,7 +20,6 @@ const Tag: React.FC<ITag> = ({ tag, onDelete }) => {
iconClassName='h-4 w-4' iconClassName='h-4 w-4'
src={require('@tabler/icons/x.svg')} src={require('@tabler/icons/x.svg')}
onClick={() => onDelete(tag)} onClick={() => onDelete(tag)}
transparent
/> />
</div> </div>
); );

View File

@ -40,10 +40,11 @@ describe('<GroupOptionsButton />', () => {
}); });
}); });
it('should render null', () => { it('should render one option for leaving the group', () => {
render(<GroupOptionsButton group={group} />); render(<GroupOptionsButton group={group} />);
expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(0); // Leave group option only
expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(1);
}); });
}); });

View File

@ -3,14 +3,14 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
import { Button } from 'soapbox/components/ui'; import { Button } from 'soapbox/components/ui';
import { deleteEntities } from 'soapbox/entity-store/actions'; import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities'; import { Entities } from 'soapbox/entity-store/entities';
import { useAppDispatch } from 'soapbox/hooks'; import { useAppDispatch } from 'soapbox/hooks';
import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/hooks/api'; import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/hooks/api';
import { GroupRoles } from 'soapbox/schemas/group-member'; import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast'; import toast from 'soapbox/toast';
import type { Group } from 'soapbox/types/entities'; import type { Group, GroupRelationship } from 'soapbox/types/entities';
interface IGroupActionButton { interface IGroupActionButton {
group: Group group: Group
@ -36,6 +36,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
const isRequested = group.relationship?.requested; const isRequested = group.relationship?.requested;
const isNonMember = !group.relationship?.member && !isRequested; const isNonMember = !group.relationship?.member && !isRequested;
const isOwner = group.relationship?.role === GroupRoles.OWNER; const isOwner = group.relationship?.role === GroupRoles.OWNER;
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
const isBlocked = group.relationship?.blocked_by; const isBlocked = group.relationship?.blocked_by;
const onJoinGroup = () => joinGroup.mutate({}, { const onJoinGroup = () => joinGroup.mutate({}, {
@ -65,7 +66,11 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
const onCancelRequest = () => cancelRequest.mutate({}, { const onCancelRequest = () => cancelRequest.mutate({}, {
onSuccess() { onSuccess() {
dispatch(deleteEntities([group.id], Entities.GROUP_RELATIONSHIPS)); const entity = {
...group.relationship as GroupRelationship,
requested: false,
};
dispatch(importEntities([entity], Entities.GROUP_RELATIONSHIPS));
}, },
}); });
@ -73,7 +78,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
return null; return null;
} }
if (isOwner) { if (isOwner || isAdmin) {
return ( return (
<Button <Button
theme='secondary' theme='secondary'

View File

@ -1,15 +1,23 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { openModal } from 'soapbox/actions/modals';
import { initReport, ReportableEntities } from 'soapbox/actions/reports'; import { initReport, ReportableEntities } from 'soapbox/actions/reports';
import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu'; import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu';
import { IconButton } from 'soapbox/components/ui'; import { IconButton } from 'soapbox/components/ui';
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks'; import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
import { useLeaveGroup } from 'soapbox/hooks/api';
import { GroupRoles } from 'soapbox/schemas/group-member'; import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast';
import type { Account, Group } from 'soapbox/types/entities'; import type { Account, Group } from 'soapbox/types/entities';
const messages = defineMessages({ const messages = defineMessages({
confirmationConfirm: { id: 'confirmations.leave_group.confirm', defaultMessage: 'Leave' },
confirmationHeading: { id: 'confirmations.leave_group.heading', defaultMessage: 'Leave group' },
confirmationMessage: { id: 'confirmations.leave_group.message', defaultMessage: 'You are about to leave the group. Do you want to continue?' },
leave: { id: 'group.leave.label', defaultMessage: 'Leave' },
leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' },
report: { id: 'group.report.label', defaultMessage: 'Report' }, report: { id: 'group.report.label', defaultMessage: 'Report' },
}); });
@ -21,19 +29,48 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => {
const account = useOwnAccount(); const account = useOwnAccount();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const leaveGroup = useLeaveGroup(group);
const isMember = group.relationship?.role === GroupRoles.USER; const isMember = group.relationship?.role === GroupRoles.USER;
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
const isBlocked = group.relationship?.blocked_by; const isBlocked = group.relationship?.blocked_by;
const menu: Menu = useMemo(() => ([ const onLeaveGroup = () =>
{ dispatch(openModal('CONFIRM', {
text: intl.formatMessage(messages.report), heading: intl.formatMessage(messages.confirmationHeading),
icon: require('@tabler/icons/flag.svg'), message: intl.formatMessage(messages.confirmationMessage),
action: () => dispatch(initReport(ReportableEntities.GROUP, account as Account, { group })), confirm: intl.formatMessage(messages.confirmationConfirm),
}, onConfirm: () => leaveGroup.mutate(group.relationship?.id as string, {
]), []); onSuccess() {
leaveGroup.invalidate();
toast.success(intl.formatMessage(messages.leaveSuccess));
},
}),
}));
if (isBlocked || !isMember || menu.length === 0) { const menu: Menu = useMemo(() => {
const items = [];
if (isMember) {
items.push({
text: intl.formatMessage(messages.report),
icon: require('@tabler/icons/flag.svg'),
action: () => dispatch(initReport(ReportableEntities.GROUP, account as Account, { group })),
});
}
if (isAdmin) {
items.push({
text: intl.formatMessage(messages.leave),
icon: require('@tabler/icons/logout.svg'),
action: onLeaveGroup,
});
}
return items;
}, [isMember, isAdmin]);
if (isBlocked || menu.length === 0) {
return null; return null;
} }

View File

@ -19,8 +19,8 @@ type RouteParams = { id: string };
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.manage_group', defaultMessage: 'Manage group' }, heading: { id: 'column.manage_group', defaultMessage: 'Manage group' },
editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit group' }, editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit group' },
pendingRequests: { id: 'manage_group.pending_requests', defaultMessage: 'Pending requests' }, pendingRequests: { id: 'manage_group.pending_requests', defaultMessage: 'Pending Requests' },
blockedMembers: { id: 'manage_group.blocked_members', defaultMessage: 'Banned members' }, blockedMembers: { id: 'manage_group.blocked_members', defaultMessage: 'Banned Members' },
deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete group' }, deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete group' },
deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' },
deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete group' }, deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete group' },

View File

@ -147,7 +147,6 @@ const Header = () => {
src={require('@tabler/icons/help.svg')} src={require('@tabler/icons/help.svg')}
className='cursor-pointer bg-transparent text-gray-700 hover:text-gray-800 dark:text-gray-600 dark:hover:text-gray-500' className='cursor-pointer bg-transparent text-gray-700 hover:text-gray-800 dark:text-gray-600 dark:hover:text-gray-500'
iconClassName='h-5 w-5' iconClassName='h-5 w-5'
transparent
/> />
</Tooltip> </Tooltip>
</Link> </Link>

View File

@ -1,15 +1,16 @@
import { Entities } from 'soapbox/entity-store/entities'; import { Entities } from 'soapbox/entity-store/entities';
import { useEntityActions } from 'soapbox/entity-store/hooks'; import { useCreateEntity } from 'soapbox/entity-store/hooks';
import { useOwnAccount } from 'soapbox/hooks'; import { useApi, useOwnAccount } from 'soapbox/hooks';
import type { Group, GroupRelationship } from 'soapbox/schemas'; import type { Group } from 'soapbox/schemas';
function useCancelMembershipRequest(group: Group) { function useCancelMembershipRequest(group: Group) {
const api = useApi();
const me = useOwnAccount(); const me = useOwnAccount();
const { createEntity, isLoading } = useEntityActions<GroupRelationship>( const { createEntity, isLoading } = useCreateEntity(
[Entities.GROUP_RELATIONSHIPS, group.id], [Entities.GROUP_RELATIONSHIPS],
{ post: `/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject` }, () => api.post(`/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject`),
); );
return { return {

View File

@ -786,6 +786,7 @@
"group.join.request_success": "Request sent to group owner", "group.join.request_success": "Request sent to group owner",
"group.join.success": "Group joined successfully!", "group.join.success": "Group joined successfully!",
"group.leave": "Leave Group", "group.leave": "Leave Group",
"group.leave.label": "Leave",
"group.leave.success": "Left the group", "group.leave.success": "Left the group",
"group.manage": "Manage Group", "group.manage": "Manage Group",
"group.popover.action": "View Group", "group.popover.action": "View Group",
@ -933,7 +934,7 @@
"login_external.errors.instance_fail": "The instance returned an error.", "login_external.errors.instance_fail": "The instance returned an error.",
"login_external.errors.network_fail": "Connection failed. Is a browser extension blocking it?", "login_external.errors.network_fail": "Connection failed. Is a browser extension blocking it?",
"login_form.header": "Sign In", "login_form.header": "Sign In",
"manage_group.blocked_members": "Banned members", "manage_group.blocked_members": "Banned Members",
"manage_group.confirmation.copy": "Copy link", "manage_group.confirmation.copy": "Copy link",
"manage_group.confirmation.info_1": "As the owner of this group, you can assign staff, delete posts and much more.", "manage_group.confirmation.info_1": "As the owner of this group, you can assign staff, delete posts and much more.",
"manage_group.confirmation.info_2": "Post the group's first post and get the conversation started.", "manage_group.confirmation.info_2": "Post the group's first post and get the conversation started.",
@ -954,7 +955,7 @@
"manage_group.fields.name_placeholder": "Group Name", "manage_group.fields.name_placeholder": "Group Name",
"manage_group.get_started": "Lets get started!", "manage_group.get_started": "Lets get started!",
"manage_group.next": "Next", "manage_group.next": "Next",
"manage_group.pending_requests": "Pending requests", "manage_group.pending_requests": "Pending Requests",
"manage_group.privacy.hint": "These settings cannot be changed later.", "manage_group.privacy.hint": "These settings cannot be changed later.",
"manage_group.privacy.label": "Privacy settings", "manage_group.privacy.label": "Privacy settings",
"manage_group.privacy.private.hint": "Discoverable. Users can join after their request is approved.", "manage_group.privacy.private.hint": "Discoverable. Users can join after their request is approved.",