0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Improved staff empty display (#19810)

ref https://linear.app/tryghost/issue/DES-84

- changed display to not show tabs when there's no staff users (only owner)
- automatically switch to Invites tab in the Staff section after sending an invite
- updated toast messages on failure

---------

Co-authored-by: Steve Larson <9larsons@gmail.com>
This commit is contained in:
Peter Zimon 2024-03-27 14:21:38 +01:00 committed by GitHub
parent f8a55de743
commit 7dcddb2e75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 54 additions and 23 deletions

View file

@ -105,9 +105,11 @@ export interface TabViewProps<ID = string> {
buttonBorder?: boolean;
width?: TabWidth;
containerClassName?: string;
testId?: string;
}
function TabView<ID extends string = string>({
testId,
tabs,
onTabChange,
selectedTab,
@ -130,7 +132,7 @@ function TabView<ID extends string = string>({
};
return (
<section className={containerClassName}>
<section className={containerClassName} data-testid={testId}>
<TabList
border={border}
buttonBorder={buttonBorder}

View file

@ -1,5 +1,6 @@
import NiceModal from '@ebay/nice-modal-react';
import validator from 'validator';
import {APIError} from '@tryghost/admin-x-framework/errors';
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
import {Modal, Radio, TextField, showToast} from '@tryghost/admin-x-design-system';
import {useAddInvite, useBrowseInvites} from '@tryghost/admin-x-framework/api/invites';
@ -127,13 +128,21 @@ const InviteUserModal = NiceModal.create(() => {
});
modal.remove();
updateRoute('staff');
updateRoute('staff?tab=invited');
} catch (e) {
setSaveState('error');
let message = (<span><strong>Your invitation failed to send.</strong><br/>If the problem persists, <a href="https://ghost.org/contact"><u>contact support</u>.</a>.</span>);
if (e instanceof APIError) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let data = e.data as any; // we have unknown data types in the APIError/error classes
if (data?.errors?.[0]?.type === 'EmailError') {
message = (<span><strong>Your invitation failed to send</strong><br/>Please check your Mailgun configuration. If the problem persists, <a href="https://ghost.org/contact"><u>contact support</u>.</a></span>);
}
}
showToast({
message: `Failed to send invitation to ${email}`,
type: 'error'
message,
type: 'neutral',
icon: 'warning'
});
handleError(e, {withToast: false});
return;

View file

@ -1,6 +1,7 @@
import React, {useState} from 'react';
import React, {useEffect, useState} from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import clsx from 'clsx';
import useQueryParams from '../../../hooks/useQueryParams';
import useStaffUsers from '../../../hooks/useStaffUsers';
import {Avatar, Button, List, ListItem, NoValueLabel, TabView, showToast, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {User, hasAdminAccess, isContributorUser, isEditorUser} from '@tryghost/admin-x-framework/api/users';
@ -223,33 +224,51 @@ const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords,
}} />
);
const [selectedTab, setSelectedTab] = useState('users-admins');
const tabParam = useQueryParams().getParam('tab');
const defaultTab = tabParam || 'administrators';
const [selectedTab, setSelectedTab] = useState(defaultTab);
useEffect(() => {
if (tabParam) {
setSelectedTab(tabParam);
}
}, [tabParam]);
const updateSelectedTab = (newTab: string) => {
updateRoute(`staff?tab=${newTab}`);
setSelectedTab(newTab);
};
const tabs = [
{
id: 'users-admins',
id: 'administrators',
title: 'Administrators',
contents: (<UsersList groupname='administrators' users={adminUsers} />)
contents: (<UsersList groupname='administrators' users={adminUsers} />),
counter: adminUsers.length ? adminUsers.length : undefined
},
{
id: 'users-editors',
id: 'editors',
title: 'Editors',
contents: (<UsersList groupname='editors' users={editorUsers} />)
contents: (<UsersList groupname='editors' users={editorUsers} />),
counter: editorUsers.length ? editorUsers.length : undefined
},
{
id: 'users-authors',
id: 'authors',
title: 'Authors',
contents: (<UsersList groupname='authors' users={authorUsers} />)
contents: (<UsersList groupname='authors' users={authorUsers} />),
counter: authorUsers.length ? authorUsers.length : undefined
},
{
id: 'users-contributors',
id: 'contributors',
title: 'Contributors',
contents: (<UsersList groupname='contributors' users={contributorUsers} />)
contents: (<UsersList groupname='contributors' users={contributorUsers} />),
counter: contributorUsers.length ? contributorUsers.length : undefined
},
{
id: 'users-invited',
id: 'invited',
title: 'Invited',
contents: (<InvitesUserList users={invites} />)
contents: (<InvitesUserList users={invites} />),
counter: invites.length ? invites.length : undefined
}
];
@ -263,7 +282,8 @@ const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords,
title='Staff'
>
<Owner user={ownerUser} />
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
{/* if there are no users besides the owner user, hide the tabs*/}
{(users.length > 1 || invites.length > 0) && <TabView selectedTab={selectedTab} tabs={tabs} testId='user-tabview' onTabChange={updateSelectedTab} />}
{hasNextPage && <Button
label={`Load more (showing ${users.length}/${totalUsers} users)`}
link

View file

@ -128,7 +128,7 @@ test.describe('User actions', async () => {
await confirmation.getByRole('button', {name: 'Delete user'}).click();
await expect(page.getByTestId('toast-success')).toHaveText(/User deleted/);
await expect(activeTab.getByTestId('user-list-item')).toHaveCount(0);
await expect(activeTab.getByTestId('user-tabview')).toHaveCount(0);
expect(lastApiRequests.deleteUser?.url).toMatch(new RegExp(`/users/${authorUser.id}`));
});

View file

@ -16,10 +16,10 @@ test.describe('User roles', async () => {
await expect(section.getByTestId('owner-user')).toHaveText(/owner@test\.com/);
await expect(section.getByRole('tab')).toHaveText([
'Administrators',
'Editors',
'Authors',
'Contributors',
'Administrators1',
'Editors1',
'Authors1',
'Contributors1',
'Invited'
]);