0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Updated installed theme list

refs. https://github.com/TryGhost/Team/issues/3432
This commit is contained in:
Peter Zimon 2023-06-16 15:44:02 +02:00
parent dfd69f9cf7
commit 78aa31973c
21 changed files with 151 additions and 106 deletions

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="6" cy="12" r="1.5" fill="currentColor"/>
<circle cx="12" cy="12" r="1.5" fill="currentColor"/>
<path d="M19.5 12C19.5 12.8284 18.8284 13.5 18 13.5C17.1716 13.5 16.5 12.8284 16.5 12C16.5 11.1716 17.1716 10.5 18 10.5C18.8284 10.5 19.5 11.1716 19.5 12Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 388 B

View file

@ -1 +0,0 @@
<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M7.500 5.000 A1.250 1.250 0 1 0 10.000 5.000 A1.250 1.250 0 1 0 7.500 5.000 Z" fill="currentColor" stroke="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="0"></path><path d="M3.750 5.000 A1.250 1.250 0 1 0 6.250 5.000 A1.250 1.250 0 1 0 3.750 5.000 Z" fill="currentColor" stroke="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="0"></path><path d="M0.000 5.000 A1.250 1.250 0 1 0 2.500 5.000 A1.250 1.250 0 1 0 0.000 5.000 Z" fill="currentColor" stroke="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="0"></path></svg>

Before

Width:  |  Height:  |  Size: 666 B

View file

@ -61,7 +61,7 @@ export const LinkButton: Story = {
export const Icon: Story = {
args: {
icon: 'menu-horizontal',
icon: 'ellipsis',
color: 'green',
iconColorClass: 'text-white'
}
@ -70,7 +70,7 @@ export const Icon: Story = {
export const IconSmall: Story = {
args: {
size: 'sm',
icon: 'menu-horizontal',
icon: 'ellipsis',
color: 'green',
iconColorClass: 'text-white'
}

View file

@ -4,7 +4,7 @@ import React from 'react';
export type ButtonColor = 'clear' | 'grey' | 'black' | 'green' | 'red' | 'white';
export type ButtonSize = 'sm' | 'md';
export interface IButton {
export interface ButtonProps {
size?: ButtonSize;
label?: React.ReactNode;
icon?: string;
@ -18,7 +18,7 @@ export interface IButton {
onClick?: () => void;
}
const Button: React.FC<IButton> = ({
const Button: React.FC<ButtonProps> = ({
size = 'md',
label = '',
icon = '',

View file

@ -1,10 +1,10 @@
import Button from './Button';
import React from 'react';
import {IButton} from './Button';
import {ButtonProps} from './Button';
interface ButtonGroupProps {
buttons: Array<IButton>;
buttons: Array<ButtonProps>;
link?: boolean;
className?: string;
}

View file

@ -7,8 +7,7 @@ import ListItem from './ListItem';
const meta = {
title: 'Global / List',
component: List,
tags: ['autodocs'],
decorators: [(_story: any) => (<div style={{maxWidth: '600px'}}>{_story()}</div>)]
tags: ['autodocs']
} satisfies Meta<typeof List>;
export default meta;
@ -29,5 +28,14 @@ export const Default: Story = {
title: 'This is a list',
children: listItems,
hint: 'And here is a hint for the whole list'
},
decorators: [(_story: any) => (<div style={{maxWidth: '600px'}}>{_story()}</div>)]
};
export const PageLevel: Story = {
args: {
pageTitle: 'A page with a list',
children: listItems,
hint: 'And here is a hint for the whole list'
}
};

View file

@ -2,37 +2,55 @@ import Heading from './Heading';
import Hint from './Hint';
import React from 'react';
import Separator from './Separator';
import clsx from 'clsx';
interface ListProps {
/**
* If the list is the primary content on a page (e.g. Members list) then you can set a pagetitle to be consistent
*/
pageTitle?: string;
/**
* When you use the list in a block and it's not the primary content of the page then you can set a title to the list
*/
title?: React.ReactNode;
titleSeparator?: boolean;
children?: React.ReactNode;
hint?: React.ReactNode;
hintSeparator?: boolean;
borderTop?: boolean;
}
const List: React.FC<ListProps> = ({title, titleSeparator, children, hint, hintSeparator}) => {
const List: React.FC<ListProps> = ({title, titleSeparator, children, hint, hintSeparator, borderTop, pageTitle}) => {
titleSeparator = (titleSeparator === undefined) ? true : titleSeparator;
hintSeparator = (hintSeparator === undefined) ? true : hintSeparator;
const listClasses = clsx(
(borderTop || pageTitle) && 'border-t border-grey-300',
pageTitle && 'mt-14'
);
return (
<section>
{title &&
<div className='flex flex-col gap-1'>
<Heading grey={true} level={6}>{title}</Heading>
{titleSeparator && <Separator />}
<>
{pageTitle && <Heading>{pageTitle}</Heading>}
<section className={listClasses}>
{(!pageTitle && title) &&
<div className='flex flex-col gap-1'>
<Heading grey={true} level={6}>{title}</Heading>
{titleSeparator && <Separator />}
</div>
}
<div className='flex flex-col'>
{children}
</div>
}
<div className='flex flex-col'>
{children}
</div>
{hint &&
<>
{hintSeparator && <Separator />}
<Hint>{hint}</Hint>
</>
}
</section>
{hint &&
<>
{hintSeparator && <Separator />}
<Hint>{hint}</Hint>
</>
}
</section>
</>
);
};

View file

@ -1,4 +1,5 @@
import React from 'react';
import clsx from 'clsx';
interface ListItemProps {
id: string;
@ -7,32 +8,41 @@ interface ListItemProps {
action?: React.ReactNode;
hideActions?: boolean;
avatar?: React.ReactNode;
className?: string;
/**
* Hidden for the last item in the list
*/
separator?: boolean;
bgOnHover?: boolean;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
const ListItem: React.FC<ListItemProps> = ({id, title, detail, action, hideActions, avatar, separator, onClick}) => {
const ListItem: React.FC<ListItemProps> = ({id, title, detail, action, hideActions, avatar, className, separator, bgOnHover = true, onClick}) => {
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
onClick?.(e);
};
separator = (separator === undefined) ? true : separator;
const listItemClasses = clsx(
'group flex items-center justify-between',
bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50',
separator ? 'border-b border-grey-100 last-of-type:border-none hover:border-grey-200' : 'border-b border-transparent hover:border-grey-200',
className
);
return (
<div className={`group flex items-center justify-between hover:bg-gradient-to-r hover:from-white hover:to-grey-50 ${separator ? 'border-b border-grey-100 last-of-type:border-none' : ''}`}>
<div className={listItemClasses}>
<div className={`flex grow items-center gap-3 ${onClick && 'cursor-pointer'}`} onClick={handleClick}>
{avatar && avatar}
<div className={`flex grow flex-col pr-6 ${separator ? 'py-3' : 'py-2'}`} id={id}>
<div className={`flex grow flex-col py-3 pr-6`} id={id}>
<span>{title}</span>
{detail && <span className='text-xs text-grey-700'>{detail}</span>}
</div>
</div>
{action &&
<div className={`px-6 ${separator ? 'py-3' : 'py-2'} ${hideActions ? 'invisible group-hover:visible' : ''}`}>
{action &&
<div className={`px-6 py-3 ${hideActions ? 'invisible group-hover:visible' : ''}`}>
{action}
</div>
}

View file

@ -1,4 +1,6 @@
import Button, {ButtonProps, ButtonSize} from './Button';
import React, {useState} from 'react';
import clsx from 'clsx';
export type MenuItem = {
id: string,
@ -10,53 +12,49 @@ type MenuPosition = 'left' | 'right';
interface MenuProps {
trigger?: React.ReactNode;
triggerButtonProps?: ButtonProps;
triggerSize?: ButtonSize;
items: MenuItem[];
position?: MenuPosition;
className?: string;
}
const Menu: React.FC<MenuProps> = ({trigger, items, position, className}) => {
const Menu: React.FC<MenuProps> = ({trigger, triggerButtonProps, items, position, className}) => {
const [menuOpen, setMenuOpen] = useState(false);
let menuListStyles = 'absolute z-40 mt-2 min-w-[160px] w-max origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none';
const toggleMenu = () => {
setMenuOpen(!menuOpen);
};
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
setMenuOpen(false);
}
};
switch (position) {
case 'left':
menuListStyles += ' right-0 ';
break;
case 'right':
menuListStyles += ' left-0 ';
break;
default:
menuListStyles += ' left-0 ';
break;
if (!trigger) {
trigger = <Button icon='ellipsis' {...triggerButtonProps} />;
}
menuListStyles += menuOpen ? 'block' : 'hidden';
const menuClasses = clsx(
'absolute z-40 mt-2 w-max min-w-[160px] origin-top-right rounded bg-white shadow-md ring-1 ring-[rgba(0,0,0,0.01)] focus:outline-none',
position === 'left' && 'right-0',
(position === 'right' || !position) && 'left-0',
menuOpen ? 'block' : 'hidden'
);
return (
<div className={`relative inline-block ${className}`}>
<div className={`fixed inset-0 z-40 ${menuOpen ? 'block' : 'hidden'}`} onClick={handleBackdropClick}></div>
{/* Menu Trigger */}
<div className='relative z-50' onClick={toggleMenu}>
<div className='relative z-30' onClick={toggleMenu}>
{trigger}
</div>
{/* Menu List */}
<div aria-labelledby="menu-button" aria-orientation="vertical" className={menuListStyles} role="menu">
<div className="py-1" role="none">
<div aria-labelledby="menu-button" aria-orientation="vertical" className={menuClasses} role="menu">
<div className="flex flex-col justify-stretch py-1" role="none">
{items.map(item => (
<button key={item.id} className="block w-full cursor-pointer px-4 py-2 text-left text-sm text-grey-900 hover:bg-grey-100" type="button" onClick={item.onClick}>{item.label}</button>
<button key={item.id} className="mx-1 block cursor-pointer rounded-[2.5px] px-4 py-1.5 text-left text-sm hover:bg-grey-100" type="button" onClick={item.onClick}>{item.label}</button>
))}
</div>
</div>

View file

@ -1,11 +1,11 @@
import React from 'react';
interface SeparatorProps {
color?: string;
className?: string;
}
const Separator: React.FC<SeparatorProps> = ({color}) => {
return <hr className={`border-${color ? color : 'grey-300'}`} />;
const Separator: React.FC<SeparatorProps> = ({className = 'border-grey-300'}) => {
return <hr className={className} />;
};
export default Separator;

View file

@ -47,7 +47,7 @@ const Checkbox: React.FC<CheckboxProps> = ({id, title, label, value, onChange, e
</div>
</label>
</div>
{(separator || error) && <Separator color={error ? 'red' : ''} />}
{(separator || error) && <Separator className={error ? 'border-red' : ''} />}
</div>
);
};

View file

@ -12,9 +12,10 @@ export interface FileUploadProps {
className?: string;
onUpload: (file: File) => void;
style?: {}
unstyled?: boolean;
}
const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, style, ...props}) => {
const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, style, unstyled = false, ...props}) => {
const [fileKey, setFileKey] = useState<number>(Date.now());
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
@ -29,7 +30,7 @@ const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, style, .
<label htmlFor={id} style={style} {...props}>
<input key={fileKey} id={id} type="file" hidden onChange={handleFileChange} />
{(typeof children === 'string') ?
<div className='inline-flex h-[34px] cursor-pointer items-center justify-center rounded px-4 text-sm font-semibold hover:bg-grey-100'>
<div className={!unstyled ? `inline-flex h-[34px] cursor-pointer items-center justify-center rounded px-4 text-sm font-semibold hover:bg-grey-100` : ''}>
{children}
</div>
:

View file

@ -116,7 +116,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
width: (unstyled ? '' : width),
height: (unstyled ? '' : height)
}
} onUpload={onUpload}>
} unstyled={unstyled} onUpload={onUpload}>
{children}
</FileUpload>
);

View file

@ -58,7 +58,7 @@ const Radio: React.FC<RadioProps> = ({id, title, options, onSelect, error, hint,
))}
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
</div>
{(separator || error) && <Separator color={error ? 'red' : ''} />}
{(separator || error) && <Separator className={error ? 'border-red' : ''} />}
</div>
);
};

View file

@ -54,7 +54,7 @@ const Toggle: React.FC<ToggleProps> = ({id, size, direction, label, hint, separa
</label>
}
</div>
{(separator || error) && <Separator color={error ? 'red' : ''} />}
{(separator || error) && <Separator className={error ? 'border-red' : ''} />}
</div>
);
};

View file

@ -1,4 +1,4 @@
import Button, {IButton} from '../Button';
import Button, {ButtonProps} from '../Button';
import ButtonGroup from '../ButtonGroup';
import ConfirmationModal from './ConfirmationModal';
import Heading from '../Heading';
@ -66,7 +66,7 @@ const Modal: React.FC<ModalProps> = ({
}) => {
const modal = useModal();
let buttons: IButton[] = [];
let buttons: ButtonProps[] = [];
const removeModal = () => {
if (!dirty) {

View file

@ -7,7 +7,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import Select, {SelectOption} from '../form/Select';
import TabView, {Tab} from '../TabView';
import {IButton} from '../Button';
import {ButtonProps} from '../Button';
export interface PreviewModalProps {
testId?: string;
@ -61,7 +61,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
onSelectMobileView
}) => {
const modal = useModal();
let buttons: IButton[] = [];
let buttons: ButtonProps[] = [];
const [view, setView] = useState('desktop');

View file

@ -1,7 +1,7 @@
import ButtonGroup from '../global/ButtonGroup';
import React from 'react';
import SettingGroupHeader from './SettingGroupHeader';
import {IButton} from '../global/Button';
import {ButtonProps} from '../global/Button';
import {SaveState} from '../../hooks/useForm';
interface SettingGroupProps {
@ -96,7 +96,7 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
);
}
let editButtons: IButton[] = [
let editButtons: ButtonProps[] = [
{
label: 'Cancel',
key: 'cancel',

View file

@ -401,7 +401,7 @@ interface UserDetailModalProps {
const UserMenuTrigger = () => (
<div className='flex h-8 cursor-pointer items-center justify-center rounded bg-[rgba(0,0,0,0.75)] px-3 opacity-80 hover:opacity-100'>
<Icon colorClass='text-white' name='menu-horizontal' size='sm' />
<Icon colorClass='text-white' name='ellipsis' size='md' />
</div>
);

View file

@ -76,9 +76,11 @@ const UsersList: React.FC<UsersListProps> = ({users, updateUser}) => {
key={user.id}
action={<Button color='green' label='Edit' link={true} onClick={() => showDetailModal(user)}/>}
avatar={(<Avatar bgColor={generateAvatarColor((user.name ? user.name : user.email))} image={user.profile_image} label={getInitials(user.name)} labelColor='white' />)}
className='min-h-[64px]'
detail={user.email}
hideActions={true}
id={`list-item-${user.id}`}
separator={false}
title={title}
onClick={() => showDetailModal(user)} />
);
@ -160,9 +162,11 @@ const InvitesUserList: React.FC<InviteListProps> = ({users}) => {
key={user.id}
action={<UserInviteActions invite={user} />}
avatar={(<Avatar bgColor={generateAvatarColor((user.email))} image={''} label={''} labelColor='white' />)}
className='min-h-[64px]'
detail={user.role}
hideActions={true}
id={`list-item-${user.id}`}
separator={false}
title={user.email}
onClick={() => {
// do nothing

View file

@ -1,8 +1,8 @@
import Button from '../../../../admin-x-ds/global/Button';
import Button, {ButtonProps} from '../../../../admin-x-ds/global/Button';
import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal';
import Heading from '../../../../admin-x-ds/global/Heading';
import List from '../../../../admin-x-ds/global/List';
import ListItem from '../../../../admin-x-ds/global/ListItem';
import Menu from '../../../../admin-x-ds/global/Menu';
import NiceModal from '@ebay/nice-modal-react';
import React from 'react';
import {Theme} from '../../../../types/api';
@ -21,17 +21,23 @@ interface ThemeSettingProps {
setThemes: (themes: Theme[]) => void;
}
function getThemeLabel(theme: Theme): string {
let label = theme.package?.name || theme.name;
function getThemeLabel(theme: Theme): React.ReactNode {
let label: React.ReactNode = theme.package?.name || theme.name;
if (isDefaultTheme(theme)) {
label += ' (default)';
} else {
label += ` (${theme.name})`;
} else if (theme.package?.name !== theme.name) {
label =
<>
{label} <span className='text-grey-600'>({theme.name})</span>
</>;
}
if (isActiveTheme(theme)) {
label += ' (active)';
label =
<span className='font-bold'>
{label} &mdash; <span className='text-green'> Active</span>
</span>;
}
return label;
@ -100,17 +106,7 @@ const ThemeActions: React.FC<ThemeActionProps> = ({
};
let actions = [];
if (isDeletableTheme(theme)) {
actions.push(
<Button
key='delete'
color='red'
label={'Delete'}
link={true}
onClick={handleDelete}
/>
);
}
if (!isActiveTheme(theme)) {
actions.push(
<Button
@ -124,20 +120,30 @@ const ThemeActions: React.FC<ThemeActionProps> = ({
);
}
actions.push(
<Button
key='download'
className='ml-2'
color='green'
label={'Download'}
link={true}
onClick={handleDownload}
/>
);
let menuItems = [
{
id: 'download',
label: 'Download',
onClick: handleDownload
}
];
if (isDeletableTheme(theme)) {
menuItems.push({
id: 'delete',
label: 'Delete',
onClick: handleDelete
});
}
const buttonProps: ButtonProps = {
size: 'sm'
};
return (
<div className='flex gap-2'>
<div className='-mr-3 flex items-center gap-4'>
{actions}
<Menu items={menuItems} position='left' triggerButtonProps={buttonProps} />
</div>
);
};
@ -147,9 +153,7 @@ const ThemeList:React.FC<ThemeSettingProps> = ({
setThemes
}) => {
return (
<List
title='Installed themes'
>
<List pageTitle='Installed themes'>
{themes.map((theme) => {
const label = getThemeLabel(theme);
const detail = getThemeVersion(theme);
@ -166,6 +170,7 @@ const ThemeList:React.FC<ThemeSettingProps> = ({
}
detail={detail}
id={`theme-${theme.name}`}
separator={false}
title={label}
/>
);
@ -180,13 +185,10 @@ const AdvancedThemeSettings: React.FC<ThemeSettingProps> = ({
}) => {
return (
<div className='p-[8vmin] pt-5'>
<Heading>Installed themes</Heading>
<div className='mt-5'>
<ThemeList
setThemes={setThemes}
themes={themes}
/>
</div>
<ThemeList
setThemes={setThemes}
themes={themes}
/>
</div>
);
};