0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Added static user detail popup in AdminX Settings

refs. https://github.com/TryGhost/Team/issues/3150
This commit is contained in:
Peter Zimon 2023-05-25 12:26:16 +02:00
parent f2277ded40
commit 9791b341be
15 changed files with 297 additions and 62 deletions

View file

@ -1,6 +1,6 @@
import React from 'react';
export type ButtonColor = 'clear' | 'black' | 'green' | 'red';
export type ButtonColor = 'clear' | 'grey' | 'black' | 'green' | 'red';
export interface IButton {
label: string;
@ -35,6 +35,9 @@ const Button: React.FC<IButton> = ({
case 'black':
styles += link ? ' text-black hover:text-grey-800' : ' bg-black text-white hover:bg-grey-900';
break;
case 'grey':
styles += link ? ' text-black hover:text-grey-800' : ' bg-grey-100 text-black hover:!bg-grey-300';
break;
case 'green':
styles += link ? ' text-green hover:text-green-400' : ' bg-green text-white hover:bg-green-400';
break;

View file

@ -6,6 +6,7 @@ type THeadingLevels = 1 | 2 | 3 | 4 | 5 | 6;
interface IHeading {
level?: THeadingLevels;
children?: React.ReactNode;
styles?: string;
/**
* Only available for Heading 6
@ -19,13 +20,13 @@ interface IHeading {
useLabelTag?: boolean;
}
const Heading: React.FC<IHeading> = ({level, children, grey, separator, useLabelTag, ...props}) => {
const Heading: React.FC<IHeading> = ({level, children, styles, grey, separator, useLabelTag, ...props}) => {
if (!level) {
level = 1;
}
const newElement = `${useLabelTag ? 'label' : `h${level}`}`;
let styles = (level === 6 || useLabelTag) ? (`block text-2xs font-semibold uppercase tracking-wide ${(grey && 'text-grey-700')}`) : '';
styles += (level === 6 || useLabelTag) ? (` block text-2xs font-semibold uppercase tracking-wide ${(grey && 'text-grey-700')}`) : ' ';
const Element = React.createElement(newElement, {className: styles, ...props}, children);

View file

@ -28,7 +28,7 @@ const ListItem: React.FC<ListItemProps> = ({id, title, detail, action, hideActio
{detail && <span className='text-xs text-grey-700'>{detail}</span>}
</div>
{action &&
<div className={`px-3 ${separator ? 'py-3' : 'py-2'} ${hideActions ? 'invisible group-hover:visible' : ''}`}>
<div className={`px-6 ${separator ? 'py-3' : 'py-2'} ${hideActions ? 'invisible group-hover:visible' : ''}`}>
{action}
</div>
}

View file

@ -10,6 +10,7 @@ export interface ModalProps {
size?: ModalSize;
title?: string;
okLabel?: string;
okColor?: string;
cancelLabel?: string;
leftButtonLabel?: string;
customFooter?: React.ReactNode;
@ -18,7 +19,7 @@ export interface ModalProps {
children?: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({size = 'md', title, okLabel, cancelLabel, customFooter, leftButtonLabel, onOk, onCancel, children}) => {
const Modal: React.FC<ModalProps> = ({size = 'md', title, okLabel, cancelLabel, customFooter, leftButtonLabel, onOk, okColor, onCancel, children}) => {
const modal = useModal();
let buttons: IButton[] = [];
@ -35,40 +36,40 @@ const Modal: React.FC<ModalProps> = ({size = 'md', title, okLabel, cancelLabel,
buttons.push({
key: 'ok-modal',
label: okLabel ? okLabel : 'OK',
color: 'black',
color: okColor ? okColor : 'black',
styles: 'min-w-[80px]',
onClick: onOk
});
}
let modalStyles = 'relative z-50 mx-auto flex flex-col justify-between bg-white p-8 shadow-xl w-full';
let modalStyles = 'relative z-50 mx-auto flex flex-col justify-between bg-white shadow-xl w-full';
let backdropStyles = 'fixed inset-0 h-[100vh] w-[100vw] overflow-y-scroll ';
switch (size) {
case 'sm':
modalStyles += ' max-w-[480px]';
modalStyles += ' max-w-[480px] p-8';
break;
case 'md':
modalStyles += ' max-w-[720px]';
modalStyles += ' max-w-[720px] p-8';
break;
case 'lg':
modalStyles += ' max-w-[940px]';
modalStyles += ' max-w-[940px] p-10';
break;
case 'xl':
modalStyles += ' max-w-[1180px] ';
modalStyles += ' max-w-[1180px] p-12';
break;
case 'full':
case 'bleed':
modalStyles += ' h-full';
modalStyles += ' h-full p-12';
break;
}
if (size !== 'bleed') {
modalStyles += ' rounded';
modalStyles += ' rounded-md overflow-hidden';
}
if (size !== 'bleed' && size !== 'full') {
@ -83,7 +84,7 @@ const Modal: React.FC<ModalProps> = ({size = 'md', title, okLabel, cancelLabel,
return (
<div className={backdropStyles} id='modal-backdrop'>
<div className='absolute inset-0 z-0 bg-[rgba(0,0,0,0.1)]' onClick={handleBackdropClick}></div>
<div className='fixed inset-0 z-0 bg-[rgba(0,0,0,0.1)]' onClick={handleBackdropClick}></div>
<section className={modalStyles}>
<div>
{title && <Heading level={4}>{title}</Heading>}

View file

@ -37,13 +37,13 @@ const Radio: React.FC<RadioProps> = ({id, title, options, onSelect, error, hint,
return (
<div>
<div className={`flex flex-col gap-1 ${separator && 'pb-2'}`}>
{title && <Heading grey={true} level={6}>{title}</Heading>}
<div className={`flex flex-col gap-2 ${separator && 'pb-2'}`}>
{title && <Heading level={6}>{title}</Heading>}
{options.map(option => (
<label key={option.value} className={`flex cursor-pointer items-start ${title && '-mb-1 mt-1'}`} htmlFor={option.value}>
<input
checked={selectedOption === option.value}
className="relative float-left mt-[3px] h-4 w-4 appearance-none rounded-full border-2 border-solid border-grey-300 after:absolute after:z-[1] after:block after:h-3 after:w-3 after:rounded-full after:content-[''] checked:border-green checked:after:absolute checked:after:left-1/2 checked:after:top-1/2 checked:after:h-[0.625rem] checked:after:w-[0.625rem] checked:after:rounded-full checked:after:border-green checked:after:bg-green checked:after:content-[''] checked:after:[transform:translate(-50%,-50%)] hover:cursor-pointer focus:shadow-none focus:outline-none focus:ring-0 checked:focus:border-green dark:border-grey-600 dark:checked:border-green dark:checked:after:border-green dark:checked:after:bg-green dark:checked:focus:border-green"
className="relative float-left mt-[3px] h-4 w-4 min-w-[16px] appearance-none rounded-full border-2 border-solid border-grey-300 after:absolute after:z-[1] after:block after:h-3 after:w-3 after:rounded-full after:content-[''] checked:border-green checked:after:absolute checked:after:left-1/2 checked:after:top-1/2 checked:after:h-[0.625rem] checked:after:w-[0.625rem] checked:after:rounded-full checked:after:border-green checked:after:bg-green checked:after:content-[''] checked:after:[transform:translate(-50%,-50%)] hover:cursor-pointer focus:shadow-none focus:outline-none focus:ring-0 checked:focus:border-green dark:border-grey-600 dark:checked:border-green dark:checked:after:border-green dark:checked:after:bg-green dark:checked:focus:border-green"
id={option.value}
name={id}
type='radio'

View file

@ -42,7 +42,7 @@ const Toggle: React.FC<ToggleProps> = ({id, size, direction, label, hint, separa
<div>
<div className={`flex items-start gap-2 ${direction === 'rtl' && 'justify-between'} ${separator && 'pb-2'}`}>
<input checked={checked}
className={`appearance-none rounded-full bg-grey-300 after:absolute after:z-[2] after:ml-0.5 after:mt-0.5 after:rounded-full after:border-none after:bg-white after:transition-[background-color_0.2s,transform_0.2s] after:content-[''] checked:bg-green checked:after:absolute checked:after:z-[2] checked:after:rounded-full checked:after:border-none checked:after:bg-white checked:after:transition-[background-color_0.2s,transform_0.2s] checked:after:content-[''] hover:cursor-pointer focus:outline-none focus:ring-0 focus:after:absolute focus:after:z-[1] focus:after:block focus:after:rounded-full focus:after:content-[''] checked:focus:border-green checked:focus:bg-green dark:bg-grey-600 dark:after:bg-grey-400 dark:checked:bg-green dark:checked:after:bg-green ${sizeStyles} ${direction === 'rtl' && ' order-2'}`}
className={`appearance-none rounded-full bg-grey-300 after:absolute after:ml-0.5 after:mt-0.5 after:rounded-full after:border-none after:bg-white after:transition-[background-color_0.2s,transform_0.2s] after:content-[''] checked:bg-green checked:after:absolute checked:after:rounded-full checked:after:border-none checked:after:bg-white checked:after:transition-[background-color_0.2s,transform_0.2s] checked:after:content-[''] hover:cursor-pointer dark:bg-grey-600 dark:after:bg-grey-400 dark:checked:bg-green dark:checked:after:bg-green ${sizeStyles} ${direction === 'rtl' && ' order-2'}`}
id={id}
role="switch"
type="checkbox"

View file

@ -77,4 +77,12 @@ export const CustomHeader: Story = {
description: SingleColumn.args?.description,
customHeader: customHeader
}
};
export const NoBorders: Story = {
args: {
title: SingleColumn.args?.title,
description: SingleColumn.args?.description,
children: twoColView
}
};

View file

@ -13,6 +13,14 @@ interface SettingGroupProps {
customHeader?: React.ReactNode;
customButtons?: React.ReactNode;
children?: React.ReactNode;
hideEditButton?: boolean;
alwaysShowSaveButton?: boolean;
/**
* Remove borders and paddings
*/
border?: boolean;
styles?: string;
/**
* Default buttons only appear if onStateChange is implemented
@ -30,6 +38,10 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
customHeader,
customButtons,
children,
hideEditButton,
alwaysShowSaveButton = true,
border = true,
styles,
onStateChange,
onSave,
onCancel
@ -50,30 +62,32 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
onStateChange?.('view');
};
let styles = '';
switch (state) {
case 'edit':
styles = 'border-grey-300';
styles += ' border-grey-300';
break;
case 'unsaved':
styles = 'border-green';
styles += ' border-green';
break;
default:
styles = 'border-grey-200';
styles += ' border-grey-200';
break;
}
const viewButtons = [
{
label: 'Edit',
key: 'edit',
color: 'green',
onClick: handleEdit
}
];
let viewButtons = [];
if (!hideEditButton) {
viewButtons.push(
{
label: 'Edit',
key: 'edit',
color: 'green',
onClick: handleEdit
}
);
}
let editButtons: IButton[] = [
{
@ -83,7 +97,7 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
}
];
if (state === 'unsaved') {
if (state === 'unsaved' || alwaysShowSaveButton) {
editButtons.push(
{
label: 'Save',
@ -95,7 +109,7 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
}
return (
<div className={`flex flex-col gap-6 rounded border p-5 md:p-7 ${styles}`} id={navid && navid}>
<div className={`flex flex-col gap-6 rounded ${border && 'border p-5 md:p-7'} ${styles}`} id={navid && navid}>
{customHeader ? customHeader :
<SettingGroupHeader description={description} title={title!}>
{customButtons ? customButtons :

View file

@ -16,7 +16,7 @@ interface ISettingGroupContent {
const SettingGroupContent: React.FC<ISettingGroupContent> = ({columns, values, children}) => {
let styles = 'flex flex-col gap-x-6 gap-y-7';
if (columns === 2) {
styles = 'grid grid-cols-2 gap-6';
styles = 'grid grid-cols-2 gap-x-8 gap-y-6';
}
return (

View file

@ -13,7 +13,7 @@ const SettingValue: React.FC<ISettingValue> = ({heading, value, hint, ...props})
return (
<div className='flex flex-col' {...props}>
{heading && <Heading grey={true} level={6}>{heading}</Heading>}
<div className={`-mt-0.5 flex items-center ${heading && `min-h-[40px]`}`}>{value}</div>
<div className={`-mt-0.5 flex items-center ${heading && `min-h-[36px]`}`}>{value}</div>
{hint && <p className='mt-0.5 text-xs'>{hint}</p>}
</div>
);

View file

@ -1,20 +0,0 @@
import Modal from '../../admin-x-ds/global/Modal';
import NiceModal from '@ebay/nice-modal-react';
const UserDetailModal = NiceModal.create(() => {
return (
<Modal
size='lg'
title='User details'
onOk={() => {
alert('Clicked OK');
}}
>
<div className='py-4'>
Some user details
</div>
</Modal>
);
});
export default UserDetailModal;

View file

@ -1,12 +1,12 @@
import Button from '../../../admin-x-ds/global/Button';
import InviteUserModal from '../../modals/InviteUserModal';
import InviteUserModal from './modals/InviteUserModal';
import List from '../../../admin-x-ds/global/List';
import ListItem from '../../../admin-x-ds/global/ListItem';
import NiceModal from '@ebay/nice-modal-react';
import React from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import TabView from '../../../admin-x-ds/global/TabView';
import UserDetailModal from '../../modals/UserDetailModal';
import UserDetailModal from './modals/UserDetailModal';
const Users: React.FC = () => {
const showInviteModal = () => {

View file

@ -1,4 +1,4 @@
import Modal from '../../admin-x-ds/global/Modal';
import Modal from '../../../../admin-x-ds/global/Modal';
import NiceModal from '@ebay/nice-modal-react';
const InviteUserModal = NiceModal.create(() => {

View file

@ -0,0 +1,227 @@
import Button from '../../../../admin-x-ds/global/Button';
import Heading from '../../../../admin-x-ds/global/Heading';
import Modal from '../../../../admin-x-ds/global/Modal';
import NiceModal from '@ebay/nice-modal-react';
import Radio from '../../../../admin-x-ds/global/Radio';
import React, {useState} from 'react';
import SettingGroup from '../../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../../admin-x-ds/global/TextField';
import Toggle from '../../../../admin-x-ds/global/Toggle';
interface CustomHeadingProps {
children?: string;
}
const CustomHeader: React.FC<CustomHeadingProps> = ({children}) => {
return (
<Heading level={4} separator={true}>{children}</Heading>
);
};
const Basic: React.FC = () => {
const inputs = (
<SettingGroupContent>
<TextField
hint="Use real name so people can recognize you"
title="Full name"
value="Martin Culhane"
/>
<TextField
title="Email"
value="martin@culhane.com"
/>
<Radio
defaultSelectedOption="administrator"
id='role'
options={[
{
hint: 'Can create and edit their own posts, but cannot publish. An Editor needs to approve and publish for them.',
label: 'Contributor',
value: 'contributor'
},
{
hint: 'A trusted user who can create, edit and publish their own posts, but cant modify others.',
label: 'Author',
value: 'author'
},
{
hint: 'Can invite and manage other Authors and Contributors, as well as edit and publish any posts on the site.',
label: 'Editor',
value: 'editor'
},
{
hint: 'Trusted staff user who should be able to manage all content and users, as well as site settings and options.',
label: 'Administrator',
value: 'administrator'
}
]}
title="Role"
onSelect={() => {}}
/>
</SettingGroupContent>
);
return (
<SettingGroup
border={false}
customHeader={<CustomHeader>Basic info</CustomHeader>}
title='Basic'
>
{inputs}
</SettingGroup>
);
};
const Details: React.FC = () => {
const inputs = (
<SettingGroupContent>
<TextField
hint="https://example.com/author"
title="Slug"
/>
<TextField
title="Location"
/>
<TextField
title="Website"
/>
<TextField
title="Facebook profile"
/>
<TextField
title="Twitter profile"
/>
<TextField
hint="Recommended: 200 characters."
title="Bio"
/>
</SettingGroupContent>
);
return (
<SettingGroup
border={false}
customHeader={<CustomHeader>Details</CustomHeader>}
title='Details'
>
{inputs}
</SettingGroup>
);
};
const EmailNotifications: React.FC = () => {
const inputs = (
<SettingGroupContent>
<Toggle
direction='rtl'
hint='Every time a member comments on one of your posts'
id='comments'
label='Comments'
/>
<Toggle
direction='rtl'
hint='Every time a new free member signs up'
id='new-signups'
label='New signups'
/>
<Toggle
direction='rtl'
hint='Every time a member starts a new paid subscription'
id='new-paid-members'
label='New paid members'
/>
<Toggle
direction='rtl'
hint='Every time a member cancels their paid subscription'
id='paid-member-cancellations'
label='Paid member cancellations'
/>
<Toggle
direction='rtl'
hint='Occasional summaries of your audience & revenue growth'
id='milestones'
label='Milestones'
/>
</SettingGroupContent>
);
return (
<SettingGroup
border={false}
customHeader={<CustomHeader>Email notifications</CustomHeader>}
title='Email notifications'
>
{inputs}
</SettingGroup>
);
};
const Password: React.FC = () => {
const [editPassword, setEditPassword] = useState(false);
const showPasswordInputs = () => {
setEditPassword(true);
};
const view = (
<Button
color='grey'
label='Change password'
onClick={showPasswordInputs}
/>
);
const form = (
<>
<TextField
title="New password"
value=''
/>
<TextField
title="Verify password"
value=''
/>
</>
);
return (
<SettingGroup
border={false}
customHeader={<CustomHeader>Password</CustomHeader>}
title='Password'
>
{editPassword ? form : view}
</SettingGroup>
);
};
const UserDetailModal = NiceModal.create(() => {
return (
<Modal
okColor='green'
okLabel='Save'
size='xl'
onOk={() => {
alert('Clicked OK');
}}
>
<div>
<div className='-mx-12 -mt-12 bg-gradient-to-tr from-grey-900 to-black p-12 text-white'>
<div className='mt-60'>
<Heading styles='text-white'>Martin Culhane</Heading>
<span className='text-md font-semibold'>Administrator</span>
</div>
</div>
<div className='mt-10 grid grid-cols-2 gap-x-12 gap-y-20 pb-10'>
<Basic />
<Details />
<EmailNotifications />
<Password />
</div>
</div>
</Modal>
);
});
export default UserDetailModal;

View file

@ -26,8 +26,8 @@ const Analytics: React.FC = () => {
const inputs = (
<SettingGroupContent columns={2}>
<Toggle
// direction='rtl'
checked={trackEmailOpens}
direction='rtl'
hint='Record when a member opens an email'
id='newsletter-opens'
label='Newsletter opens'
@ -36,8 +36,8 @@ const Analytics: React.FC = () => {
}}
/>
<Toggle
// direction='rtl'
checked={trackEmailClicks}
direction='rtl'
hint='Record when a member clicks on any link in an email'
id='newsletter-clicks'
label='Newsletter clicks'
@ -46,8 +46,8 @@ const Analytics: React.FC = () => {
}}
/>
<Toggle
// direction='rtl'
checked={trackMemberSources}
direction='rtl'
hint='Track the traffic sources and posts that drive the most member growth'
id='member-sources'
label='Member sources'
@ -56,8 +56,8 @@ const Analytics: React.FC = () => {
}}
/>
<Toggle
// direction='rtl'
checked={outboundLinkTagging}
direction='rtl'
hint='Make it easier for other sites to track the traffic you send them in their analytics'
id='outbound-links'
label='Outbound link tagging'
@ -71,6 +71,7 @@ const Analytics: React.FC = () => {
return (
<SettingGroup
description='Decide what data you collect from your members'
hideEditButton={true}
state={currentState}
title='Analytics'
onCancel={handleCancel}