diff --git a/client/packages/common/src/ui/components/navigation/AppNavLink/AppNavLink.tsx b/client/packages/common/src/ui/components/navigation/AppNavLink/AppNavLink.tsx index 4307f8d165..a2f9c540f9 100644 --- a/client/packages/common/src/ui/components/navigation/AppNavLink/AppNavLink.tsx +++ b/client/packages/common/src/ui/components/navigation/AppNavLink/AppNavLink.tsx @@ -72,7 +72,7 @@ export interface AppNavLinkProps { text?: string; to: string; visible?: boolean; - onClick?: () => void; + onClick?: React.MouseEventHandler; } export const AppNavLink: FC = props => { @@ -92,11 +92,11 @@ export const AppNavLink: FC = props => { const match = useMatch({ path: `${to}/*` }); const isSelectedParentItem = inactive && !!match; const showMenuSectionIcon = inactive && drawer.isOpen; - const handleClick = () => { + const handleClick = (e: React.MouseEvent) => { // reset the clicked nav path when navigating // otherwise the child menu remains open drawer.setClickedNavPath(undefined); - if (onClick) onClick(); + if (onClick) onClick(e); drawer.onClick(); }; diff --git a/client/packages/common/src/ui/components/navigation/Breadcrumbs/Breadcrumbs.tsx b/client/packages/common/src/ui/components/navigation/Breadcrumbs/Breadcrumbs.tsx index 0e038f4a53..5766905b6b 100644 --- a/client/packages/common/src/ui/components/navigation/Breadcrumbs/Breadcrumbs.tsx +++ b/client/packages/common/src/ui/components/navigation/Breadcrumbs/Breadcrumbs.tsx @@ -15,12 +15,7 @@ export const Breadcrumb = styled(Link)({ }); export const Breadcrumbs = ({ - topLevelPaths = [ - AppRoute.Settings, - AppRoute.Sync, - AppRoute.Reports, - AppRoute.Help, - ], + topLevelPaths = [AppRoute.Settings, AppRoute.Reports, AppRoute.Help], }: { topLevelPaths?: string[]; }) => { diff --git a/client/packages/common/src/utils/environment/EnvUtils.ts b/client/packages/common/src/utils/environment/EnvUtils.ts index 4dd0bb5dd9..5f931e6de1 100644 --- a/client/packages/common/src/utils/environment/EnvUtils.ts +++ b/client/packages/common/src/utils/environment/EnvUtils.ts @@ -64,8 +64,6 @@ const mapRoute = (route: string): RouteMapping => { return { title: 'stock', docs: '/inventory/stock-view/' }; case inRoute(AppRoute.Stocktakes): return { title: 'stocktakes', docs: '/inventory/stock-takes/' }; - case inRoute(AppRoute.Sync): - return { title: 'sync', docs: '/sync/synchronisation/' }; case inRoute(AppRoute.Settings): return { title: 'settings', docs: '/settings/' }; case inRoute(AppRoute.Patients): diff --git a/client/packages/config/src/routes.ts b/client/packages/config/src/routes.ts index 7634b78c5e..73869ec863 100644 --- a/client/packages/config/src/routes.ts +++ b/client/packages/config/src/routes.ts @@ -50,8 +50,6 @@ export enum AppRoute { Messages = 'messages', - Sync = 'sync', - Settings = 'settings', Help = 'help', diff --git a/client/packages/host/src/CommandK.tsx b/client/packages/host/src/CommandK.tsx index 36ec47cb17..9bee2d9023 100644 --- a/client/packages/host/src/CommandK.tsx +++ b/client/packages/host/src/CommandK.tsx @@ -24,6 +24,7 @@ import { import { AppRoute } from '@openmsupply-client/config'; import { Action } from 'kbar/lib/types'; import { useEasterEggModal } from './components/EasterEggModal'; +import { useSyncModal } from './components/Sync'; const CustomKBarSearch = styled(KBarSearch)(({ theme }) => ({ width: 500, @@ -90,6 +91,7 @@ const Actions = () => { const t = useTranslation(); const { store, logout, user, userHasPermission } = useAuthContext(); const showEasterEgg = useEasterEggModal(); + const showSync = useSyncModal(); const confirmLogout = useConfirmationModal({ onConfirm: () => { logout(); @@ -275,6 +277,13 @@ const Actions = () => { shortcut: ['h'], perform: () => navigate(RouteBuilder.create(AppRoute.Help).build()), }, + { + id: 'action:sync', + name: `${t('sync')} (Alt+Control+S)`, + keywords: 'sync', + shortcut: ['Alt+Control+KeyS'], + perform: showSync, + }, ]; if (userHasPermission(UserPermission.ServerAdmin)) { diff --git a/client/packages/host/src/Site.tsx b/client/packages/host/src/Site.tsx index 220f05aee0..a4e0157322 100644 --- a/client/packages/host/src/Site.tsx +++ b/client/packages/host/src/Site.tsx @@ -37,9 +37,10 @@ import { } from './routers'; import { RequireAuthentication } from './components/Navigation/RequireAuthentication'; import { QueryErrorHandler } from './QueryErrorHandler'; -import { Sync } from './components/Sync'; +// import { Sync } from './components/Sync'; import { EasterEggModalProvider } from './components'; import { Help } from './Help/Help'; +import { SyncModalProvider } from './components/Sync'; const NotifyOnLogin = () => { const { success } = useNotification(); @@ -72,158 +73,154 @@ export const Site: FC = () => { return ( - - - - - - - - - - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - } - /> - } - /> - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - - } - /> - } /> - + + + + + + + + + + + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + } + /> + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + + } + /> + } /> + + + + } /> - - } /> - - - - - - + + + + + + ); diff --git a/client/packages/host/src/components/AppBar/SectionIcon.tsx b/client/packages/host/src/components/AppBar/SectionIcon.tsx index c585d68bf9..439a6e98ee 100644 --- a/client/packages/host/src/components/AppBar/SectionIcon.tsx +++ b/client/packages/host/src/components/AppBar/SectionIcon.tsx @@ -4,7 +4,6 @@ import { HelpIcon, InvoiceIcon, ListIcon, - RadioIcon, ReportsIcon, SettingsIcon, SlidersIcon, @@ -48,8 +47,6 @@ const getIcon = (section?: AppRoute) => { return ; case AppRoute.Reports: return ; - case AppRoute.Sync: - return ; case AppRoute.Manage: return ; case AppRoute.Programs: @@ -70,7 +67,6 @@ const useSection = (): Section | undefined => { AppRoute.Inventory, AppRoute.Replenishment, AppRoute.Reports, - AppRoute.Sync, AppRoute.Manage, AppRoute.Programs, ]; diff --git a/client/packages/host/src/components/AppDrawer/SyncNavLink.tsx b/client/packages/host/src/components/AppDrawer/SyncNavLink.tsx index 44ef7d181f..1081dc9b00 100644 --- a/client/packages/host/src/components/AppDrawer/SyncNavLink.tsx +++ b/client/packages/host/src/components/AppDrawer/SyncNavLink.tsx @@ -6,15 +6,17 @@ import { useTheme, useTranslation, } from '@openmsupply-client/common'; -import { AppRoute } from '@openmsupply-client/config'; import { getBadgeProps } from '../../utils'; import { useSync } from '@openmsupply-client/system'; +import { useSyncModal } from '../Sync'; const POLLING_INTERVAL_IN_MILLISECONDS = 60 * 1000; export const SyncNavLink = () => { const t = useTranslation(); const theme = useTheme(); + const showSync = useSyncModal(); + const { syncStatus, numberOfRecordsInPushQueue } = useSync.utils.syncInfo( POLLING_INTERVAL_IN_MILLISECONDS ); @@ -36,7 +38,12 @@ export const SyncNavLink = () => { } return ( { + // prevent the anchor element from navigating + e.preventDefault(); + showSync(); + }} icon={} text={t('sync')} badgeProps={badgeProps} diff --git a/client/packages/host/src/components/Sync/Sync.tsx b/client/packages/host/src/components/Sync/SyncModal.tsx similarity index 55% rename from client/packages/host/src/components/Sync/Sync.tsx rename to client/packages/host/src/components/Sync/SyncModal.tsx index aee06873dd..efec5eff79 100644 --- a/client/packages/host/src/components/Sync/Sync.tsx +++ b/client/packages/host/src/components/Sync/SyncModal.tsx @@ -1,6 +1,7 @@ import React, { PropsWithChildren, useState, useEffect } from 'react'; import { + CloseIcon, DateUtils, Formatter, Grid, @@ -16,13 +17,22 @@ import { import { useSync } from '@openmsupply-client/system'; import { SyncProgress } from '../SyncProgress'; import { ServerInfo } from './ServerInfo'; +import { BasicModal, IconButton } from '@common/components'; const STATUS_POLLING_INTERVAL = 1000; -const useHostSync = () => { +interface SyncModalProps { + open: boolean; + width?: number; + height?: number; + onCancel: () => void; +} + +const useHostSync = (enabled: boolean) => { // Polling whenever Sync page is opened const { syncStatus, numberOfRecordsInPushQueue } = useSync.utils.syncInfo( - STATUS_POLLING_INTERVAL + STATUS_POLLING_INTERVAL, + enabled ); const { mutateAsync: manualSync } = useSync.sync.manualSync(); const { allowSleep, keepAwake } = useNativeClient(); @@ -69,7 +79,12 @@ const useHostSync = () => { }; }; -export const Sync = () => { +export const SyncModal = ({ + onCancel, + open, + width = 800, + height = 500, +}: SyncModalProps) => { const t = useTranslation(); const { syncStatus, @@ -79,7 +94,7 @@ export const Sync = () => { numberOfRecordsInPushQueue, isLoading, onManualSync, - } = useHostSync(); + } = useHostSync(open); const { updateUserIsLoading, updateUser } = useAuthContext(); const sync = async () => { @@ -96,63 +111,87 @@ export const Sync = () => { ); return ( - - - - - {t('heading.synchronise-status')} - - - {t('sync-info.summary') - .split('\n') - .map(line => ( -
{line}
- ))} -
- - {numberOfRecordsInPushQueue} - - - - - - - - - - - {DateUtils.formatDuration(durationAsDate)} - - - - - - - - } - variant="contained" - sx={{ fontSize: '12px' }} - disabled={false} - onClick={sync} + { + if (e.key === 'Escape') onCancel(); + }} + > + + } + color="primary" + onClick={onCancel} + sx={{ position: 'absolute', right: 8, top: 8 }} + label={t('button.close')} + /> + + + + - {t('button.sync-now')} - - - + {t('heading.synchronise-status')} + + + {t('sync-info.summary') + .split('\n') + .map(line => ( +
{line}
+ ))} +
+ + {numberOfRecordsInPushQueue} + + + + + + + + + + + {DateUtils.formatDuration(durationAsDate)} + + + + + + + + } + variant="contained" + sx={{ fontSize: '12px' }} + disabled={false} + onClick={sync} + > + {t('button.sync-now')} + + + +
+
- - + ); }; diff --git a/client/packages/host/src/components/Sync/SyncModalContext.ts b/client/packages/host/src/components/Sync/SyncModalContext.ts new file mode 100644 index 0000000000..948e705df8 --- /dev/null +++ b/client/packages/host/src/components/Sync/SyncModalContext.ts @@ -0,0 +1,15 @@ +import { createContext } from 'react'; + +export interface SyncModalState { + open: boolean; +} + +export interface SyncModalControllerState extends SyncModalState { + setState: (state: SyncModalState) => void; + setOpen: (open: boolean) => void; +} + +export const SyncModalContext = createContext( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as any +); diff --git a/client/packages/host/src/components/Sync/SyncModalProvider.tsx b/client/packages/host/src/components/Sync/SyncModalProvider.tsx new file mode 100644 index 0000000000..f3678c7483 --- /dev/null +++ b/client/packages/host/src/components/Sync/SyncModalProvider.tsx @@ -0,0 +1,36 @@ +import React, { FC, useMemo, useState } from 'react'; +import { + SyncModalContext, + SyncModalState, + SyncModalControllerState, +} from './SyncModalContext'; +import { SyncModal } from './SyncModal'; +import { PropsWithChildrenOnly } from '@common/types'; + +export const SyncModalProvider: FC = ({ children }) => { + const [syncModalState, setState] = useState({ + open: false, + }); + const { open } = syncModalState; + + const syncModalController: SyncModalControllerState = useMemo( + () => ({ + setOpen: (open: boolean) => setState(state => ({ ...state, open })), + setState, + ...syncModalState, + }), + [setState, syncModalState] + ); + + return ( + + {children} + { + setState(state => ({ ...state, open: false })); + }} + /> + + ); +}; diff --git a/client/packages/host/src/components/Sync/index.tsx b/client/packages/host/src/components/Sync/index.tsx index 5de2d6d294..2ac31fccdd 100644 --- a/client/packages/host/src/components/Sync/index.tsx +++ b/client/packages/host/src/components/Sync/index.tsx @@ -1 +1,3 @@ -export * from './Sync'; +export * from './SyncModal'; +export * from './useSyncModal'; +export * from './SyncModalProvider'; diff --git a/client/packages/host/src/components/Sync/useSyncModal.ts b/client/packages/host/src/components/Sync/useSyncModal.ts new file mode 100644 index 0000000000..9cff5924be --- /dev/null +++ b/client/packages/host/src/components/Sync/useSyncModal.ts @@ -0,0 +1,12 @@ +import { useContext, useCallback } from 'react'; +import { SyncModalContext } from './SyncModalContext'; + +export const useSyncModal = () => { + const { setOpen } = useContext(SyncModalContext); + + const trigger = () => { + setOpen(true); + }; + + return useCallback(trigger, [setOpen]); +}; diff --git a/client/packages/system/src/Sync/api/hooks/utils/useSyncInfo.ts b/client/packages/system/src/Sync/api/hooks/utils/useSyncInfo.ts index b74384d598..0e0bfa6831 100644 --- a/client/packages/system/src/Sync/api/hooks/utils/useSyncInfo.ts +++ b/client/packages/system/src/Sync/api/hooks/utils/useSyncInfo.ts @@ -1,7 +1,10 @@ import { getAuthCookie, useQuery } from '@openmsupply-client/common'; import { useSyncApi } from './useSyncApi'; -export const useSyncInfo = (refetchInterval: number | false = false) => { +export const useSyncInfo = ( + refetchInterval: number | false = false, + enabled: boolean = true +) => { const api = useSyncApi(); const { token } = getAuthCookie(); @@ -15,7 +18,7 @@ export const useSyncInfo = (refetchInterval: number | false = false) => { () => api.get.syncInfo(token), { refetchInterval, - enabled: !!token, + enabled: !!token && enabled, } );