Loading Web/src/components/layout/navigation/Navbar.tsx +18 −1 Original line number Diff line number Diff line Loading @@ -18,13 +18,15 @@ import { } from '@chakra-ui/react'; import i18next, { t } from 'i18next'; import { FiChevronDown, FiMenu } from 'react-icons/fi'; import { Link, useNavigate } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useApi, useAuth } from '../../../hooks/Caffeine'; import { ColorModeToggle } from '../../utils/ColorModeToggle'; import { KafeAvatar } from '../../utils/KafeAvatar'; import { LanguageToggle } from '../../utils/LanguageToggle'; import { Logo } from '../Logo'; import { MessageButton } from '../MessageButton'; import { useAuthLinkFunction } from '../../../hooks/useAuthLink'; import { ConfirmExitLink } from '../../utils/ConfirmExitLink'; export const NAVBAR_HEIGHT = 20; Loading @@ -36,7 +38,10 @@ interface INavbarProps extends FlexProps { export function Navbar({ onOpen, forceReload, signedIn, ...rest }: INavbarProps) { const { toggleColorMode } = useColorMode(); const { user, setUser } = useAuth(); const location = useLocation(); const navigate = useNavigate(); const authLink = useAuthLinkFunction(); const api = useApi(); const toggleLanguage = () => { Loading Loading @@ -90,6 +95,18 @@ export function Navbar({ onOpen, forceReload, signedIn, ...rest }: INavbarProps) /> )} {signedIn && <Spacer display={{ base: 'flex', md: 'none' }} />} <Flex h="20" alignItems="center" ml={2} mr={8} justifyContent="space-between" key="heading"> <ConfirmExitLink alertCondition={location.pathname.endsWith("edit") || location.pathname.endsWith("create")} destPath={authLink("/")} handleConfirm={() => {}} > <Logo /> </ConfirmExitLink> </Flex> <Spacer /> <HStack spacing={1}> Loading Web/src/components/layout/navigation/Sidebar.tsx +47 −26 Original line number Diff line number Diff line import { BoxProps, CloseButton, Flex, IconButton, Spacer, useColorModeValue, VStack } from '@chakra-ui/react'; import { BoxProps, CloseButton, Flex, IconButton, Spacer, useColorModeValue, VStack, } from '@chakra-ui/react'; import { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; import { IoReader, IoReaderOutline, IoSettingsOutline } from 'react-icons/io5'; import { Link, useMatches } from 'react-router-dom'; import { useLocation, useMatches } from 'react-router-dom'; import { useAuth, useOrganizations } from '../../../hooks/Caffeine'; import { useAuthLink } from '../../../hooks/useAuthLink'; import { useAuthLinkFunction } from '../../../hooks/useAuthLink'; import { useColorScheme } from '../../../hooks/useColorScheme'; import { AppRoute, authRoutes } from '../../../routes'; import { Permission } from '../../../schemas/generic'; Loading @@ -13,6 +21,7 @@ import { OrganizationAvatar } from '../../utils/OrganizationAvatar/OrganizationA import { Footer } from '../Footer'; import { ReportButton } from '../ReportButton'; import { NavItem } from './NavItem'; import { ConfirmExitLink } from '../../utils/ConfirmExitLink'; interface ISidebarProps extends BoxProps { onClose: () => void; Loading @@ -29,6 +38,8 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { const { user } = useAuth(); const routeValues = authRoutes(i18next.t, user, useOrganizations().currentOrganization); const location = useLocation(); const authLink = useAuthLinkFunction(); const match = matches[matches.length - 1].id .split('-') Loading @@ -55,7 +66,11 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { return ( <Fragment key={i}> <Link to={useAuthLink(fullPath)}> <ConfirmExitLink alertCondition={location.pathname.endsWith("edit") || location.pathname.endsWith("create")} destPath={authLink(fullPath)} handleConfirm={() => {}} > <NavItem key={i} icon={ Loading @@ -71,7 +86,7 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { > {route.title} </NavItem> </Link> </ConfirmExitLink> {selected && children && children.length > 0 && ( <Flex direction="column" Loading Loading @@ -128,24 +143,29 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { h="full" > {organizations.map((org, key) => ( <Link to={useAuthLink(undefined, org.id)} <ConfirmExitLink key={key} onClick={() => { alertCondition={location.pathname.endsWith("edit") || location.pathname.endsWith("create")} destPath={authLink(`/`, org.id)} handleConfirm={() => { localStorage.setItem(LS_LATEST_ORG_KEY, org.id); forceReload(); }} forceReload();} } > <OrganizationAvatar organization={org} size="sm" cursor="pointer" /> </Link> </ConfirmExitLink> ))} <Spacer /> {(['read', 'append', 'inspect', 'write', 'all'] as Permission[]).some((perm) => user?.permissions['system']?.includes(perm), ) && ( <Link to={useAuthLink('/system')}> <ConfirmExitLink alertCondition={location.pathname.endsWith("edit") || location.pathname.endsWith("create")} destPath={authLink('/system')} handleConfirm={() => {}} > <IconButton aria-label="Settings" icon={<IoSettingsOutline />} borderRadius="full" /> </Link> </ConfirmExitLink> )} </VStack> <Flex Loading Loading @@ -174,3 +194,4 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { </Flex> ); } Web/src/components/utils/ConfirmExitLink.tsx 0 → 100644 +44 −0 Original line number Diff line number Diff line import { Link, useNavigate } from 'react-router-dom'; import { LeavePageAlert } from './LeavePageAlert'; import { useDisclosure } from '@chakra-ui/react'; interface ILeavePageAlertProps { alertCondition: boolean; destPath: string; handleConfirm: () => void; children: React.ReactNode; } export function ConfirmExitLink({ alertCondition, destPath, handleConfirm, children }: ILeavePageAlertProps) { const { isOpen, onOpen, onClose } = useDisclosure() const navigate = useNavigate(); return ( <Link to={destPath} onClick={(e) => { if (alertCondition) { e.preventDefault(); onOpen(); } else { handleConfirm(); } }} > <LeavePageAlert isOpen={isOpen} onClose={onClose} handleConfirm={() => { navigate(destPath); handleConfirm(); }} /> {children} </Link> ) } No newline at end of file Web/src/components/utils/LeavePageAlert.tsx 0 → 100644 +52 −0 Original line number Diff line number Diff line import { BoxProps, AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, Button, } from '@chakra-ui/react'; import { createRef } from 'react'; import { t } from 'i18next'; interface ILeavePageAlertProps extends BoxProps { isOpen: boolean; onClose: () => void; handleConfirm: () => void; } export function LeavePageAlert({ isOpen, onClose, handleConfirm }: ILeavePageAlertProps) { const cancelRef = createRef<HTMLButtonElement>(); return ( <AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}> <AlertDialogOverlay> <AlertDialogContent> <AlertDialogHeader fontSize="lg" fontWeight="bold"> {t('error.leavePageTitle').toString()} </AlertDialogHeader> <AlertDialogBody>{t('error.leavePageWarn').toString()}</AlertDialogBody> <AlertDialogFooter> <Button ref={cancelRef} onClick={onClose}> {t('generic.cancel').toString()} </Button> <Button colorScheme="red" onClick={() => { handleConfirm(); onClose(); }} ml={3} > {t('generic.ok').toString()} </Button> </AlertDialogFooter> </AlertDialogContent> </AlertDialogOverlay> </AlertDialog> ) } No newline at end of file Web/src/translations/cs/common.json +4 −1 Original line number Diff line number Diff line Loading @@ -138,10 +138,13 @@ "projectDoesNotExist": "Projekt s daným ID neexistuje", "subtitle": "Omlouváme se za potíže. Pokud máte pocit, že se to nemělo stát (což pravděpodobně nemělo), pošlete nám prosím tento report", "title": "Jejky, tak to nešlo podle plánu", "tokenInvalid": "Token není platný" "tokenInvalid": "Token není platný", "leavePageTitle": "Neuložené změny", "leavePageWarn": "Můžete mít neuložené změny, opravdu chcete stránku opustit?" }, "generic": { "cancel": "Zrušit", "ok": "OK", "edit": "Upravit", "empty": "Kde nic, tu nic...", "for": "pro", Loading Loading
Web/src/components/layout/navigation/Navbar.tsx +18 −1 Original line number Diff line number Diff line Loading @@ -18,13 +18,15 @@ import { } from '@chakra-ui/react'; import i18next, { t } from 'i18next'; import { FiChevronDown, FiMenu } from 'react-icons/fi'; import { Link, useNavigate } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useApi, useAuth } from '../../../hooks/Caffeine'; import { ColorModeToggle } from '../../utils/ColorModeToggle'; import { KafeAvatar } from '../../utils/KafeAvatar'; import { LanguageToggle } from '../../utils/LanguageToggle'; import { Logo } from '../Logo'; import { MessageButton } from '../MessageButton'; import { useAuthLinkFunction } from '../../../hooks/useAuthLink'; import { ConfirmExitLink } from '../../utils/ConfirmExitLink'; export const NAVBAR_HEIGHT = 20; Loading @@ -36,7 +38,10 @@ interface INavbarProps extends FlexProps { export function Navbar({ onOpen, forceReload, signedIn, ...rest }: INavbarProps) { const { toggleColorMode } = useColorMode(); const { user, setUser } = useAuth(); const location = useLocation(); const navigate = useNavigate(); const authLink = useAuthLinkFunction(); const api = useApi(); const toggleLanguage = () => { Loading Loading @@ -90,6 +95,18 @@ export function Navbar({ onOpen, forceReload, signedIn, ...rest }: INavbarProps) /> )} {signedIn && <Spacer display={{ base: 'flex', md: 'none' }} />} <Flex h="20" alignItems="center" ml={2} mr={8} justifyContent="space-between" key="heading"> <ConfirmExitLink alertCondition={location.pathname.endsWith("edit") || location.pathname.endsWith("create")} destPath={authLink("/")} handleConfirm={() => {}} > <Logo /> </ConfirmExitLink> </Flex> <Spacer /> <HStack spacing={1}> Loading
Web/src/components/layout/navigation/Sidebar.tsx +47 −26 Original line number Diff line number Diff line import { BoxProps, CloseButton, Flex, IconButton, Spacer, useColorModeValue, VStack } from '@chakra-ui/react'; import { BoxProps, CloseButton, Flex, IconButton, Spacer, useColorModeValue, VStack, } from '@chakra-ui/react'; import { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; import { IoReader, IoReaderOutline, IoSettingsOutline } from 'react-icons/io5'; import { Link, useMatches } from 'react-router-dom'; import { useLocation, useMatches } from 'react-router-dom'; import { useAuth, useOrganizations } from '../../../hooks/Caffeine'; import { useAuthLink } from '../../../hooks/useAuthLink'; import { useAuthLinkFunction } from '../../../hooks/useAuthLink'; import { useColorScheme } from '../../../hooks/useColorScheme'; import { AppRoute, authRoutes } from '../../../routes'; import { Permission } from '../../../schemas/generic'; Loading @@ -13,6 +21,7 @@ import { OrganizationAvatar } from '../../utils/OrganizationAvatar/OrganizationA import { Footer } from '../Footer'; import { ReportButton } from '../ReportButton'; import { NavItem } from './NavItem'; import { ConfirmExitLink } from '../../utils/ConfirmExitLink'; interface ISidebarProps extends BoxProps { onClose: () => void; Loading @@ -29,6 +38,8 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { const { user } = useAuth(); const routeValues = authRoutes(i18next.t, user, useOrganizations().currentOrganization); const location = useLocation(); const authLink = useAuthLinkFunction(); const match = matches[matches.length - 1].id .split('-') Loading @@ -55,7 +66,11 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { return ( <Fragment key={i}> <Link to={useAuthLink(fullPath)}> <ConfirmExitLink alertCondition={location.pathname.endsWith("edit") || location.pathname.endsWith("create")} destPath={authLink(fullPath)} handleConfirm={() => {}} > <NavItem key={i} icon={ Loading @@ -71,7 +86,7 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { > {route.title} </NavItem> </Link> </ConfirmExitLink> {selected && children && children.length > 0 && ( <Flex direction="column" Loading Loading @@ -128,24 +143,29 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { h="full" > {organizations.map((org, key) => ( <Link to={useAuthLink(undefined, org.id)} <ConfirmExitLink key={key} onClick={() => { alertCondition={location.pathname.endsWith("edit") || location.pathname.endsWith("create")} destPath={authLink(`/`, org.id)} handleConfirm={() => { localStorage.setItem(LS_LATEST_ORG_KEY, org.id); forceReload(); }} forceReload();} } > <OrganizationAvatar organization={org} size="sm" cursor="pointer" /> </Link> </ConfirmExitLink> ))} <Spacer /> {(['read', 'append', 'inspect', 'write', 'all'] as Permission[]).some((perm) => user?.permissions['system']?.includes(perm), ) && ( <Link to={useAuthLink('/system')}> <ConfirmExitLink alertCondition={location.pathname.endsWith("edit") || location.pathname.endsWith("create")} destPath={authLink('/system')} handleConfirm={() => {}} > <IconButton aria-label="Settings" icon={<IoSettingsOutline />} borderRadius="full" /> </Link> </ConfirmExitLink> )} </VStack> <Flex Loading Loading @@ -174,3 +194,4 @@ export function Sidebar({ onClose, forceReload, ...rest }: ISidebarProps) { </Flex> ); }
Web/src/components/utils/ConfirmExitLink.tsx 0 → 100644 +44 −0 Original line number Diff line number Diff line import { Link, useNavigate } from 'react-router-dom'; import { LeavePageAlert } from './LeavePageAlert'; import { useDisclosure } from '@chakra-ui/react'; interface ILeavePageAlertProps { alertCondition: boolean; destPath: string; handleConfirm: () => void; children: React.ReactNode; } export function ConfirmExitLink({ alertCondition, destPath, handleConfirm, children }: ILeavePageAlertProps) { const { isOpen, onOpen, onClose } = useDisclosure() const navigate = useNavigate(); return ( <Link to={destPath} onClick={(e) => { if (alertCondition) { e.preventDefault(); onOpen(); } else { handleConfirm(); } }} > <LeavePageAlert isOpen={isOpen} onClose={onClose} handleConfirm={() => { navigate(destPath); handleConfirm(); }} /> {children} </Link> ) } No newline at end of file
Web/src/components/utils/LeavePageAlert.tsx 0 → 100644 +52 −0 Original line number Diff line number Diff line import { BoxProps, AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, Button, } from '@chakra-ui/react'; import { createRef } from 'react'; import { t } from 'i18next'; interface ILeavePageAlertProps extends BoxProps { isOpen: boolean; onClose: () => void; handleConfirm: () => void; } export function LeavePageAlert({ isOpen, onClose, handleConfirm }: ILeavePageAlertProps) { const cancelRef = createRef<HTMLButtonElement>(); return ( <AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}> <AlertDialogOverlay> <AlertDialogContent> <AlertDialogHeader fontSize="lg" fontWeight="bold"> {t('error.leavePageTitle').toString()} </AlertDialogHeader> <AlertDialogBody>{t('error.leavePageWarn').toString()}</AlertDialogBody> <AlertDialogFooter> <Button ref={cancelRef} onClick={onClose}> {t('generic.cancel').toString()} </Button> <Button colorScheme="red" onClick={() => { handleConfirm(); onClose(); }} ml={3} > {t('generic.ok').toString()} </Button> </AlertDialogFooter> </AlertDialogContent> </AlertDialogOverlay> </AlertDialog> ) } No newline at end of file
Web/src/translations/cs/common.json +4 −1 Original line number Diff line number Diff line Loading @@ -138,10 +138,13 @@ "projectDoesNotExist": "Projekt s daným ID neexistuje", "subtitle": "Omlouváme se za potíže. Pokud máte pocit, že se to nemělo stát (což pravděpodobně nemělo), pošlete nám prosím tento report", "title": "Jejky, tak to nešlo podle plánu", "tokenInvalid": "Token není platný" "tokenInvalid": "Token není platný", "leavePageTitle": "Neuložené změny", "leavePageWarn": "Můžete mít neuložené změny, opravdu chcete stránku opustit?" }, "generic": { "cancel": "Zrušit", "ok": "OK", "edit": "Upravit", "empty": "Kde nic, tu nic...", "for": "pro", Loading