mirror of
https://bitbucket.org/wisemapping/wisemapping-frontend.git
synced 2024-11-22 06:37:56 +01:00
Add labels support.
This commit is contained in:
parent
3725188f8f
commit
d4b37f4139
@ -4,20 +4,15 @@ import { useMutation, useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Client from '../../../../classes/client';
|
||||
import { activeInstance } from '../../../../redux/clientSlice';
|
||||
import { handleOnMutationSuccess } from '..';
|
||||
import { handleOnMutationSuccess, MultiDialogProps } from '..';
|
||||
import BaseDialog from '../base-dialog';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import AlertTitle from '@mui/material/AlertTitle';
|
||||
|
||||
export type DeleteMultiselectDialogProps = {
|
||||
mapsId: number[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const DeleteMultiselectDialog = ({
|
||||
onClose,
|
||||
mapsId,
|
||||
}: DeleteMultiselectDialogProps): React.ReactElement => {
|
||||
}: MultiDialogProps): React.ReactElement => {
|
||||
const intl = useIntl();
|
||||
const client: Client = useSelector(activeInstance);
|
||||
const queryClient = useQueryClient();
|
||||
|
@ -62,7 +62,7 @@ const ActionDispatcher = ({ mapsId, action, onClose, fromEditor }: ActionDialogP
|
||||
<ExportDialog onClose={handleOnClose} mapId={mapsId[0]} enableImgExport={fromEditor} />
|
||||
)}
|
||||
{action === 'share' && <ShareDialog onClose={handleOnClose} mapId={mapsId[0]} />}
|
||||
{action === 'label' && <LabelDialog onClose={handleOnClose} mapId={mapsId[0]} />}
|
||||
{action === 'label' && <LabelDialog onClose={handleOnClose} mapsId={mapsId} />}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@ -81,4 +81,9 @@ export type SimpleDialogProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export type MultiDialogProps = {
|
||||
mapsId: number[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default ActionDispatcher;
|
||||
|
@ -1,16 +1,19 @@
|
||||
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 { 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 { ChangeLabelMutationFunctionParam, getChangeLabelMutationFunction } from '../../maps-list';
|
||||
|
||||
|
||||
const LabelDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement => {
|
||||
const LabelDialog = ({ mapsId, onClose }: MultiDialogProps): React.ReactElement => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles();
|
||||
const client: Client = useSelector(activeInstance);
|
||||
@ -21,19 +24,10 @@ const LabelDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
|
||||
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>(
|
||||
async ({ label, checked }) => {
|
||||
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);
|
||||
}
|
||||
},
|
||||
const changeLabelMutation = useMutation<void, ErrorInfo, ChangeLabelMutationFunctionParam, number>(
|
||||
getChangeLabelMutationFunction(client),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('maps');
|
||||
@ -47,6 +41,7 @@ const LabelDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
|
||||
|
||||
const handleChangesInLabels = (label: Label, checked: boolean) => {
|
||||
changeLabelMutation.mutate({
|
||||
maps,
|
||||
label,
|
||||
checked
|
||||
});
|
||||
@ -63,11 +58,17 @@ const LabelDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
|
||||
description={intl.formatMessage({
|
||||
id: 'label.description',
|
||||
defaultMessage:
|
||||
'Use labels to organize your maps',
|
||||
'Use labels to organize your maps.',
|
||||
})}
|
||||
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>
|
||||
</div>);
|
||||
};
|
||||
|
@ -40,6 +40,7 @@ import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
|
||||
|
||||
import logoIcon from './logo-small.svg';
|
||||
import poweredByIcon from './pwrdby-white.svg';
|
||||
import LabelDeleteConfirm from './maps-list/label-delete-confirm';
|
||||
|
||||
export type Filter = GenericFilter | LabelFilter;
|
||||
|
||||
@ -64,7 +65,7 @@ const MapsPage = (): ReactElement => {
|
||||
const client: Client = useSelector(activeInstance);
|
||||
const queryClient = useQueryClient();
|
||||
const [activeDialog, setActiveDialog] = React.useState<ActionType | undefined>(undefined);
|
||||
|
||||
const [labelToDelete, setLabelToDelete] = React.useState<number | null>(null);
|
||||
// Reload based on user preference ...
|
||||
const userLocale = AppI18n.getUserLocale();
|
||||
|
||||
@ -239,7 +240,7 @@ const MapsPage = (): ReactElement => {
|
||||
filter={buttonInfo.filter}
|
||||
active={filter}
|
||||
onClick={handleMenuClick}
|
||||
onDelete={handleLabelDelete}
|
||||
onDelete={setLabelToDelete}
|
||||
key={`${buttonInfo.filter.type}:${buttonInfo.label}`}
|
||||
/>
|
||||
);
|
||||
@ -260,6 +261,14 @@ const MapsPage = (): ReactElement => {
|
||||
<MapsList filter={filter} />
|
||||
</main>
|
||||
</div>
|
||||
{ labelToDelete && <LabelDeleteConfirm
|
||||
onClose={() => setLabelToDelete(null)}
|
||||
onConfirm={() => {
|
||||
handleLabelDelete(labelToDelete);
|
||||
setLabelToDelete(null);
|
||||
}}
|
||||
label={labels.find(l => l.id === labelToDelete)}
|
||||
/> }
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
`;
|
@ -33,13 +33,13 @@ import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
import StarRateRoundedIcon from '@mui/icons-material/StarRateRounded';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
import { AddLabelButton } from './add-label-button';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { LabelsCell } from './labels-cell';
|
||||
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import AppI18n from '../../../classes/app-i18n';
|
||||
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
|
||||
|
||||
dayjs.extend(LocalizedFormat)
|
||||
dayjs.extend(LocalizedFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||
@ -59,9 +59,9 @@ function getComparator<Key extends keyof any>(
|
||||
order: Order,
|
||||
orderBy: Key
|
||||
): (
|
||||
a: { [key in Key]: number | string | boolean | Label[] | undefined },
|
||||
b: { [key in Key]: number | string | Label[] | boolean }
|
||||
) => number {
|
||||
a: { [key in Key]: number | string | boolean | Label[] | undefined },
|
||||
b: { [key in Key]: number | string | Label[] | boolean }
|
||||
) => number {
|
||||
return order === 'desc'
|
||||
? (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 => {
|
||||
const classes = useStyles();
|
||||
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 }) => {
|
||||
return client.deleteLabelFromMap(labelId, mapId);
|
||||
},
|
||||
@ -402,40 +432,6 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
|
||||
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;
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
@ -470,10 +466,31 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{selected.length > 0 && <AddLabelButton
|
||||
onChange={handleChangesInLabels}
|
||||
maps={mapsInfo.filter(m => isSelected(m.id))}
|
||||
/>}
|
||||
{selected.length > 0 && (
|
||||
<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={handleAddLabelClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="action.label"
|
||||
defaultMessage="Add Label"
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={classes.toolbarListActions}>
|
||||
@ -614,10 +631,13 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className={classes.bodyCell}>
|
||||
<LabelsCell labels={row.labels} onDelete={(lbl) => {
|
||||
handleRemoveLabel(row.id, lbl.id);
|
||||
}} />
|
||||
<TableCell className={[classes.bodyCell, classes.labelsCell].join(' ')}>
|
||||
<LabelsCell
|
||||
labels={row.labels}
|
||||
onDelete={(lbl) => {
|
||||
handleRemoveLabel(row.id, lbl.id);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className={classes.bodyCell}>
|
||||
|
@ -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;
|
@ -1,125 +1,55 @@
|
||||
import React from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
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 Container from '@mui/material/Container';
|
||||
import { Label as LabelComponent } from '../label';
|
||||
import LabelComponent from '../label';
|
||||
import Client, { Label, ErrorInfo, MapInfo } from '../../../../classes/client';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { activeInstance } from '../../../../redux/clientSlice';
|
||||
import { StyledButton, NewLabelContainer, NewLabelColor, CreateLabel } from './styled';
|
||||
import { TextField } from '@mui/material';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
const labelColors = [
|
||||
'#00b327',
|
||||
'#0565ff',
|
||||
'#2d2dd6',
|
||||
'#6a00ba',
|
||||
'#ad1599',
|
||||
'#ff1e35',
|
||||
'#ff6600',
|
||||
'#ffff47',
|
||||
];
|
||||
import AddLabelForm from '../add-label-form';
|
||||
import { LabelListContainer } from './styled';
|
||||
|
||||
export type LabelSelectorProps = {
|
||||
maps: MapInfo[];
|
||||
onChange: (label: Label, checked: boolean) => void;
|
||||
maps: MapInfo[];
|
||||
onChange: (label: Label, checked: boolean) => void;
|
||||
};
|
||||
|
||||
export function LabelSelector({ onChange, maps }: LabelSelectorProps): React.ReactElement {
|
||||
const client: Client = useSelector(activeInstance);
|
||||
const intl = useIntl();
|
||||
const client: Client = useSelector(activeInstance);
|
||||
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)));
|
||||
|
||||
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 = () => {
|
||||
onChange({
|
||||
title: newLabelTitle,
|
||||
color: newLabelColor,
|
||||
id: 0,
|
||||
}, true);
|
||||
setNewLabelTitle('');
|
||||
setNextLabelColorIndex();
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
return (
|
||||
<Container>
|
||||
<FormGroup>
|
||||
<AddLabelForm onAdd={(label) => onChange(label, true)} />
|
||||
</FormGroup>
|
||||
<LabelListContainer>
|
||||
{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 label={{ id, title, color }} size="big" />}
|
||||
/>
|
||||
))}
|
||||
</LabelListContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -1,33 +1,8 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const StyledButton = styled(Button)`
|
||||
margin: 4px;
|
||||
`;
|
||||
|
||||
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;
|
||||
export const LabelListContainer = styled(FormGroup)`
|
||||
max-height: 400px;
|
||||
flex-wrap: nowrap;
|
||||
overflow-y: scroll;
|
||||
`;
|
@ -1,13 +1,38 @@
|
||||
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 (
|
||||
<StyledLabel>
|
||||
<Color color={color} />
|
||||
<Name>{name}</Name>
|
||||
</StyledLabel>
|
||||
);
|
||||
|
||||
type LabelSize = 'small' | 'big';
|
||||
type LabelComponentProps = { label: Label; onDelete?: (label: Label) => void; size?: LabelSize };
|
||||
|
||||
export default function LabelComponent({ label, onDelete, size = 'small' }: LabelComponentProps): React.ReactElement<LabelComponentProps> {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -1,23 +1,15 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SIZE = 20;
|
||||
|
||||
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;
|
||||
export const LabelContainer = styled.div`
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
align-items: center;
|
||||
font-size: smaller;
|
||||
`;
|
||||
|
||||
export const Name = styled.div`
|
||||
flex: 1;
|
||||
export const LabelText = styled.span`
|
||||
margin-left: 4px;
|
||||
margin-right: 2px;
|
||||
`;
|
@ -33,6 +33,13 @@ export const useStyles = makeStyles((theme: Theme) =>
|
||||
bodyCell: {
|
||||
border: '0px',
|
||||
},
|
||||
labelsCell: {
|
||||
maxWidth: '300px',
|
||||
overflow: 'hidden',
|
||||
textAlign: 'right',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis'
|
||||
},
|
||||
visuallyHidden: {
|
||||
border: 0,
|
||||
clip: 'rect(0 0 0 0)',
|
||||
|
Loading…
Reference in New Issue
Block a user