mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-01 02:41:39 -05:00
Added sorting to newsletters list (#18963)
refs https://github.com/TryGhost/Product/issues/4128 --- <!-- Leave the line below if you'd like GitHub Copilot to generate a summary from your commit --> <!-- copilot:summary --> ### <samp>🤖 Generated by Copilot at 9e300ac</samp> This pull request enhances the `SortableList` component and its related hooks in the design system, and applies them to the newsletters and tiers settings in the admin app. It also exports and imports some types and constants to improve code reusability and consistency. --------- Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
This commit is contained in:
parent
5eb4e3330c
commit
7e5cad8e11
10 changed files with 229 additions and 56 deletions
|
@ -1,14 +1,15 @@
|
|||
import {DndContext, DragOverlay, DraggableAttributes, closestCenter} from '@dnd-kit/core';
|
||||
import {SortableContext, useSortable, verticalListSortingStrategy} from '@dnd-kit/sortable';
|
||||
import {CSS} from '@dnd-kit/utilities';
|
||||
import clsx from 'clsx';
|
||||
import React, {ElementType, HTMLProps, ReactNode, useState} from 'react';
|
||||
import Heading from './Heading';
|
||||
import Hint from './Hint';
|
||||
import Icon from './Icon';
|
||||
import React, {HTMLProps, ReactNode, useState} from 'react';
|
||||
import Separator from './Separator';
|
||||
import clsx from 'clsx';
|
||||
import {CSS} from '@dnd-kit/utilities';
|
||||
import {DndContext, DragOverlay, DraggableAttributes, closestCenter} from '@dnd-kit/core';
|
||||
import {SortableContext, useSortable, verticalListSortingStrategy} from '@dnd-kit/sortable';
|
||||
|
||||
export interface SortableItemContainerProps {
|
||||
id: string;
|
||||
setRef?: (element: HTMLElement | null) => void;
|
||||
isDragging: boolean;
|
||||
dragHandleAttributes?: DraggableAttributes;
|
||||
|
@ -21,15 +22,32 @@ export interface SortableItemContainerProps {
|
|||
separator?: boolean;
|
||||
}
|
||||
|
||||
export type DragIndicatorProps = Pick<SortableItemContainerProps, 'isDragging' | 'dragHandleAttributes' | 'dragHandleListeners' | 'dragHandleClass'> & React.HTMLAttributes<HTMLButtonElement>
|
||||
|
||||
export const DragIndicator: React.FC<DragIndicatorProps> = ({isDragging, dragHandleAttributes, dragHandleListeners, dragHandleClass, className, ...props}) => (
|
||||
<button
|
||||
className={clsx(
|
||||
'opacity-50 group-hover:opacity-100',
|
||||
isDragging ? 'cursor-grabbing' : 'cursor-grab',
|
||||
dragHandleClass,
|
||||
className
|
||||
)}
|
||||
type='button'
|
||||
{...dragHandleAttributes}
|
||||
{...dragHandleListeners}
|
||||
{...props}
|
||||
>
|
||||
<Icon colorClass='text-grey-500' name='hamburger' size='sm' />
|
||||
</button>
|
||||
);
|
||||
|
||||
const DefaultContainer: React.FC<SortableItemContainerProps> = ({
|
||||
setRef,
|
||||
isDragging,
|
||||
dragHandleAttributes,
|
||||
dragHandleListeners,
|
||||
dragHandleClass,
|
||||
style,
|
||||
separator,
|
||||
children
|
||||
children,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
ref={setRef}
|
||||
|
@ -40,18 +58,7 @@ const DefaultContainer: React.FC<SortableItemContainerProps> = ({
|
|||
)}
|
||||
style={style}
|
||||
>
|
||||
<button
|
||||
className={clsx(
|
||||
'h-7 opacity-50 group-hover:opacity-100',
|
||||
isDragging ? 'cursor-grabbing' : 'cursor-grab',
|
||||
dragHandleClass
|
||||
)}
|
||||
type='button'
|
||||
{...dragHandleAttributes}
|
||||
{...dragHandleListeners}
|
||||
>
|
||||
<Icon colorClass='text-grey-500' name='hamburger' size='sm' />
|
||||
</button>
|
||||
<DragIndicator className='h-7' isDragging={isDragging} {...props} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -77,6 +84,7 @@ const SortableItem: React.FC<{
|
|||
};
|
||||
|
||||
return container({
|
||||
id,
|
||||
setRef: setNodeRef,
|
||||
isDragging: false,
|
||||
separator: separator,
|
||||
|
@ -98,6 +106,8 @@ export interface SortableListProps<Item extends {id: string}> extends HTMLProps<
|
|||
onMove: (id: string, overId: string) => void;
|
||||
renderItem: (item: Item) => ReactNode;
|
||||
container?: (props: SortableItemContainerProps) => ReactNode;
|
||||
wrapper?: ElementType;
|
||||
dragOverlayWrapper?: keyof JSX.IntrinsicElements;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -113,6 +123,8 @@ const SortableList = <Item extends {id: string}>({
|
|||
onMove,
|
||||
renderItem,
|
||||
container = props => <DefaultContainer {...props} />,
|
||||
wrapper: Wrapper = React.Fragment,
|
||||
dragOverlayWrapper,
|
||||
...props
|
||||
}: SortableListProps<Item>) => {
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||
|
@ -130,16 +142,19 @@ const SortableList = <Item extends {id: string}>({
|
|||
onDragEnd={event => onMove(event.active.id as string, event.over?.id as string)}
|
||||
onDragStart={event => setDraggingId(event.active.id as string)}
|
||||
>
|
||||
<SortableContext
|
||||
items={items}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{items.map(item => (
|
||||
<SortableItem key={item.id} container={container} dragHandleClass={dragHandleClass} id={item.id} separator={itemSeparator}>{renderItem(item)}</SortableItem>
|
||||
))}
|
||||
</SortableContext>
|
||||
<DragOverlay>
|
||||
<Wrapper>
|
||||
<SortableContext
|
||||
items={items}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{items.map(item => (
|
||||
<SortableItem key={item.id} container={container} dragHandleClass={dragHandleClass} id={item.id} separator={itemSeparator}>{renderItem(item)}</SortableItem>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Wrapper>
|
||||
<DragOverlay wrapperElement={dragOverlayWrapper}>
|
||||
{draggingId ? container({
|
||||
id: draggingId,
|
||||
isDragging: true,
|
||||
children: renderItem(items.find(({id}) => id === draggingId)!)
|
||||
}) : null}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import {ReactNode} from 'react';
|
||||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
import {ReactNode} from 'react';
|
||||
|
||||
import * as TableRowStories from './TableRow.stories';
|
||||
import {useSortableIndexedList} from '..';
|
||||
import SortableList, {DragIndicator, SortableItemContainerProps} from './SortableList';
|
||||
import Table from './Table';
|
||||
import TableCell from './TableCell';
|
||||
import TableHead from './TableHead';
|
||||
import TableRow from './TableRow';
|
||||
import * as TableRowStories from './TableRow.stories';
|
||||
|
||||
const meta = {
|
||||
title: 'Global / Table',
|
||||
|
@ -95,4 +97,58 @@ export const Loading: Story = {
|
|||
hint: 'This is a hint',
|
||||
hintSeparator: true
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Components for Sortable example
|
||||
|
||||
const SortableContainer: React.FC<Partial<SortableItemContainerProps>> = ({setRef, isDragging, style, children, ...props}) => {
|
||||
const container = (
|
||||
<TableRow ref={setRef} className={isDragging ? 'opacity-75' : ''} style={style} hideActions>
|
||||
{(props.dragHandleAttributes || isDragging) && <TableCell className='w-10'>
|
||||
<DragIndicator className='h-5' isDragging={isDragging || false} {...props} />
|
||||
</TableCell>}
|
||||
{children}
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
if (isDragging) {
|
||||
return <Table>{container}</Table>;
|
||||
} else {
|
||||
return container;
|
||||
}
|
||||
};
|
||||
|
||||
const SortableItem: React.FC<{id: string; item: string}> = ({id, item}) => {
|
||||
return (
|
||||
<>
|
||||
<TableCell className='whitespace-nowrap'>{id}.</TableCell>
|
||||
<TableCell className='w-full'>{item}</TableCell>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SortableTable = () => {
|
||||
const list = useSortableIndexedList({
|
||||
items: ['First', 'Second'],
|
||||
setItems: () => {},
|
||||
blank: '',
|
||||
canAddNewItem: () => false
|
||||
});
|
||||
|
||||
return <SortableList
|
||||
container={props => <SortableContainer {...props} />}
|
||||
items={list.items}
|
||||
renderItem={item => <SortableItem {...item} />}
|
||||
wrapper={Table}
|
||||
onMove={list.moveItem}
|
||||
/>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Example of combining Table and SortableList to create a sortable table.
|
||||
* This is a little complex as each type of container/item needs to be overridden
|
||||
* to end up with the correct table->tbody->tr->td structure.
|
||||
*/
|
||||
export const Sortable: Story = {
|
||||
render: () => <SortableTable />
|
||||
};
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import React, {forwardRef} from 'react';
|
||||
|
||||
export interface TableRowProps {
|
||||
id?: string;
|
||||
action?: React.ReactNode;
|
||||
hideActions?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
testId?: string;
|
||||
|
||||
/**
|
||||
|
@ -18,7 +19,7 @@ export interface TableRowProps {
|
|||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const TableRow: React.FC<TableRowProps> = ({id, action, hideActions, className, testId, separator, bgOnHover = true, onClick, children}) => {
|
||||
const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(function TableRow({id, action, hideActions, className, style, testId, separator, bgOnHover = true, onClick, children}, ref) {
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
onClick?.(e);
|
||||
};
|
||||
|
@ -33,7 +34,7 @@ const TableRow: React.FC<TableRowProps> = ({id, action, hideActions, className,
|
|||
);
|
||||
|
||||
return (
|
||||
<tr className={tableRowClasses} data-testid={testId} id={id} onClick={handleClick}>
|
||||
<tr ref={ref} className={tableRowClasses} data-testid={testId} id={id} style={style} onClick={handleClick}>
|
||||
{children}
|
||||
{action &&
|
||||
<td className={`w-[1%] whitespace-nowrap p-0 hover:cursor-pointer`}>
|
||||
|
@ -44,6 +45,6 @@ const TableRow: React.FC<TableRowProps> = ({id, action, hideActions, className,
|
|||
}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default TableRow;
|
||||
|
|
|
@ -106,8 +106,8 @@ export {default as Popover} from './global/Popover';
|
|||
export type {PopoverProps} from './global/Popover';
|
||||
export {default as Separator} from './global/Separator';
|
||||
export type {SeparatorProps} from './global/Separator';
|
||||
export {default as SortableList} from './global/SortableList';
|
||||
export type {SortableListProps} from './global/SortableList';
|
||||
export {DragIndicator, default as SortableList} from './global/SortableList';
|
||||
export type {DragIndicatorProps, SortableItemContainerProps, SortableListProps} from './global/SortableList';
|
||||
export {default as StickyFooter} from './global/StickyFooter';
|
||||
export type {StickyFooterProps} from './global/StickyFooter';
|
||||
export {default as TabView} from './global/TabView';
|
||||
|
@ -149,6 +149,7 @@ export type {StripeButtonProps} from './settings/StripeButton';
|
|||
export {default as useGlobalDirtyState} from './hooks/useGlobalDirtyState';
|
||||
export {usePagination} from './hooks/usePagination';
|
||||
export type {PaginationData} from './hooks/usePagination';
|
||||
export {default as useSortableIndexedList} from './hooks/useSortableIndexedList';
|
||||
|
||||
export {debounce} from './utils/debounce';
|
||||
export {confirmIfDirty} from './utils/modals';
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface NewslettersResponseType {
|
|||
}
|
||||
|
||||
const dataType = 'NewslettersResponseType';
|
||||
export const newslettersDataType = dataType;
|
||||
|
||||
export const useBrowseNewsletters = createInfiniteQuery<NewslettersResponseType & {isEnd: boolean}>({
|
||||
dataType,
|
||||
|
|
|
@ -7,7 +7,9 @@ import useQueryParams from '../../../hooks/useQueryParams';
|
|||
import useRouting from '../../../hooks/useRouting';
|
||||
import {APIError} from '../../../utils/errors';
|
||||
import {Button, ConfirmationModal, TabView, withErrorBoundary} from '@tryghost/admin-x-design-system';
|
||||
import {useBrowseNewsletters, useVerifyNewsletterEmail} from '../../../api/newsletters';
|
||||
import {InfiniteData, useQueryClient} from '@tanstack/react-query';
|
||||
import {Newsletter, NewslettersResponseType, newslettersDataType, useBrowseNewsletters, useEditNewsletter, useVerifyNewsletterEmail} from '../../../api/newsletters';
|
||||
import {arrayMove} from '@dnd-kit/sortable';
|
||||
|
||||
const NavigateToNewsletter = ({id, children}: {id: string; children: ReactNode}) => {
|
||||
const modal = useModal();
|
||||
|
@ -25,12 +27,20 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
updateRoute('newsletters/new');
|
||||
};
|
||||
const [selectedTab, setSelectedTab] = useState('active-newsletters');
|
||||
const {data: {newsletters, meta, isEnd} = {}, fetchNextPage} = useBrowseNewsletters();
|
||||
const {data: {newsletters: apiNewsletters, meta, isEnd} = {}, fetchNextPage} = useBrowseNewsletters();
|
||||
const {mutateAsync: editNewsletter} = useEditNewsletter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const verifyEmailToken = useQueryParams().getParam('verifyEmail');
|
||||
const {mutateAsync: verifyEmail} = useVerifyNewsletterEmail();
|
||||
const handleError = useHandleError();
|
||||
|
||||
const [newsletters, setNewsletters] = useState<Newsletter[]>(apiNewsletters || []);
|
||||
|
||||
useEffect(() => {
|
||||
setNewsletters(apiNewsletters || []);
|
||||
}, [apiNewsletters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!verifyEmailToken) {
|
||||
return;
|
||||
|
@ -72,16 +82,55 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
}} />
|
||||
);
|
||||
|
||||
const sortedActiveNewsletters = newsletters.filter(n => n.status === 'active').sort((a, b) => a.sort_order - b.sort_order) || [];
|
||||
const archivedNewsletters = newsletters.filter(newsletter => newsletter.status !== 'active');
|
||||
|
||||
const onSort = async (id: string, overId?: string) => {
|
||||
const fromIndex = sortedActiveNewsletters.findIndex(newsletter => newsletter.id === id);
|
||||
const toIndex = sortedActiveNewsletters.findIndex(newsletter => newsletter.id === overId) || 0;
|
||||
const newSortOrder = arrayMove(sortedActiveNewsletters, fromIndex, toIndex);
|
||||
|
||||
const updatedActiveNewsletters = newSortOrder.map((newsletter, index) => (
|
||||
newsletter.sort_order === index ? null : {...newsletter, sort_order: index}
|
||||
)).filter((newsletter): newsletter is Newsletter => !!newsletter);
|
||||
|
||||
const updatedArchivedNewsletters = archivedNewsletters.map((newsletter, index) => (
|
||||
newsletter.sort_order === index + sortedActiveNewsletters.length ? null : {...newsletter, sort_order: index}
|
||||
)).filter((newsletter): newsletter is Newsletter => !!newsletter);
|
||||
|
||||
const orderUpdatedNewsletters = [...updatedActiveNewsletters, ...updatedArchivedNewsletters].sort((a, b) => a.sort_order - b.sort_order);
|
||||
|
||||
// Set the new order in local state and cache first so that the UI updates immediately
|
||||
setNewsletters(newsletters.map(newsletter => orderUpdatedNewsletters.find(n => n.id === newsletter.id) || newsletter));
|
||||
queryClient.setQueriesData<InfiniteData<NewslettersResponseType>>([newslettersDataType], (currentData) => {
|
||||
if (!currentData) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentData,
|
||||
pages: currentData.pages.map(page => ({
|
||||
...page,
|
||||
newsletters: page.newsletters.map(newsletter => orderUpdatedNewsletters.find(n => n.id === newsletter.id) || newsletter)
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
for (const newsletter of orderUpdatedNewsletters) {
|
||||
await editNewsletter(newsletter);
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'active-newsletters',
|
||||
title: 'Active',
|
||||
contents: (<NewslettersList newsletters={newsletters?.filter(newsletter => newsletter.status === 'active') || []} />)
|
||||
contents: (<NewslettersList newsletters={sortedActiveNewsletters} isSortable onSort={onSort} />)
|
||||
},
|
||||
{
|
||||
id: 'archived-newsletters',
|
||||
title: 'Archived',
|
||||
contents: (<NewslettersList newsletters={newsletters?.filter(newsletter => newsletter.status !== 'active') || []} />)
|
||||
contents: (<NewslettersList newsletters={archivedNewsletters} />)
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -1,13 +1,52 @@
|
|||
import React from 'react';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {Button, NoValueLabel, Table, TableCell, TableRow} from '@tryghost/admin-x-design-system';
|
||||
import {Button, DragIndicator, NoValueLabel, SortableItemContainerProps, SortableList, Table, TableCell, TableRow} from '@tryghost/admin-x-design-system';
|
||||
import {Newsletter} from '../../../../api/newsletters';
|
||||
import {numberWithCommas} from '../../../../utils/helpers';
|
||||
|
||||
interface NewslettersListProps {
|
||||
newsletters: Newsletter[]
|
||||
newsletters: Newsletter[];
|
||||
isSortable?: boolean;
|
||||
onSort?: (activeId: string, overId?: string) => void;
|
||||
}
|
||||
|
||||
const NewsletterItemContainer: React.FC<Partial<SortableItemContainerProps>> = ({
|
||||
id,
|
||||
setRef,
|
||||
isDragging,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const showDetails = () => {
|
||||
updateRoute({route: `newsletters/${id}`});
|
||||
};
|
||||
|
||||
const container = (
|
||||
<TableRow
|
||||
ref={setRef}
|
||||
action={<Button color='green' label='Edit' link onClick={showDetails} />}
|
||||
className={isDragging ? 'opacity-75' : ''}
|
||||
style={style}
|
||||
hideActions
|
||||
onClick={showDetails}
|
||||
>
|
||||
{(props.dragHandleAttributes || isDragging) && <TableCell className='w-10 !align-middle' >
|
||||
<DragIndicator className='h-10' isDragging={isDragging || false} {...props} />
|
||||
</TableCell>}
|
||||
{children}
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
if (isDragging) {
|
||||
return <Table>{container}</Table>;
|
||||
} else {
|
||||
return container;
|
||||
}
|
||||
};
|
||||
|
||||
const NewsletterItem: React.FC<{newsletter: Newsletter}> = ({newsletter}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
|
@ -16,33 +55,45 @@ const NewsletterItem: React.FC<{newsletter: Newsletter}> = ({newsletter}) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<TableRow action={<Button color='green' label='Edit' link onClick={showDetails} />} hideActions onClick={showDetails}>
|
||||
<TableCell onClick={showDetails}>
|
||||
<>
|
||||
<TableCell className='w-full' onClick={showDetails}>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span className='font-medium'>{newsletter.name}</span>
|
||||
<span className='mt-0.5 text-xs leading-tight text-grey-700'>{newsletter.description || 'No description'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:!visible md:!table-cell' onClick={showDetails}>
|
||||
<TableCell className='hidden md:!visible md:!table-cell md:min-w-[11rem]' onClick={showDetails}>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span>{numberWithCommas(newsletter.count?.active_members || 0) }</span>
|
||||
<span className='mt-0.5 whitespace-nowrap text-xs leading-tight text-grey-700'>Subscribers</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:!visible md:!table-cell' onClick={showDetails}>
|
||||
<TableCell className='hidden md:!visible md:!table-cell md:min-w-[11rem]' onClick={showDetails}>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span>{numberWithCommas(newsletter.count?.posts || 0)}</span>
|
||||
<span className='mt-0.5 whitespace-nowrap text-xs leading-tight text-grey-700'>Delivered</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NewslettersList: React.FC<NewslettersListProps> = ({newsletters}) => {
|
||||
if (newsletters.length) {
|
||||
const NewslettersList: React.FC<NewslettersListProps> = ({newsletters, isSortable, onSort}) => {
|
||||
if (newsletters.length && isSortable) {
|
||||
return <SortableList
|
||||
container={props => <NewsletterItemContainer {...props} />}
|
||||
items={newsletters}
|
||||
renderItem={item => <NewsletterItem newsletter={item} />}
|
||||
wrapper={Table}
|
||||
onMove={(id, overId) => onSort?.(id, overId)}
|
||||
/>;
|
||||
} else if (newsletters.length) {
|
||||
return <Table>
|
||||
{newsletters.map(newsletter => <NewsletterItem key={newsletter.id} newsletter={newsletter} />)}
|
||||
{newsletters.map(newsletter => (
|
||||
<NewsletterItemContainer id={newsletter.id}>
|
||||
<NewsletterItem newsletter={newsletter} />
|
||||
</NewsletterItemContainer>
|
||||
))}
|
||||
</Table>;
|
||||
} else {
|
||||
return <NoValueLabel icon='mail-block'>
|
||||
|
|
|
@ -5,8 +5,7 @@ import useForm, {ErrorMessages} from '../../../../hooks/useForm';
|
|||
import useHandleError from '../../../../utils/api/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||
import useSortableIndexedList from '../../../../hooks/useSortableIndexedList';
|
||||
import {Button, ButtonProps, ConfirmationModal, CurrencyField, Form, Heading, Icon, Modal, Select, SortableList, TextField, Toggle, URLTextField, showToast} from '@tryghost/admin-x-design-system';
|
||||
import {Button, ButtonProps, ConfirmationModal, CurrencyField, Form, Heading, Icon, Modal, Select, SortableList, TextField, Toggle, URLTextField, showToast, useSortableIndexedList} from '@tryghost/admin-x-design-system';
|
||||
import {RoutingModalProps} from '../../../providers/RoutingProvider';
|
||||
import {Tier, useAddTier, useBrowseTiers, useEditTier} from '../../../../api/tiers';
|
||||
import {currencies, currencySelectGroups, validateCurrencyAmount} from '../../../../utils/currency';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import useSortableIndexedList from '../useSortableIndexedList';
|
||||
import validator from 'validator';
|
||||
import {useSortableIndexedList} from '@tryghost/admin-x-design-system';
|
||||
|
||||
export type NavigationItem = {
|
||||
label: string;
|
||||
|
|
Loading…
Add table
Reference in a new issue