mirror of
https://bitbucket.org/wisemapping/wisemapping-frontend.git
synced 2024-11-25 23:54:55 +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 { 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();
|
||||||
|
@ -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;
|
||||||
|
@ -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>);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 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) {
|
||||||
@ -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
|
||||||
|
labels={row.labels}
|
||||||
|
onDelete={(lbl) => {
|
||||||
handleRemoveLabel(row.id, lbl.id);
|
handleRemoveLabel(row.id, lbl.id);
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className={classes.bodyCell}>
|
<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,30 +1,15 @@
|
|||||||
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[];
|
||||||
@ -33,38 +18,20 @@ export type LabelSelectorProps = {
|
|||||||
|
|
||||||
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)
|
||||||
const checkedLabelIds = labels.map(l => l.id).filter(labelId => maps.every(m => m.labels.find(l => l.id === labelId)));
|
.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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
<AddLabelForm onAdd={(label) => onChange(label, true)} />
|
||||||
|
</FormGroup>
|
||||||
|
<LabelListContainer>
|
||||||
{labels.map(({ id, title, color }) => (
|
{labels.map(({ id, title, color }) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={id}
|
key={id}
|
||||||
@ -79,47 +46,10 @@ export function LabelSelector({ onChange, maps }: LabelSelectorProps): React.Rea
|
|||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={<LabelComponent name={title} color={color} />}
|
label={<LabelComponent label={{ id, title, color }} size="big" />}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Divider />
|
</LabelListContainer>
|
||||||
<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>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
`;
|
`;
|
@ -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';
|
||||||
|
|
||||||
|
|
||||||
|
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' };
|
||||||
|
|
||||||
export function Label({ name, color }: Props): React.ReactElement<Props> {
|
|
||||||
return (
|
return (
|
||||||
<StyledLabel>
|
<LabelContainer color={label.color}>
|
||||||
<Color color={color} />
|
<LabelTwoTone htmlColor={label.color} style={iconSize} />
|
||||||
<Name>{name}</Name>
|
<LabelText>{label.title}</LabelText>
|
||||||
</StyledLabel>
|
{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 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;
|
||||||
`;
|
`;
|
@ -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)',
|
||||||
|
Loading…
Reference in New Issue
Block a user