diff --git a/apps/admin-x-settings/src/api/users.ts b/apps/admin-x-settings/src/api/users.ts index dcf078e192..697cd36038 100644 --- a/apps/admin-x-settings/src/api/users.ts +++ b/apps/admin-x-settings/src/api/users.ts @@ -85,6 +85,7 @@ export const useEditUser = createMutation({ method: 'PUT', path: user => `/users/${user.id}/`, body: user => ({users: [user]}), + searchParams: () => ({include: 'roles'}), updateQueries: { dataType, update: updateUsers diff --git a/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx b/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx index a1ee40c353..175e8ec056 100644 --- a/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx +++ b/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx @@ -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({ +export const RouteContext = createContext({ 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 = ({children}) => { const [route, setRoute] = useState(''); const [yScroll, setYScroll] = useState(0); const [scrolledRoute, setScrolledRoute] = useState(''); + const routeChangeListeners = useRef([]); - 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 = ({children}) => { const handleHashChange = () => { const matchedRoute = handleNavigation(); setRoute(matchedRoute); + callRouteChangeListeners(matchedRoute, routeChangeListeners.current); }; const handleScroll = () => { @@ -127,6 +167,7 @@ const RoutingProvider: React.FC = ({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 = ({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 ( = ({children}) => { scrolledRoute, yScroll, updateRoute, - updateScrolled + updateScrolled, + addRouteChangeListener }} > {children} diff --git a/apps/admin-x-settings/src/components/settings/email/Newsletters.tsx b/apps/admin-x-settings/src/components/settings/email/Newsletters.tsx index d1e6d2658d..a496a9fdb0 100644 --- a/apps/admin-x-settings/src/components/settings/email/Newsletters.tsx +++ b/apps/admin-x-settings/src/components/settings/email/Newsletters.tsx @@ -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 = (