Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions frontend/src/components/DeviceOptionMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState } from 'react'
import { PROTOCOL } from '../constants'
import { Dispatch } from '../store'
import { useDispatch } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { Link, useParams, useHistory } from 'react-router-dom'
import { Divider, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material'
import { Divider, Menu, MenuItem, ListItemIcon, ListItemText, Tooltip } from '@mui/material'
import { selectPermissions } from '../selectors/organizations'
import { DeleteServiceMenuItem } from '../buttons/DeleteServiceMenuItem'
import { ListItemLocation } from './ListItemLocation'
import { CopyMenuItem } from './CopyMenuItem'
Expand All @@ -24,6 +25,7 @@ export const DeviceOptionMenu: React.FC<Props> = ({ device, service }) => {
const handleClick = event => setAnchorEl(event.currentTarget)
const handleClose = () => setAnchorEl(null)
const dispatch = useDispatch<Dispatch>()
const admin = useSelector(selectPermissions).includes('ADMIN')
const { t } = useTranslation()
const devicesSection = !!deviceID
const deviceOnly = device && !service
Expand All @@ -45,6 +47,17 @@ export const DeviceOptionMenu: React.FC<Props> = ({ device, service }) => {
if (product) history.push(`/products/${product.id}/details`)
}

// Only wrapped in a tooltip when disabled — MenuList arrow-key traversal skips a
// span-wrapped item, so an enabled one has to stay a direct child of the Menu.
const makeProductItem = (
<MenuItem dense key="makeProduct" disabled={!admin} onClick={handleMakeProduct}>
<ListItemIcon>
<Icon name="conveyor-belt-boxes" size="md" />
</ListItemIcon>
<ListItemText primary={t('deviceOptionMenu.makeProduct', 'Make Product')} />
</MenuItem>
)

return (
<>
<MobileUI hide>{!devicesSection && <InfoButton device={device} service={service} />}</MobileUI>
Expand Down Expand Up @@ -123,12 +136,18 @@ export const DeviceOptionMenu: React.FC<Props> = ({ device, service }) => {
</ListItemIcon>
<ListItemText primary={t('deviceOptionMenu.transferDevice', 'Transfer Device')} />
</MenuItem>,
<MenuItem dense key="makeProduct" onClick={handleMakeProduct}>
<ListItemIcon>
<Icon name="conveyor-belt-boxes" size="md" />
</ListItemIcon>
<ListItemText primary={t('deviceOptionMenu.makeProduct', 'Make Product')} />
</MenuItem>,
admin ? (
makeProductItem
) : (
<Tooltip
key="makeProduct"
placement="left"
title={t('deviceOptionMenu.adminRequired', 'Admin permissions required')}
arrow
>
<span>{makeProductItem}</span>
</Tooltip>
),
]}
{device.permissions.includes('MANAGE') &&
devicesSection &&
Expand All @@ -140,7 +159,13 @@ export const DeviceOptionMenu: React.FC<Props> = ({ device, service }) => {
service &&
devicesSection && [
<Divider key="divider" />,
<DeleteServiceMenuItem key="deleteService" device={device} service={service} onClick={handleClose} onCancel={handleClose} />,
<DeleteServiceMenuItem
key="deleteService"
device={device}
service={service}
onClick={handleClose}
onCancel={handleClose}
/>,
]}
<LeaveDevice key="leaveDevice" device={device} menuItem />
</Menu>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export const Header: React.FC<Props> = ({ panels = 1 }) => {
<HeaderDeviceOptionMenu />
</Route>
)}
<Route path="/products" exact>
<Route path={['/products', '/products/select']} exact>
<ProductsHeaderButtons />
</Route>
</>
Expand Down
54 changes: 28 additions & 26 deletions frontend/src/components/Header/ProductsHeaderButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,48 @@
import React from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { Button } from '@mui/material'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Button, Tooltip } from '@mui/material'
import { IconButton } from '../../buttons/IconButton'
import { selectPermissions } from '../../selectors/organizations'
import { getHasProducts } from '../../selectors/products'
import { Icon } from '../Icon'

export const ProductsHeaderButtons: React.FC = () => {
const history = useHistory()
const location = useLocation()

const searchParams = new URLSearchParams(location.search)
const isSelectMode = searchParams.get('select') === 'true'
const hasProducts = useSelector(getHasProducts)
const permissions = useSelector(selectPermissions)
const { t } = useTranslation()

const toggleSelect = () => {
const newParams = new URLSearchParams(location.search)
if (isSelectMode) {
newParams.delete('select')
} else {
newParams.set('select', 'true')
}
const search = newParams.toString()
history.push(`${location.pathname}${search ? `?${search}` : ''}`)
}
const admin = permissions.includes('ADMIN')
const isSelectMode = location.pathname === '/products/select'
const adminRequired = admin ? '' : t('productsHeaderButtons.adminRequired', 'Admin permissions required')

return (
<>
<IconButton
onClick={toggleSelect}
hide={!hasProducts || !admin}
to={isSelectMode ? '/products' : '/products/select'}
icon="check-square"
type={isSelectMode ? 'solid' : 'regular'}
color={isSelectMode ? 'primary' : undefined}
title={isSelectMode ? 'Hide Select' : 'Show Select'}
title={isSelectMode ? t('header.hideSelect', 'Hide Select') : t('header.showSelect', 'Show Select')}
/>
<Button
size="small"
variant="contained"
color="primary"
onClick={() => history.push('/products/add')}
startIcon={<Icon name="plus" />}
>
Create
</Button>
<Tooltip title={adminRequired} placement="top" arrow>
<span>
<Button
size="small"
variant="contained"
color="primary"
disabled={!admin}
onClick={() => history.push('/products/add')}
startIcon={<Icon name="plus" />}
>
{t('header.create', 'Create')}
</Button>
</span>
</Tooltip>
</>
)
}

58 changes: 36 additions & 22 deletions frontend/src/components/ProductOptionMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, useHistory } from 'react-router-dom'
import { Divider, ListItemIcon, ListItemText, Menu, MenuItem, Typography } from '@mui/material'
import { useSelector } from 'react-redux'
import { Divider, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip, Typography } from '@mui/material'
import { IDeviceProduct } from '../models/products'
import { IconButton } from '../buttons/IconButton'
import { selectPermissions } from '../selectors/organizations'
import { Icon } from './Icon'
import { DeleteButton } from '../buttons/DeleteButton'
import { Notice } from './Notice'
Expand All @@ -15,11 +17,37 @@ export const ProductOptionMenu: React.FC<Props> = ({ product }) => {
const { t } = useTranslation()
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null)
const history = useHistory()
const admin = useSelector(selectPermissions).includes('ADMIN')
const handleClick = (event: React.MouseEvent<Element>) => setAnchorEl(event.currentTarget as HTMLButtonElement)
const handleClose = () => setAnchorEl(null)

if (!product) return null

const deleteButton = (
<DeleteButton
menuItem
disabled={!admin}
title={t('productOptionMenu.deleteProduct', 'Delete Product')}
onCancel={handleClose}
onDelete={async () => {
handleClose()
const success = await dispatch.products.delete(product.id)
if (success) history.push('/products')
}}
warning={
<>
<Notice severity="error" gutterBottom fullWidth>
{t('productOptionMenu.cannotBeUndone', 'This action cannot be undone.')}
</Notice>
<Typography variant="body2">
{t('productOptionMenu.deleteConfirmPrefix', 'Are you sure you want to permanently delete the product')}{' '}
<b>{product.name}</b> {t('productOptionMenu.deleteConfirmSuffix', 'and all its services?')}
</Typography>
</>
}
/>
)

return (
<>
<IconButton onClick={handleClick} name="ellipsis-v" size="md" color="grayDarker" fixedWidth />
Expand All @@ -40,27 +68,13 @@ export const ProductOptionMenu: React.FC<Props> = ({ product }) => {
<ListItemText primary={t('productOptionMenu.transferProduct', 'Transfer Product')} />
</MenuItem>
<Divider />
<DeleteButton
menuItem
title={t('productOptionMenu.deleteProduct', 'Delete Product')}
onCancel={handleClose}
onDelete={async () => {
handleClose()
const success = await dispatch.products.delete(product.id)
if (success) history.push('/products')
}}
warning={
<>
<Notice severity="error" gutterBottom fullWidth>
{t('productOptionMenu.cannotBeUndone', 'This action cannot be undone.')}
</Notice>
<Typography variant="body2">
{t('productOptionMenu.deleteConfirmPrefix', 'Are you sure you want to permanently delete the product')}{' '}
<b>{product.name}</b> {t('productOptionMenu.deleteConfirmSuffix', 'and all its services?')}
</Typography>
</>
}
/>
{admin ? (
deleteButton
) : (
<Tooltip placement="left" title={t('productOptionMenu.adminRequired', 'Admin permissions required')} arrow>
<span>{deleteButton}</span>
</Tooltip>
)}
</Menu>
</>
)
Expand Down
22 changes: 11 additions & 11 deletions frontend/src/components/ProductsActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState } from 'react'
import { MOBILE_WIDTH } from '../constants'
import { useMediaQuery, Box, Typography, Collapse } from '@mui/material'
import { useSelector } from 'react-redux'
import { useHistory, useLocation } from 'react-router-dom'
import { useHistory } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { ConfirmIconButton } from '../buttons/ConfirmIconButton'
import { IconButton } from '../buttons/IconButton'
Expand All @@ -12,25 +12,21 @@ import { Title } from './Title'
import { Icon } from './Icon'
import { spacing, radius } from '../styling'
import { getProductsSelected } from '../selectors/products'
import { selectPermissions } from '../selectors/organizations'

type Props = {
select?: boolean
}

export const ProductsActionBar: React.FC<Props> = ({ select }) => {
const selected = useSelector(getProductsSelected)
const admin = useSelector(selectPermissions).includes('ADMIN')
const [deleting, setDeleting] = useState(false)
const mobile = useMediaQuery(`(max-width:${MOBILE_WIDTH}px)`)
const history = useHistory()
const location = useLocation()
const { t } = useTranslation()

const clearSelectMode = () => {
const newParams = new URLSearchParams(location.search)
newParams.delete('select')
const search = newParams.toString()
history.push(`${location.pathname}${search ? `?${search}` : ''}`)
}
const clearSelectMode = () => history.push('/products')

const handleDelete = async () => {
setDeleting(true)
Expand Down Expand Up @@ -73,10 +69,15 @@ export const ProductsActionBar: React.FC<Props> = ({ select }) => {
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<ConfirmIconButton
icon="trash"
title={t('productsActionBar.deleteSelected', 'Delete selected')}
title={
admin
? t('productsActionBar.deleteSelected', 'Delete selected')
: t('productsActionBar.adminRequired', 'Admin permissions required')
}
color="alwaysWhite"
placement="bottom"
disabled={!selected.length}
forceTitle
disabled={!admin || !selected.length}
loading={deleting}
onClick={handleDelete}
confirmProps={{
Expand Down Expand Up @@ -119,4 +120,3 @@ export const ProductsActionBar: React.FC<Props> = ({ select }) => {
</Collapse>
)
}

14 changes: 14 additions & 0 deletions frontend/src/i18n/locales/de/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@
"label": "Gerätename"
},
"deviceOptionMenu": {
"adminRequired": "",
"deviceDetails": "Gerätedetails",
"deviceLink": "Gerätelink",
"makeProduct": "Produkt erstellen",
Expand Down Expand Up @@ -774,6 +775,7 @@
},
"header": {
"back": "Zurück",
"create": "",
"deviceSearch": "Gerätesuche",
"hideSelect": "Auswahl ausblenden",
"products": "Produkte",
Expand Down Expand Up @@ -1424,6 +1426,7 @@
"localPort": "Lokaler Port"
},
"productAddPage": {
"adminRequired": "",
"createFailed": "Erstellen des Produkts fehlgeschlagen",
"creating": "Wird erstellt...",
"nameRequired": "Produktname ist erforderlich",
Expand All @@ -1433,6 +1436,7 @@
"title": "Produkt erstellen"
},
"productOptionMenu": {
"adminRequired": "",
"cannotBeUndone": "Diese Aktion kann nicht rückgängig gemacht werden.",
"deleteConfirmPrefix": "Möchten Sie das Produkt wirklich dauerhaft löschen",
"deleteConfirmSuffix": "und alle zugehörigen Dienste?",
Expand All @@ -1450,6 +1454,7 @@
"unknown": "Unbekannt"
},
"productsActionBar": {
"adminRequired": "",
"clearSelection": "Auswahl aufheben",
"confirmAction_one": "{{count}} Produkt löschen",
"confirmAction_other": "{{count}} Produkte löschen",
Expand All @@ -1459,6 +1464,15 @@
"deleteSelected": "Auswahl löschen",
"selected": "Ausgewählt"
},
"productsHeaderButtons": {
"adminRequired": ""
},
"productsPage": {
"createFirst": "",
"emptyHelp": "",
"emptyHelpReadOnly": "",
"loading": ""
},
"productServiceAddPage": {
"failedToAddService": "Dienst konnte nicht hinzugefügt werden",
"newService": "Neuer Dienst",
Expand Down
Loading
Loading