Add labels support.

This commit is contained in:
Matias Arriola 2022-02-11 21:23:06 +00:00 committed by Paulo Veiga
parent 3725188f8f
commit d4b37f4139
14 changed files with 372 additions and 321 deletions

View File

@ -4,20 +4,15 @@ import { useMutation, useQueryClient } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import Client from '../../../../classes/client'; import Client from '../../../../classes/client';
import { activeInstance } from '../../../../redux/clientSlice'; import { activeInstance } from '../../../../redux/clientSlice';
import { handleOnMutationSuccess } from '..'; import { handleOnMutationSuccess, MultiDialogProps } from '..';
import BaseDialog from '../base-dialog'; import BaseDialog from '../base-dialog';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle'; import AlertTitle from '@mui/material/AlertTitle';
export type DeleteMultiselectDialogProps = {
mapsId: number[];
onClose: () => void;
};
const DeleteMultiselectDialog = ({ const DeleteMultiselectDialog = ({
onClose, onClose,
mapsId, mapsId,
}: DeleteMultiselectDialogProps): React.ReactElement => { }: MultiDialogProps): React.ReactElement => {
const intl = useIntl(); const intl = useIntl();
const client: Client = useSelector(activeInstance); const client: Client = useSelector(activeInstance);
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

@ -62,7 +62,7 @@ const ActionDispatcher = ({ mapsId, action, onClose, fromEditor }: ActionDialogP
<ExportDialog onClose={handleOnClose} mapId={mapsId[0]} enableImgExport={fromEditor} /> <ExportDialog onClose={handleOnClose} mapId={mapsId[0]} enableImgExport={fromEditor} />
)} )}
{action === 'share' && <ShareDialog onClose={handleOnClose} mapId={mapsId[0]} />} {action === 'share' && <ShareDialog onClose={handleOnClose} mapId={mapsId[0]} />}
{action === 'label' && <LabelDialog onClose={handleOnClose} mapId={mapsId[0]} />} {action === 'label' && <LabelDialog onClose={handleOnClose} mapsId={mapsId} />}
</span> </span>
); );
}; };
@ -81,4 +81,9 @@ export type SimpleDialogProps = {
onClose: () => void; onClose: () => void;
}; };
export type MultiDialogProps = {
mapsId: number[];
onClose: () => void;
};
export default ActionDispatcher; export default ActionDispatcher;

View File

@ -1,16 +1,19 @@
import React from 'react'; import React from 'react';
import BaseDialog from '../base-dialog';
import { SimpleDialogProps } from '..';
import { useIntl } from 'react-intl';
import Client, { ErrorInfo, Label, MapInfo } from '../../../../classes/client';
import { useStyles } from './style';
import { LabelSelector } from '../../maps-list/label-selector';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { FormattedMessage, useIntl } from 'react-intl';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import Typography from '@mui/material/Typography';
import { useStyles } from './style';
import { MultiDialogProps } from '..';
import BaseDialog from '../base-dialog';
import Client, { ErrorInfo, Label, MapInfo } from '../../../../classes/client';
import { LabelSelector } from '../../maps-list/label-selector';
import { activeInstance } from '../../../../redux/clientSlice'; import { activeInstance } from '../../../../redux/clientSlice';
import { ChangeLabelMutationFunctionParam, getChangeLabelMutationFunction } from '../../maps-list';
const LabelDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement => { const LabelDialog = ({ mapsId, onClose }: MultiDialogProps): React.ReactElement => {
const intl = useIntl(); const intl = useIntl();
const classes = useStyles(); const classes = useStyles();
const client: Client = useSelector(activeInstance); const client: Client = useSelector(activeInstance);
@ -21,19 +24,10 @@ const LabelDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
return client.fetchAllMaps(); return client.fetchAllMaps();
}); });
const map = data.find(m => m.id === mapId); const maps = data.filter(m => mapsId.includes(m.id));
const changeLabelMutation = useMutation<void, ErrorInfo, { label: Label, checked: boolean }, number>( const changeLabelMutation = useMutation<void, ErrorInfo, ChangeLabelMutationFunctionParam, number>(
async ({ label, checked }) => { getChangeLabelMutationFunction(client),
if (!label.id) {
label.id = await client.createLabel(label.title, label.color);
}
if (checked){
return client.addLabelToMap(label.id, mapId);
} else {
return client.deleteLabelFromMap(label.id, mapId);
}
},
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries('maps'); queryClient.invalidateQueries('maps');
@ -47,6 +41,7 @@ const LabelDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
const handleChangesInLabels = (label: Label, checked: boolean) => { const handleChangesInLabels = (label: Label, checked: boolean) => {
changeLabelMutation.mutate({ changeLabelMutation.mutate({
maps,
label, label,
checked checked
}); });
@ -63,11 +58,17 @@ const LabelDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
description={intl.formatMessage({ description={intl.formatMessage({
id: 'label.description', id: 'label.description',
defaultMessage: defaultMessage:
'Use labels to organize your maps', 'Use labels to organize your maps.',
})} })}
PaperProps={{ classes: { root: classes.paper } }} PaperProps={{ classes: { root: classes.paper } }}
> >
<LabelSelector onChange={handleChangesInLabels} maps={[map]} /> <>
<Typography variant="body2" marginTop="10px">
<FormattedMessage id="label.add-for" defaultMessage="Editing labels for maps: " />
{ maps.map(m => m.title).join(', ') }
</Typography>
<LabelSelector onChange={handleChangesInLabels} maps={maps} />
</>
</BaseDialog> </BaseDialog>
</div>); </div>);
}; };

View File

@ -40,6 +40,7 @@ import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import logoIcon from './logo-small.svg'; import logoIcon from './logo-small.svg';
import poweredByIcon from './pwrdby-white.svg'; import poweredByIcon from './pwrdby-white.svg';
import LabelDeleteConfirm from './maps-list/label-delete-confirm';
export type Filter = GenericFilter | LabelFilter; export type Filter = GenericFilter | LabelFilter;
@ -64,7 +65,7 @@ const MapsPage = (): ReactElement => {
const client: Client = useSelector(activeInstance); const client: Client = useSelector(activeInstance);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [activeDialog, setActiveDialog] = React.useState<ActionType | undefined>(undefined); const [activeDialog, setActiveDialog] = React.useState<ActionType | undefined>(undefined);
const [labelToDelete, setLabelToDelete] = React.useState<number | null>(null);
// Reload based on user preference ... // Reload based on user preference ...
const userLocale = AppI18n.getUserLocale(); const userLocale = AppI18n.getUserLocale();
@ -239,7 +240,7 @@ const MapsPage = (): ReactElement => {
filter={buttonInfo.filter} filter={buttonInfo.filter}
active={filter} active={filter}
onClick={handleMenuClick} onClick={handleMenuClick}
onDelete={handleLabelDelete} onDelete={setLabelToDelete}
key={`${buttonInfo.filter.type}:${buttonInfo.label}`} key={`${buttonInfo.filter.type}:${buttonInfo.label}`}
/> />
); );
@ -260,6 +261,14 @@ const MapsPage = (): ReactElement => {
<MapsList filter={filter} /> <MapsList filter={filter} />
</main> </main>
</div> </div>
{ labelToDelete && <LabelDeleteConfirm
onClose={() => setLabelToDelete(null)}
onConfirm={() => {
handleLabelDelete(labelToDelete);
setLabelToDelete(null);
}}
label={labels.find(l => l.id === labelToDelete)}
/> }
</IntlProvider> </IntlProvider>
); );
}; };

View File

@ -1,72 +0,0 @@
import React from 'react';
import Popover from '@mui/material/Popover';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
import { FormattedMessage, useIntl } from 'react-intl';
import { Label, MapInfo } from '../../../../classes/client';
import { LabelSelector } from '../label-selector';
type AddLabelButtonTypes = {
maps: MapInfo[];
onChange: (label: Label, checked: boolean) => void;
};
export function AddLabelButton({ onChange, maps }: AddLabelButtonTypes): React.ReactElement {
const intl = useIntl();
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'add-label-popover' : undefined;
return (
<>
<Tooltip
arrow={true}
title={intl.formatMessage({
id: 'map.tooltip-add',
defaultMessage: 'Add label to selected',
})}
>
<Button
color="primary"
size="medium"
variant="outlined"
type="button"
style={{ marginLeft: '10px' }}
disableElevation={true}
startIcon={<LabelTwoTone />}
onClick={handleClick}
>
<FormattedMessage id="action.label" defaultMessage="Add Label" />
</Button>
</Tooltip>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<LabelSelector onChange={onChange} maps={maps} />
</Popover>
</>
);
}

View File

@ -0,0 +1,94 @@
import React from 'react';
import { useIntl } from 'react-intl';
import AddIcon from '@mui/icons-material/Add';
import TextField from '@mui/material/TextField';
import { Label } from '../../../../classes/client';
import { StyledButton, NewLabelContainer, NewLabelColor, CreateLabel } from './styled';
import { Tooltip } from '@mui/material';
const labelColors = [
'#00b327',
'#0565ff',
'#2d2dd6',
'#6a00ba',
'#ad1599',
'#ff1e35',
'#ff6600',
'#ffff47',
];
type AddLabelFormProps = {
onAdd: (newLabel: Label) => void;
};
export default function AddLabelForm({ onAdd }: AddLabelFormProps): React.ReactElement {
const intl = useIntl();
const [createLabelColorIndex, setCreateLabelColorIndex] = React.useState(
Math.floor(Math.random() * labelColors.length)
);
const [newLabelTitle, setNewLabelTitle] = React.useState('');
const newLabelColor = labelColors[createLabelColorIndex];
const setNextLabelColorIndex = () => {
const nextIndex = labelColors[createLabelColorIndex + 1] ? createLabelColorIndex + 1 : 0;
setCreateLabelColorIndex(nextIndex);
};
const handleSubmitNew = () => {
onAdd({
title: newLabelTitle,
color: newLabelColor,
id: 0,
});
setNewLabelTitle('');
setNextLabelColorIndex();
};
return (
<CreateLabel>
<NewLabelContainer>
<Tooltip
arrow={true}
title={intl.formatMessage({
id: 'label.change-color',
defaultMessage: 'Change label color',
})}
>
<NewLabelColor
htmlColor={newLabelColor}
onClick={(e) => {
e.stopPropagation();
setNextLabelColorIndex();
}}
/>
</Tooltip>
<TextField
variant="standard"
label={intl.formatMessage({
id: 'label.add-placeholder',
defaultMessage: 'Label title',
})}
onChange={(e) => setNewLabelTitle(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSubmitNew();
}
}}
value={newLabelTitle}
/>
<StyledButton
onClick={() => handleSubmitNew()}
disabled={!newLabelTitle.length}
aria-label={intl.formatMessage({
id: 'label.add-button',
defaultMessage: 'Add label',
})}
>
<AddIcon />
</StyledButton>
</NewLabelContainer>
</CreateLabel>
);
}

View File

@ -0,0 +1,25 @@
import styled from 'styled-components';
import IconButton from '@mui/material/IconButton';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
export const StyledButton = styled(IconButton)`
margin: 4px;
`;
export const NewLabelContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`;
export const NewLabelColor = styled(LabelTwoTone)`
margin-right: 12px;
cursor: pointer;
`;
export const CreateLabel = styled.div`
padding-top: 10px;
display: flex;
flex-direction: column;
justify-content: flex-end;
`;

View File

@ -33,13 +33,13 @@ import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import StarRateRoundedIcon from '@mui/icons-material/StarRateRounded'; import StarRateRoundedIcon from '@mui/icons-material/StarRateRounded';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import { AddLabelButton } from './add-label-button';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import { LabelsCell } from './labels-cell'; import { LabelsCell } from './labels-cell';
import LocalizedFormat from 'dayjs/plugin/localizedFormat'; import LocalizedFormat from 'dayjs/plugin/localizedFormat';
import AppI18n from '../../../classes/app-i18n'; import AppI18n from '../../../classes/app-i18n';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
dayjs.extend(LocalizedFormat) dayjs.extend(LocalizedFormat);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) { function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
@ -59,9 +59,9 @@ function getComparator<Key extends keyof any>(
order: Order, order: Order,
orderBy: Key orderBy: Key
): ( ): (
a: { [key in Key]: number | string | boolean | Label[] | undefined }, a: { [key in Key]: number | string | boolean | Label[] | undefined },
b: { [key in Key]: number | string | Label[] | boolean } b: { [key in Key]: number | string | Label[] | boolean }
) => number { ) => number {
return order === 'desc' return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy) ? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy); : (a, b) => -descendingComparator(a, b, orderBy);
@ -236,6 +236,24 @@ const mapsFilter = (filter: Filter, search: string): ((mapInfo: MapInfo) => bool
}; };
}; };
export type ChangeLabelMutationFunctionParam = { maps: MapInfo[]; label: Label; checked: boolean };
export const getChangeLabelMutationFunction =
(client: Client) =>
async ({ maps, label, checked }: ChangeLabelMutationFunctionParam): Promise<void> => {
if (!label.id) {
label.id = await client.createLabel(label.title, label.color);
}
if (checked) {
const toAdd = maps.filter((m) => !m.labels.find((l) => l.id === label.id));
await Promise.all(toAdd.map((m) => client.addLabelToMap(label.id, m.id)));
} else {
const toRemove = maps.filter((m) => m.labels.find((l) => l.id === label.id));
await Promise.all(toRemove.map((m) => client.deleteLabelFromMap(label.id, m.id)));
}
return Promise.resolve();
};
export const MapsList = (props: MapsListProps): React.ReactElement => { export const MapsList = (props: MapsListProps): React.ReactElement => {
const classes = useStyles(); const classes = useStyles();
const [order, setOrder] = React.useState<Order>('desc'); const [order, setOrder] = React.useState<Order>('desc');
@ -384,7 +402,19 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
}); });
}; };
const removeLabelMultation = useMutation<void, ErrorInfo, { mapId: number, labelId: number }, number>( const handleAddLabelClick = () => {
setActiveDialog({
actionType: 'label',
mapsId: selected,
});
};
const removeLabelMultation = useMutation<
void,
ErrorInfo,
{ mapId: number; labelId: number },
number
>(
({ mapId, labelId }) => { ({ mapId, labelId }) => {
return client.deleteLabelFromMap(labelId, mapId); return client.deleteLabelFromMap(labelId, mapId);
}, },
@ -402,40 +432,6 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
removeLabelMultation.mutate({ mapId, labelId }); removeLabelMultation.mutate({ mapId, labelId });
}; };
const changeLabelMutation = useMutation<void, ErrorInfo, { mapIds: number[], label: Label, checked: boolean }, number>(
async ({ mapIds, label, checked }) => {
const selectedMaps: MapInfo[] = mapsInfo.filter((m) => mapIds.includes(m.id));
if (!label.id) {
label.id = await client.createLabel(label.title, label.color);
}
if (checked) {
const toAdd = selectedMaps.filter((m) => !m.labels.find((l) => l.id === label.id));
await Promise.all(toAdd.map((m) => client.addLabelToMap(label.id, m.id)));
} else {
const toRemove = selectedMaps.filter((m) => m.labels.find((l) => l.id === label.id));
await Promise.all(toRemove.map((m) => client.deleteLabelFromMap(label.id, m.id)));
}
return Promise.resolve();
},
{
onSuccess: () => {
queryClient.invalidateQueries('maps');
queryClient.invalidateQueries('labels');
},
onError: (error) => {
console.error(error);
}
}
);
const handleChangesInLabels = (label: Label, checked: boolean) => {
changeLabelMutation.mutate({
mapIds: selected,
label,
checked
});
};
const isSelected = (id: number) => selected.indexOf(id) !== -1; const isSelected = (id: number) => selected.indexOf(id) !== -1;
return ( return (
<div className={classes.root}> <div className={classes.root}>
@ -470,10 +466,31 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip> </Tooltip>
)} )}
{selected.length > 0 && <AddLabelButton {selected.length > 0 && (
onChange={handleChangesInLabels} <Tooltip
maps={mapsInfo.filter(m => isSelected(m.id))} arrow={true}
/>} title={intl.formatMessage({
id: 'map.tooltip-add',
defaultMessage: 'Add label to selected',
})}
>
<Button
color="primary"
size="medium"
variant="outlined"
type="button"
style={{ marginLeft: '10px' }}
disableElevation={true}
startIcon={<LabelTwoTone />}
onClick={handleAddLabelClick}
>
<FormattedMessage
id="action.label"
defaultMessage="Add Label"
/>
</Button>
</Tooltip>
)}
</div> </div>
<div className={classes.toolbarListActions}> <div className={classes.toolbarListActions}>
@ -614,10 +631,13 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell className={classes.bodyCell}> <TableCell className={[classes.bodyCell, classes.labelsCell].join(' ')}>
<LabelsCell labels={row.labels} onDelete={(lbl) => { <LabelsCell
handleRemoveLabel(row.id, lbl.id); labels={row.labels}
}} /> onDelete={(lbl) => {
handleRemoveLabel(row.id, lbl.id);
}}
/>
</TableCell> </TableCell>
<TableCell className={classes.bodyCell}> <TableCell className={classes.bodyCell}>

View File

@ -0,0 +1,45 @@
import React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
import Typography from '@mui/material/Typography';
import BaseDialog from '../../action-dispatcher/base-dialog';
import { Label } from '../../../../classes/client';
export type LabelDeleteConfirmType = {
label: Label;
onClose: () => void;
onConfirm: () => void;
};
const LabelDeleteConfirm = ({ label, onClose, onConfirm }: LabelDeleteConfirmType): React.ReactElement => {
const intl = useIntl();
return (
<div>
<BaseDialog
onClose={onClose}
onSubmit={onConfirm}
title={intl.formatMessage({ id: 'label.delete-title', defaultMessage: 'Confirm label deletion' })}
submitButton={intl.formatMessage({
id: 'action.delete-title',
defaultMessage: 'Delete',
})}
>
<Alert severity="warning">
<AlertTitle>{intl.formatMessage({ id: 'label.delete-title', defaultMessage: 'Confirm label deletion' })}</AlertTitle>
<span>
<Typography fontWeight="bold" component="span">{label.title} </Typography>
<FormattedMessage
id="label.delete-description"
defaultMessage="will be deleted, including its associations to all existing maps. Do you want to continue?"
/>
</span>
</Alert>
</BaseDialog>
</div>
);
};
export default LabelDeleteConfirm;

View File

@ -1,125 +1,55 @@
import React from 'react'; import React from 'react';
import FormGroup from '@mui/material/FormGroup'; import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import Divider from '@mui/material/Divider';
import AddIcon from '@mui/icons-material/Add';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import Container from '@mui/material/Container'; import Container from '@mui/material/Container';
import { Label as LabelComponent } from '../label'; import LabelComponent from '../label';
import Client, { Label, ErrorInfo, MapInfo } from '../../../../classes/client'; import Client, { Label, ErrorInfo, MapInfo } from '../../../../classes/client';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { activeInstance } from '../../../../redux/clientSlice'; import { activeInstance } from '../../../../redux/clientSlice';
import { StyledButton, NewLabelContainer, NewLabelColor, CreateLabel } from './styled'; import AddLabelForm from '../add-label-form';
import { TextField } from '@mui/material'; import { LabelListContainer } from './styled';
import { FormattedMessage, useIntl } from 'react-intl';
import Typography from '@mui/material/Typography';
const labelColors = [
'#00b327',
'#0565ff',
'#2d2dd6',
'#6a00ba',
'#ad1599',
'#ff1e35',
'#ff6600',
'#ffff47',
];
export type LabelSelectorProps = { export type LabelSelectorProps = {
maps: MapInfo[]; maps: MapInfo[];
onChange: (label: Label, checked: boolean) => void; onChange: (label: Label, checked: boolean) => void;
}; };
export function LabelSelector({ onChange, maps }: LabelSelectorProps): React.ReactElement { export function LabelSelector({ onChange, maps }: LabelSelectorProps): React.ReactElement {
const client: Client = useSelector(activeInstance); const client: Client = useSelector(activeInstance);
const intl = useIntl(); const { data: labels = [] } = useQuery<unknown, ErrorInfo, Label[]>('labels', async () =>
client.fetchLabels()
);
const { data: labels = [] } = useQuery<unknown, ErrorInfo, Label[]>('labels', async () => client.fetchLabels()); const checkedLabelIds = labels
.map((l) => l.id)
.filter((labelId) => maps.every((m) => m.labels.find((l) => l.id === labelId)));
const checkedLabelIds = labels.map(l => l.id).filter(labelId => maps.every(m => m.labels.find(l => l.id === labelId))); return (
<Container>
const [createLabelColorIndex, setCreateLabelColorIndex] = React.useState(Math.floor(Math.random() * labelColors.length)); <FormGroup>
const [newLabelTitle, setNewLabelTitle] = React.useState(''); <AddLabelForm onAdd={(label) => onChange(label, true)} />
</FormGroup>
const newLabelColor = labelColors[createLabelColorIndex]; <LabelListContainer>
{labels.map(({ id, title, color }) => (
const setNextLabelColorIndex = () => { <FormControlLabel
const nextIndex = labelColors[createLabelColorIndex + 1] ? key={id}
createLabelColorIndex + 1 : control={
0; <Checkbox
setCreateLabelColorIndex(nextIndex); id={`${id}`}
}; checked={checkedLabelIds.includes(id)}
onChange={(e) => {
onChange({ id, title, color }, e.target.checked);
const handleSubmitNew = () => { }}
onChange({ name={title}
title: newLabelTitle, color="primary"
color: newLabelColor, />
id: 0, }
}, true); label={<LabelComponent label={{ id, title, color }} size="big" />}
setNewLabelTitle(''); />
setNextLabelColorIndex(); ))}
}; </LabelListContainer>
</Container>
return ( );
<Container>
<FormGroup>
{labels.map(({ id, title, color}) => (
<FormControlLabel
key={id}
control={
<Checkbox
id={`${id}`}
checked={checkedLabelIds.includes(id)}
onChange={(e) => {
onChange({ id, title, color }, e.target.checked);
}}
name={title}
color="primary"
/>
}
label={<LabelComponent name={title} color={color} />}
/>
))}
<Divider />
<CreateLabel>
<Typography variant="h4" component="h4" fontSize={14} >
<FormattedMessage id="label.create-new" defaultMessage={
intl.formatMessage({
id: 'label.add-placeholder',
defaultMessage: 'Label title',
})
} />
</Typography>
<NewLabelContainer>
<NewLabelColor
color={newLabelColor}
onClick={(e) => {
e.stopPropagation();
setNextLabelColorIndex();
}}
/>
<TextField variant='outlined' label="Label title"
onChange={(e) => setNewLabelTitle(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSubmitNew();
}
}}
value={newLabelTitle} />
</NewLabelContainer>
<StyledButton
color="primary"
startIcon={<AddIcon />}
onClick={() => handleSubmitNew()}
disabled={!newLabelTitle.length}
>
<FormattedMessage id="label.add-button" defaultMessage="Add label" />
</StyledButton>
</CreateLabel>
</FormGroup>
</Container>
);
} }

View File

@ -1,33 +1,8 @@
import styled, { css } from 'styled-components'; import FormGroup from '@mui/material/FormGroup';
import Button from '@mui/material/Button'; import styled from 'styled-components';
export const StyledButton = styled(Button)` export const LabelListContainer = styled(FormGroup)`
margin: 4px; max-height: 400px;
`; flex-wrap: nowrap;
overflow-y: scroll;
export const NewLabelContainer = styled.div` `;
display: flex;
flex-direction: row;
align-items: center;
padding: 15px 0 5px 0 ;
`;
const SIZE = 25;
export const NewLabelColor = styled.div`
width: ${SIZE}px;
height: ${SIZE}px;
border-radius: ${SIZE * 0.25}px;
border: 1px solid black;
margin: 1px ${SIZE * 0.5}px 1px 0px;
${props => props.color && css`
background-color: ${props.color};
`}
cursor: pointer;
`;
export const CreateLabel = styled.div`
padding-top: 10px;
display: flex;
flex-direction: column;
justify-content: flex-end;
`;

View File

@ -1,13 +1,38 @@
import React from 'react'; import React from 'react';
import { Color, StyledLabel, Name } from './styled'; import { LabelContainer, LabelText } from './styled';
type Props = { name: string, color: string }; import { Label } from '../../../../classes/client';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
import DeleteIcon from '@mui/icons-material/Clear';
import IconButton from '@mui/material/IconButton';
export function Label({ name, color }: Props): React.ReactElement<Props> {
return ( type LabelSize = 'small' | 'big';
<StyledLabel> type LabelComponentProps = { label: Label; onDelete?: (label: Label) => void; size?: LabelSize };
<Color color={color} />
<Name>{name}</Name> export default function LabelComponent({ label, onDelete, size = 'small' }: LabelComponentProps): React.ReactElement<LabelComponentProps> {
</StyledLabel> const iconSize = size === 'small' ? {
); height: '0.6em', width: '0.6em'
} : { height: '0.9em', width: '0.9em' };
return (
<LabelContainer color={label.color}>
<LabelTwoTone htmlColor={label.color} style={iconSize} />
<LabelText>{label.title}</LabelText>
{onDelete && (
<IconButton
color="default"
size="small"
aria-label="delete tag"
component="span"
onClick={(e) => {
e.stopPropagation();
onDelete(label);
}}
>
<DeleteIcon style={iconSize} />
</IconButton>
)}
</LabelContainer>
);
} }

View File

@ -1,23 +1,15 @@
import styled, { css } from 'styled-components'; import styled from 'styled-components';
const SIZE = 20; export const LabelContainer = styled.div`
display: inline-flex;
export const Color = styled.div`
width: ${SIZE}px;
height: ${SIZE}px;
border-radius: ${SIZE * 0.25}px;
border: 1px solid black;
margin: 1px ${SIZE * 0.5}px 1px 0px;
${props => props.color && css`
background-color: ${props.color};
`}
`;
export const StyledLabel = styled.div`
display: flex;
flex-direction: row; flex-direction: row;
margin: 4px;
padding: 4px;
align-items: center;
font-size: smaller;
`; `;
export const Name = styled.div` export const LabelText = styled.span`
flex: 1; margin-left: 4px;
`; margin-right: 2px;
`;

View File

@ -33,6 +33,13 @@ export const useStyles = makeStyles((theme: Theme) =>
bodyCell: { bodyCell: {
border: '0px', border: '0px',
}, },
labelsCell: {
maxWidth: '300px',
overflow: 'hidden',
textAlign: 'right',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
},
visuallyHidden: { visuallyHidden: {
border: 0, border: 0,
clip: 'rect(0 0 0 0)', clip: 'rect(0 0 0 0)',