mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Updated AdminX detail modals to use routes (#17639)
refs https://github.com/TryGhost/Product/issues/3349
This commit is contained in:
parent
8d0eed2dcd
commit
7ddaad8209
16 changed files with 167 additions and 48 deletions
apps/admin-x-settings
src
api
components
providers
settings
email
general
membership
hooks
test/e2e/general/users
|
@ -85,6 +85,7 @@ export const useEditUser = createMutation<UsersResponseType, User>({
|
|||
method: 'PUT',
|
||||
path: user => `/users/${user.id}/`,
|
||||
body: user => ({users: [user]}),
|
||||
searchParams: () => ({include: 'roles'}),
|
||||
updateQueries: {
|
||||
dataType,
|
||||
update: updateUsers
|
||||
|
|
|
@ -5,26 +5,39 @@ import InviteUserModal from '../settings/general/InviteUserModal';
|
|||
import NavigationModal from '../settings/site/NavigationModal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import PortalModal from '../settings/membership/portal/PortalModal';
|
||||
import React, {createContext, useCallback, useEffect, useState} from 'react';
|
||||
import React, {createContext, useCallback, useEffect, useRef, useState} from 'react';
|
||||
import StripeConnectModal from '../settings/membership/stripe/StripeConnectModal';
|
||||
import TierDetailModal from '../settings/membership/tiers/TierDetailModal';
|
||||
|
||||
type RoutingContextProps = {
|
||||
export type RouteParams = {[key: string]: string}
|
||||
|
||||
export type RoutingContextData = {
|
||||
route: string;
|
||||
scrolledRoute: string;
|
||||
yScroll: number;
|
||||
updateRoute: (newPath: string) => void;
|
||||
updateRoute: (newPath: string, params?: RouteParams) => void;
|
||||
updateScrolled: (newPath: string) => void;
|
||||
addRouteChangeListener: (listener: RouteChangeListener) => (() => void);
|
||||
};
|
||||
|
||||
export const RouteContext = createContext<RoutingContextProps>({
|
||||
export const RouteContext = createContext<RoutingContextData>({
|
||||
route: '',
|
||||
scrolledRoute: '',
|
||||
yScroll: 0,
|
||||
updateRoute: () => {},
|
||||
updateScrolled: () => {}
|
||||
updateScrolled: () => {},
|
||||
addRouteChangeListener: () => (() => {})
|
||||
});
|
||||
|
||||
// These routes need to be handled by a SettingGroup (or other component) with the
|
||||
// useHandleRoute hook. The idea is that those components will open a modal after
|
||||
// loading any data required for the route
|
||||
export const modalRoutes = {
|
||||
showUser: 'users/show/:slug',
|
||||
showNewsletter: 'newsletters/show/:id',
|
||||
showTier: 'tiers/show/:id'
|
||||
};
|
||||
|
||||
function getHashPath(urlPath: string | undefined) {
|
||||
if (!urlPath) {
|
||||
return null;
|
||||
|
@ -84,29 +97,55 @@ const handleNavigation = (scroll: boolean = true) => {
|
|||
return '';
|
||||
};
|
||||
|
||||
const matchRoute = (pathname: string, routeDefinition: string) => {
|
||||
const regex = new RegExp(routeDefinition.replace(/:(\w+)/, '(?<$1>[^/]+)'));
|
||||
|
||||
return pathname.match(regex)?.groups;
|
||||
};
|
||||
|
||||
const callRouteChangeListeners = (newPath: string, listeners: RouteChangeListener[]) => {
|
||||
listeners.forEach((listener) => {
|
||||
const params = matchRoute(newPath, listener.route);
|
||||
|
||||
if (params) {
|
||||
listener.callback(params);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
type RouteProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type RouteChangeListener = {
|
||||
route: string;
|
||||
callback: (params: RouteParams) => void;
|
||||
}
|
||||
|
||||
const RoutingProvider: React.FC<RouteProviderProps> = ({children}) => {
|
||||
const [route, setRoute] = useState<string>('');
|
||||
const [yScroll, setYScroll] = useState(0);
|
||||
const [scrolledRoute, setScrolledRoute] = useState<string>('');
|
||||
const routeChangeListeners = useRef<RouteChangeListener[]>([]);
|
||||
|
||||
const updateRoute = useCallback(
|
||||
(newPath: string) => {
|
||||
if (newPath) {
|
||||
if (newPath === route) {
|
||||
scrollToSectionGroup(newPath);
|
||||
} else {
|
||||
window.location.hash = `/settings-x/${newPath}`;
|
||||
}
|
||||
const updateRoute = useCallback((newPath: string, params?: RouteParams) => {
|
||||
if (params) {
|
||||
newPath = Object.entries(params).reduce(
|
||||
(path, [name, value]) => path.replace(`:${name}`, value),
|
||||
newPath
|
||||
);
|
||||
}
|
||||
|
||||
if (newPath) {
|
||||
if (newPath === route) {
|
||||
scrollToSectionGroup(newPath);
|
||||
} else {
|
||||
window.location.hash = `/settings-x`;
|
||||
window.location.hash = `/settings-x/${newPath}`;
|
||||
}
|
||||
},
|
||||
[route]
|
||||
);
|
||||
} else {
|
||||
window.location.hash = `/settings-x`;
|
||||
}
|
||||
}, [route]);
|
||||
|
||||
const updateScrolled = useCallback((newPath: string) => {
|
||||
setScrolledRoute(newPath);
|
||||
|
@ -116,6 +155,7 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({children}) => {
|
|||
const handleHashChange = () => {
|
||||
const matchedRoute = handleNavigation();
|
||||
setRoute(matchedRoute);
|
||||
callRouteChangeListeners(matchedRoute, routeChangeListeners.current);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
|
@ -127,6 +167,7 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({children}) => {
|
|||
const element = document.getElementById('admin-x-root');
|
||||
const matchedRoute = handleNavigation();
|
||||
setRoute(matchedRoute);
|
||||
callRouteChangeListeners(matchedRoute, routeChangeListeners.current);
|
||||
element!.addEventListener('scroll', handleScroll);
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
|
@ -135,7 +176,17 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({children}) => {
|
|||
element!.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
};
|
||||
}, []);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const addRouteChangeListener = useCallback((listener: RouteChangeListener) => {
|
||||
if (route && !routeChangeListeners.current.some(current => current.route === listener.route)) {
|
||||
callRouteChangeListeners(route, [listener]);
|
||||
}
|
||||
|
||||
routeChangeListeners.current = [...routeChangeListeners.current, listener];
|
||||
|
||||
return () => routeChangeListeners.current = routeChangeListeners.current.filter(current => current !== listener);
|
||||
}, [route]);
|
||||
|
||||
return (
|
||||
<RouteContext.Provider
|
||||
|
@ -144,7 +195,8 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({children}) => {
|
|||
scrolledRoute,
|
||||
yScroll,
|
||||
updateRoute,
|
||||
updateScrolled
|
||||
updateScrolled,
|
||||
addRouteChangeListener
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import NewsletterDetailModal from './newsletters/NewsletterDetailModal';
|
||||
import NewslettersList from './newsletters/NewslettersList';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React, {useState} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import useHandleRoute from '../../../hooks/useHandleRoute';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {modalRoutes} from '../../providers/RoutingProvider';
|
||||
import {useBrowseNewsletters} from '../../../api/newsletters';
|
||||
|
||||
const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
|
@ -14,6 +18,16 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
const [selectedTab, setSelectedTab] = useState('active-newsletters');
|
||||
const {data: {newsletters} = {}} = useBrowseNewsletters();
|
||||
|
||||
useHandleRoute(modalRoutes.showNewsletter, ({id}) => {
|
||||
const newsletter = newsletters?.find(u => u.id === id);
|
||||
|
||||
if (!newsletter) {
|
||||
return;
|
||||
}
|
||||
|
||||
NiceModal.show(NewsletterDetailModal, {newsletter});
|
||||
}, [newsletters]);
|
||||
|
||||
const buttons = (
|
||||
<Button color='green' label='Add newsletter' link={true} onClick={() => {
|
||||
openNewsletterModal();
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Form from '../../../../admin-x-ds/global/form/Form';
|
||||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||
import NewsletterDetailModal from './NewsletterDetailModal';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||
|
@ -8,6 +7,7 @@ import TextField from '../../../../admin-x-ds/global/form/TextField';
|
|||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {modalRoutes} from '../../../providers/RoutingProvider';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
import {toast} from 'react-hot-toast';
|
||||
import {useAddNewsletter} from '../../../../api/newsletters';
|
||||
|
@ -37,9 +37,7 @@ const AddNewsletterModal: React.FC<AddNewsletterModalProps> = () => {
|
|||
opt_in_existing: formState.optInExistingSubscribers
|
||||
});
|
||||
|
||||
NiceModal.show(NewsletterDetailModal, {
|
||||
newsletter: response.newsletters[0]
|
||||
});
|
||||
updateRoute(modalRoutes.showNewsletter, {id: response.newsletters[0].id});
|
||||
},
|
||||
onValidate: () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
@ -65,7 +63,6 @@ const AddNewsletterModal: React.FC<AddNewsletterModalProps> = () => {
|
|||
toast.remove();
|
||||
if (await handleSave()) {
|
||||
modal.remove();
|
||||
updateRoute('newsletters');
|
||||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
|
|
|
@ -17,6 +17,7 @@ import TextField from '../../../../admin-x-ds/global/form/TextField';
|
|||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import ToggleGroup from '../../../../admin-x-ds/global/form/ToggleGroup';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import validator from 'validator';
|
||||
import {Newsletter, useEditNewsletter} from '../../../../api/newsletters';
|
||||
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
|
||||
|
@ -281,6 +282,7 @@ const NewsletterDetailModal: React.FC<NewsletterDetailModalProps> = ({newsletter
|
|||
const modal = useModal();
|
||||
const {siteData} = useGlobalData();
|
||||
const {mutateAsync: editNewsletter} = useEditNewsletter();
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const {formState, updateForm, handleSave, validate, errors, clearError} = useForm({
|
||||
initialState: newsletter,
|
||||
|
@ -329,6 +331,7 @@ const NewsletterDetailModal: React.FC<NewsletterDetailModalProps> = ({newsletter
|
|||
const sidebar = <Sidebar clearError={clearError} errors={errors} newsletter={formState} updateNewsletter={updateNewsletter} validate={validate} />;
|
||||
|
||||
return <PreviewModalContent
|
||||
afterClose={() => updateRoute('newsletters')}
|
||||
deviceSelector={false}
|
||||
okLabel='Save & close'
|
||||
preview={preview}
|
||||
|
@ -342,6 +345,7 @@ const NewsletterDetailModal: React.FC<NewsletterDetailModalProps> = ({newsletter
|
|||
toast.remove();
|
||||
if (await handleSave()) {
|
||||
modal.remove();
|
||||
updateRoute('newsletters');
|
||||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import Button from '../../../../admin-x-ds/global/Button';
|
||||
import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal';
|
||||
import NewsletterDetailModal from './NewsletterDetailModal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
|
||||
import React from 'react';
|
||||
import Table from '../../../../admin-x-ds/global/Table';
|
||||
import TableCell from '../../../../admin-x-ds/global/TableCell';
|
||||
import TableRow from '../../../../admin-x-ds/global/TableRow';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {Newsletter, useEditNewsletter} from '../../../../api/newsletters';
|
||||
import {modalRoutes} from '../../../providers/RoutingProvider';
|
||||
|
||||
interface NewslettersListProps {
|
||||
newsletters: Newsletter[]
|
||||
|
@ -15,6 +16,7 @@ interface NewslettersListProps {
|
|||
|
||||
const NewsletterItem: React.FC<{newsletter: Newsletter, onlyOne: boolean}> = ({newsletter, onlyOne}) => {
|
||||
const {mutateAsync: editNewsletter} = useEditNewsletter();
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const action = newsletter.status === 'active' ? (
|
||||
<Button color='green' disabled={onlyOne} label='Archive' link onClick={() => {
|
||||
|
@ -48,7 +50,7 @@ const NewsletterItem: React.FC<{newsletter: Newsletter, onlyOne: boolean}> = ({n
|
|||
);
|
||||
|
||||
const showDetails = () => {
|
||||
NiceModal.show(NewsletterDetailModal, {newsletter});
|
||||
updateRoute(modalRoutes.showNewsletter, {id: newsletter.id});
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -12,6 +12,7 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
|||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
||||
import validator from 'validator';
|
||||
import {User, isAdminUser, isOwnerUser, useDeleteUser, useEditUser, useMakeOwner, useUpdatePassword} from '../../../api/users';
|
||||
|
@ -419,6 +420,7 @@ const UserMenuTrigger = () => (
|
|||
);
|
||||
|
||||
const UserDetailModal:React.FC<UserDetailModalProps> = ({user}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
const {ownerUser} = useStaffUsers();
|
||||
const [userData, setUserData] = useState(user);
|
||||
const [saveState, setSaveState] = useState('');
|
||||
|
@ -434,6 +436,15 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user}) => {
|
|||
const {mutateAsync: deleteUser} = useDeleteUser();
|
||||
const {mutateAsync: makeOwner} = useMakeOwner();
|
||||
|
||||
useEffect(() => {
|
||||
if (saveState === 'saved') {
|
||||
setTimeout(() => {
|
||||
mainModal.remove();
|
||||
updateRoute('users');
|
||||
}, 300);
|
||||
}
|
||||
}, [mainModal, saveState, updateRoute]);
|
||||
|
||||
const confirmSuspend = (_user: User) => {
|
||||
let warningText = 'This user will no longer be able to log in but their posts will be kept.';
|
||||
if (_user.status === 'inactive') {
|
||||
|
@ -583,9 +594,6 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user}) => {
|
|||
okLabel = 'Saving...';
|
||||
} else if (saveState === 'saved') {
|
||||
okLabel = 'Saved';
|
||||
setTimeout(() => {
|
||||
mainModal.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
const fileUploadButtonClasses = 'absolute right-[104px] bottom-12 bg-[rgba(0,0,0,0.75)] rounded text-sm text-white flex items-center justify-center px-3 h-8 opacity-80 hover:opacity-100 transition cursor-pointer font-medium z-10';
|
||||
|
@ -617,6 +625,7 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user}) => {
|
|||
|
||||
return (
|
||||
<Modal
|
||||
afterClose={() => updateRoute('users')}
|
||||
okLabel={okLabel}
|
||||
size='lg'
|
||||
stickyFooter={true}
|
||||
|
|
|
@ -8,11 +8,13 @@ import React, {useState} from 'react';
|
|||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import UserDetailModal from './UserDetailModal';
|
||||
import useHandleRoute from '../../../hooks/useHandleRoute';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
||||
import {User} from '../../../api/users';
|
||||
import {UserInvite, useAddInvite, useDeleteInvite} from '../../../api/invites';
|
||||
import {generateAvatarColor, getInitials} from '../../../utils/helpers';
|
||||
import {modalRoutes} from '../../providers/RoutingProvider';
|
||||
import {showToast} from '../../../admin-x-ds/global/Toast';
|
||||
|
||||
interface OwnerProps {
|
||||
|
@ -31,8 +33,10 @@ interface InviteListProps {
|
|||
}
|
||||
|
||||
const Owner: React.FC<OwnerProps> = ({user}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const showDetailModal = () => {
|
||||
NiceModal.show(UserDetailModal, {user});
|
||||
updateRoute(modalRoutes.showUser, {slug: user.slug});
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
|
@ -51,8 +55,10 @@ const Owner: React.FC<OwnerProps> = ({user}) => {
|
|||
};
|
||||
|
||||
const UsersList: React.FC<UsersListProps> = ({users}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const showDetailModal = (user: User) => {
|
||||
NiceModal.show(UserDetailModal, {user});
|
||||
updateRoute(modalRoutes.showUser, {slug: user.slug});
|
||||
};
|
||||
|
||||
if (!users || !users.length) {
|
||||
|
@ -179,6 +185,7 @@ const InvitesUserList: React.FC<InviteListProps> = ({users}) => {
|
|||
|
||||
const Users: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const {
|
||||
users,
|
||||
ownerUser,
|
||||
adminUsers,
|
||||
editorUsers,
|
||||
|
@ -186,7 +193,19 @@ const Users: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
contributorUsers,
|
||||
invites
|
||||
} = useStaffUsers();
|
||||
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
useHandleRoute(modalRoutes.showUser, ({slug}) => {
|
||||
const user = users.find(u => u.slug === slug);
|
||||
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
NiceModal.show(UserDetailModal, {user});
|
||||
}, [users]);
|
||||
|
||||
const showInviteModal = () => {
|
||||
updateRoute('users/invite');
|
||||
};
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React, {useState} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import StripeButton from '../../../admin-x-ds/settings/StripeButton';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import TierDetailModal from './tiers/TierDetailModal';
|
||||
import TiersList from './tiers/TiersList';
|
||||
import useHandleRoute from '../../../hooks/useHandleRoute';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {Tier, getActiveTiers, getArchivedTiers, useBrowseTiers} from '../../../api/tiers';
|
||||
import {checkStripeEnabled} from '../../../api/settings';
|
||||
import {modalRoutes} from '../../providers/RoutingProvider';
|
||||
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
||||
|
||||
const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
|
@ -16,6 +20,14 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
const archivedTiers = getArchivedTiers(tiers || []);
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
useHandleRoute(modalRoutes.showTier, ({id}) => {
|
||||
const tier = tiers?.find(t => t.id === id);
|
||||
|
||||
if (tier) {
|
||||
NiceModal.show(TierDetailModal, {tier});
|
||||
}
|
||||
}, [tiers]);
|
||||
|
||||
const openConnectModal = () => {
|
||||
updateRoute('stripe-connect');
|
||||
};
|
||||
|
|
|
@ -132,6 +132,7 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
|||
if (saveState !== 'unsaved') {
|
||||
toast.dismiss();
|
||||
modal.remove();
|
||||
updateRoute('tiers');
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import Button from '../../../../admin-x-ds/global/Button';
|
||||
import Icon from '../../../../admin-x-ds/global/Icon';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
|
||||
import React from 'react';
|
||||
import TierDetailModal from './TierDetailModal';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {Tier, useEditTier} from '../../../../api/tiers';
|
||||
import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
|
||||
import {modalRoutes} from '../../../providers/RoutingProvider';
|
||||
import {numberWithCommas} from '../../../../utils/helpers';
|
||||
|
||||
interface TiersListProps {
|
||||
|
@ -21,6 +20,7 @@ interface TierCardProps {
|
|||
const cardContainerClasses = 'group flex min-h-[200px] flex-col items-start justify-between gap-4 self-stretch rounded-sm border border-grey-300 p-4 transition-all hover:border-grey-400';
|
||||
|
||||
const TierCard: React.FC<TierCardProps> = ({tier}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
const {mutateAsync: updateTier} = useEditTier();
|
||||
const currency = tier?.currency || 'USD';
|
||||
const currencySymbol = currency ? getSymbol(currency) : '$';
|
||||
|
@ -28,7 +28,7 @@ const TierCard: React.FC<TierCardProps> = ({tier}) => {
|
|||
return (
|
||||
<div className={cardContainerClasses} data-testid='tier-card'>
|
||||
<div className='w-full grow cursor-pointer' onClick={() => {
|
||||
NiceModal.show(TierDetailModal, {tier});
|
||||
updateRoute(modalRoutes.showTier, {id: tier.id});
|
||||
}}>
|
||||
<div className='text-[1.65rem] font-bold leading-tight tracking-tight text-pink'>{tier.name}</div>
|
||||
<div className='mt-2 flex items-baseline'>
|
||||
|
|
14
apps/admin-x-settings/src/hooks/useHandleRoute.tsx
Normal file
14
apps/admin-x-settings/src/hooks/useHandleRoute.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {RouteContext, RouteParams} from '../components/providers/RoutingProvider';
|
||||
import {useContext, useEffect} from 'react';
|
||||
|
||||
const useHandleRoute = (route: string, callback: (params: RouteParams) => void, dependencies: unknown[]) => {
|
||||
const {addRouteChangeListener} = useContext(RouteContext);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = addRouteChangeListener({route, callback});
|
||||
|
||||
return unsubscribe;
|
||||
}, [route, ...dependencies]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
};
|
||||
|
||||
export default useHandleRoute;
|
|
@ -1,13 +1,7 @@
|
|||
import {RouteContext} from '../components/providers/RoutingProvider';
|
||||
import {RouteContext, RoutingContextData} from '../components/providers/RoutingProvider';
|
||||
import {useContext} from 'react';
|
||||
|
||||
export type RoutingHook = {
|
||||
route: string;
|
||||
scrolledRoute: string;
|
||||
yScroll?: number;
|
||||
updateScrolled: (newPath: string) => void,
|
||||
updateRoute: (newPath: string) => void
|
||||
};
|
||||
export type RoutingHook = Pick<RoutingContextData, 'route' | 'scrolledRoute' | 'yScroll' | 'updateRoute' | 'updateScrolled'>;
|
||||
|
||||
const useRouting = (): RoutingHook => {
|
||||
const {route, scrolledRoute, yScroll, updateScrolled, updateRoute} = useContext(RouteContext);
|
||||
|
|
|
@ -8,7 +8,7 @@ test.describe('User actions', async () => {
|
|||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/`, response: {
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||
users: [{
|
||||
...userToEdit,
|
||||
status: 'inactive'
|
||||
|
@ -59,7 +59,7 @@ test.describe('User actions', async () => {
|
|||
}
|
||||
]
|
||||
}},
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/`, response: {
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||
users: [{
|
||||
...userToEdit,
|
||||
status: 'active'
|
||||
|
@ -152,7 +152,7 @@ test.describe('User actions', async () => {
|
|||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
||||
editUser: {method: 'PUT', path: /^\/users\/\w{24}\/$/, response: responseFixtures.users},
|
||||
editUser: {method: 'PUT', path: /^\/users\/\w{24}\/\?include=roles$/, response: responseFixtures.users},
|
||||
makeOwner: {method: 'PUT', path: '/users/owner/', response: makeOwnerResponse}
|
||||
}});
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ test.describe('User profile', async () => {
|
|||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/`, response: {
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||
users: [{
|
||||
...userToEdit,
|
||||
email: 'newadmin@test.com',
|
||||
|
@ -111,7 +111,7 @@ test.describe('User profile', async () => {
|
|||
...globalDataRequests,
|
||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
||||
uploadImage: {method: 'POST', path: '/images/upload/', response: {images: [{url: 'http://example.com/image.png', ref: null}]}},
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/`, response: {
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||
users: [{
|
||||
...userToEdit,
|
||||
profile_image: 'http://example.com/image.png',
|
||||
|
|
|
@ -45,7 +45,7 @@ test.describe('User roles', async () => {
|
|||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
||||
browseRoles: {method: 'GET', path: '/roles/?limit=all', response: responseFixtures.roles},
|
||||
browseAssignableRoles: {method: 'GET', path: '/roles/?limit=all&permissions=assign', response: responseFixtures.roles},
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/`, response: {
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||
users: [{
|
||||
...userToEdit,
|
||||
roles: [responseFixtures.roles.roles.find(role => role.name === 'Editor')!]
|
||||
|
|
Loading…
Add table
Reference in a new issue