diff --git a/apps/admin-x-settings/src/App.tsx b/apps/admin-x-settings/src/App.tsx index 8668dc5db0..0f1d39811c 100644 --- a/apps/admin-x-settings/src/App.tsx +++ b/apps/admin-x-settings/src/App.tsx @@ -1,10 +1,7 @@ -import ExitSettingsButton from './components/ExitSettingsButton'; import GlobalDataProvider from './components/providers/GlobalDataProvider'; -import Heading from './admin-x-ds/global/Heading'; +import MainContent from './MainContent'; import NiceModal from '@ebay/nice-modal-react'; import RoutingProvider, {ExternalLink} from './components/providers/RoutingProvider'; -import Settings from './components/Settings'; -import Sidebar from './components/Sidebar'; import clsx from 'clsx'; import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState'; import {OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider'; @@ -49,27 +46,7 @@ function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, d > -
- -
- - {/* Main container */} -
- - {/* Sidebar */} -
-
- Settings -
-
- -
-
-
- {/*
*/} - -
-
+
diff --git a/apps/admin-x-settings/src/MainContent.tsx b/apps/admin-x-settings/src/MainContent.tsx new file mode 100644 index 0000000000..428fdb1673 --- /dev/null +++ b/apps/admin-x-settings/src/MainContent.tsx @@ -0,0 +1,67 @@ +import ExitSettingsButton from './components/ExitSettingsButton'; +import Heading from './admin-x-ds/global/Heading'; +import Settings from './components/Settings'; +import Sidebar from './components/Sidebar'; +import Users from './components/settings/general/Users'; +import useRouting from './hooks/useRouting'; +import {ReactNode, useEffect} from 'react'; +import {canAccessSettings, isEditorUser} from './api/users'; +import {useGlobalData} from './components/providers/GlobalDataProvider'; + +const Page: React.FC<{children: ReactNode}> = ({children}) => { + return <> +
+ +
+ +
+ {children} +
+ ; +}; + +const MainContent: React.FC = () => { + const {currentUser} = useGlobalData(); + const {route, updateRoute} = useRouting(); + + useEffect(() => { + if (!canAccessSettings(currentUser) && route !== `users/show/${currentUser.slug}`) { + updateRoute(`users/show/${currentUser.slug}`); + } + }, [currentUser, route, updateRoute]); + + if (!canAccessSettings(currentUser)) { + return null; + } + + if (isEditorUser(currentUser)) { + return ( + +
+ Settings + +
+
+ ); + } + + return ( + + {/* Sidebar */} +
+
+ Settings +
+
+ +
+
+
+ {/*
*/} + +
+
+ ); +}; + +export default MainContent; diff --git a/apps/admin-x-settings/src/api/users.ts b/apps/admin-x-settings/src/api/users.ts index 0aedc76b9c..00b294bfca 100644 --- a/apps/admin-x-settings/src/api/users.ts +++ b/apps/admin-x-settings/src/api/users.ts @@ -145,3 +145,19 @@ export function isAdminUser(user: User) { export function isEditorUser(user: User) { return user.roles.some(role => role.name === 'Editor'); } + +export function isAuthorUser(user: User) { + return user.roles.some(role => role.name === 'Author'); +} + +export function isContributorUser(user: User) { + return user.roles.some(role => role.name === 'Contributor'); +} + +export function canAccessSettings(user: User) { + return isOwnerUser(user) || isAdminUser(user) || isEditorUser(user); +} + +export function hasAdminAccess(user: User) { + return isOwnerUser(user) || isAdminUser(user); +} diff --git a/apps/admin-x-settings/src/components/Settings.tsx b/apps/admin-x-settings/src/components/Settings.tsx index f15cc391b9..d3c711dc84 100644 --- a/apps/admin-x-settings/src/components/Settings.tsx +++ b/apps/admin-x-settings/src/components/Settings.tsx @@ -5,24 +5,15 @@ import EmailSettings from './settings/email/EmailSettings'; import GeneralSettings from './settings/general/GeneralSettings'; import MembershipSettings from './settings/membership/MembershipSettings'; import SiteSettings from './settings/site/SiteSettings'; -import Users from './settings/general/Users'; -import {isEditorUser} from '../api/users'; -import {useGlobalData} from './providers/GlobalDataProvider'; const Settings: React.FC = () => { - const {currentUser} = useGlobalData(); - return (
- {isEditorUser(currentUser) ? - - : <> - - - - - - } + + + + +
Click here to open the original Admin settings.
diff --git a/apps/admin-x-settings/src/components/Sidebar.tsx b/apps/admin-x-settings/src/components/Sidebar.tsx index 26270ca56c..354f5eb3b7 100644 --- a/apps/admin-x-settings/src/components/Sidebar.tsx +++ b/apps/admin-x-settings/src/components/Sidebar.tsx @@ -9,7 +9,6 @@ import {searchKeywords as advancedSearchKeywords} from './settings/advanced/Adva import {searchKeywords as emailSearchKeywords} from './settings/email/EmailSettings'; import {searchKeywords as generalSearchKeywords} from './settings/general/GeneralSettings'; import {getSettingValues} from '../api/settings'; -import {isEditorUser} from '../api/users'; import {searchKeywords as membershipSearchKeywords} from './settings/membership/MembershipSettings'; import {searchKeywords as siteSearchKeywords} from './settings/site/SiteSettings'; import {useGlobalData} from './providers/GlobalDataProvider'; @@ -19,7 +18,7 @@ const Sidebar: React.FC = () => { const {filter, setFilter} = useSearch(); const {updateRoute} = useRouting(); - const {settings, config, currentUser} = useGlobalData(); + const {settings, config} = useGlobalData(); const [newslettersEnabled] = getSettingValues(settings, ['editor_default_email_recipients']) as [string]; const handleSectionClick = (e: React.MouseEvent) => { @@ -38,11 +37,6 @@ const Sidebar: React.FC = () => { } }; - // Editors can only see staff settings, so no point in showing navigation - if (isEditorUser(currentUser)) { - return null; - } - return (
diff --git a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx index 4fcc120d74..feb9a7a0b3 100644 --- a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx @@ -8,7 +8,7 @@ import Menu, {MenuItem} from '../../../admin-x-ds/global/Menu'; import Modal from '../../../admin-x-ds/global/modal/Modal'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import Radio from '../../../admin-x-ds/global/form/Radio'; -import React, {useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent'; import TextArea from '../../../admin-x-ds/global/form/TextArea'; @@ -21,7 +21,7 @@ import useStaffUsers from '../../../hooks/useStaffUsers'; import validator from 'validator'; import {HostLimitError, useLimiter} from '../../../hooks/useLimiter'; import {RoutingModalProps} from '../../providers/RoutingProvider'; -import {User, isAdminUser, isOwnerUser, useDeleteUser, useEditUser, useMakeOwner, useUpdatePassword} from '../../../api/users'; +import {User, canAccessSettings, hasAdminAccess, isAdminUser, isOwnerUser, useDeleteUser, useEditUser, useMakeOwner, useUpdatePassword} from '../../../api/users'; import {genStaffToken, getStaffToken} from '../../../api/staffToken'; import {getImageUrl, useUploadImage} from '../../../api/images'; import {getSettingValues} from '../../../api/settings'; @@ -108,6 +108,8 @@ const RoleSelector: React.FC = ({user, setUserData}) => { }; const BasicInputs: React.FC = ({errors, validators, user, setUserData}) => { + const {currentUser} = useGlobalData(); + return ( = ({errors, validators, user, setUs setUserData?.({...user, email: e.target.value}); }} /> - + {hasAdminAccess(currentUser) && } ); }; @@ -227,6 +229,7 @@ const Details: React.FC = ({errors, validators, user, setUserDa const EmailNotificationsInputs: React.FC = ({user, setUserData}) => { const hasWebmentions = useFeatureFlag('webmentions'); + const {currentUser} = useGlobalData(); return ( @@ -239,51 +242,53 @@ const EmailNotificationsInputs: React.FC = ({user, setUserData} setUserData?.({...user, comment_notifications: e.target.checked}); }} /> - {hasWebmentions && { - setUserData?.({...user, mention_notifications: e.target.checked}); - }} - />} - { - setUserData?.({...user, free_member_signup_notification: e.target.checked}); - }} - /> - { - setUserData?.({...user, paid_subscription_started_notification: e.target.checked}); - }} - /> - { - setUserData?.({...user, paid_subscription_canceled_notification: e.target.checked}); - }} - /> - { - setUserData?.({...user, milestone_notifications: e.target.checked}); - }} - /> + {hasAdminAccess(currentUser) && <> + {hasWebmentions && { + setUserData?.({...user, mention_notifications: e.target.checked}); + }} + />} + { + setUserData?.({...user, free_member_signup_notification: e.target.checked}); + }} + /> + { + setUserData?.({...user, paid_subscription_started_notification: e.target.checked}); + }} + /> + { + setUserData?.({...user, paid_subscription_canceled_notification: e.target.checked}); + }} + /> + { + setUserData?.({...user, milestone_notifications: e.target.checked}); + }} + /> + } ); }; @@ -439,7 +444,7 @@ const StaffToken: React.FC = () => { const [token, setToken] = useState(''); const {mutateAsync: newApiKey} = genStaffToken(); const [copied, setCopied] = useState(false); - + const copyToClipboard = () => { navigator.clipboard.writeText(token); setCopied(true); @@ -496,6 +501,7 @@ const UserMenuTrigger = () => ( const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { const {updateRoute} = useRouting(); const {ownerUser} = useStaffUsers(); + const {currentUser} = useGlobalData(); const [userData, _setUserData] = useState(user); const [saveState, setSaveState] = useState<'' | 'unsaved' | 'saving' | 'saved'>(''); const [errors, setErrors] = useState<{ @@ -531,14 +537,22 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { disabled: !pinturaEnabled} ); + const navigateOnClose = useCallback(() => { + if (canAccessSettings(currentUser)) { + updateRoute('users'); + } else { + updateRoute({isExternal: true, route: 'dashboard'}); + } + }, [currentUser, updateRoute]); + useEffect(() => { if (saveState === 'saved') { setTimeout(() => { mainModal.remove(); - updateRoute('users'); + navigateOnClose(); }, 300); } - }, [mainModal, saveState, updateRoute]); + }, [mainModal, navigateOnClose, saveState, updateRoute]); const confirmSuspend = async (_user: User) => { if (_user.status === 'inactive' && _user.roles[0].name !== 'Contributor') { @@ -741,10 +755,12 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { return ( updateRoute('users')} + afterClose={navigateOnClose} + animate={canAccessSettings(currentUser)} + backDrop={canAccessSettings(currentUser)} dirty={saveState === 'unsaved'} okLabel={okLabel} - size='lg' + size={canAccessSettings(currentUser) ? 'lg' : 'full'} stickyFooter={true} testId='user-detail-modal' onOk={async () => { diff --git a/apps/admin-x-settings/src/components/settings/general/Users.tsx b/apps/admin-x-settings/src/components/settings/general/Users.tsx index b0b0e35ab4..0f43419461 100644 --- a/apps/admin-x-settings/src/components/settings/general/Users.tsx +++ b/apps/admin-x-settings/src/components/settings/general/Users.tsx @@ -6,12 +6,14 @@ import NoValueLabel from '../../../admin-x-ds/global/NoValueLabel'; import React, {useState} from 'react'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import TabView from '../../../admin-x-ds/global/TabView'; +import clsx from 'clsx'; import useRouting from '../../../hooks/useRouting'; import useStaffUsers from '../../../hooks/useStaffUsers'; -import {User} from '../../../api/users'; +import {User, hasAdminAccess, isContributorUser, isEditorUser} from '../../../api/users'; import {UserInvite, useAddInvite, useDeleteInvite} from '../../../api/invites'; import {generateAvatarColor, getInitials} from '../../../utils/helpers'; import {showToast} from '../../../admin-x-ds/global/Toast'; +import {useGlobalData} from '../../providers/GlobalDataProvider'; interface OwnerProps { user: User; @@ -31,9 +33,12 @@ interface InviteListProps { const Owner: React.FC = ({user}) => { const {updateRoute} = useRouting(); + const {currentUser} = useGlobalData(); const showDetailModal = () => { - updateRoute({route: `users/show/${user.slug}`}); + if (hasAdminAccess(currentUser)) { + updateRoute({route: `users/show/${user.slug}`}); + } }; if (!user) { @@ -41,10 +46,10 @@ const Owner: React.FC = ({user}) => { } return ( -
+
- {user.name} — Owner + {user.name} — Owner {hasAdminAccess(currentUser) && } {user.email}
@@ -53,6 +58,7 @@ const Owner: React.FC = ({user}) => { const UsersList: React.FC = ({users, groupname}) => { const {updateRoute} = useRouting(); + const {currentUser} = useGlobalData(); const showDetailModal = (user: User) => { updateRoute({route: `users/show/${user.slug}`}); @@ -73,11 +79,17 @@ const UsersList: React.FC = ({users, groupname}) => { if (user.status === 'inactive') { title = `${title} (Suspended)`; } + + const canEdit = hasAdminAccess(currentUser) || + (isEditorUser(currentUser) && isContributorUser(user)) || + currentUser.id === user.id; + return ( showDetailModal(user)}/>} + action={canEdit &&
- {{#if (gh-user-can-admin this.session.user)}} + {{#if (or (gh-user-can-admin this.session.user) this.session.user.isEditor)}} {{#if (feature "adminXSettings")}} {{svg-jar "settings"}} {{else}} {{svg-jar "settings"}} {{/if}} {{/if}} - {{#if this.session.user.isEditor}} - {{svg-jar "settings"}} - {{/if}}
{{svg-jar "sun"}}