mirror of
https://bitbucket.org/wisemapping/wisemapping-frontend.git
synced 2024-11-22 14:47:56 +01:00
Merge branch 'feature/labels' into develop
This commit is contained in:
commit
67fb3bbc35
@ -90,10 +90,22 @@ class CacheDecoratorClient implements Client {
|
|||||||
return this.client.fetchLabels();
|
return this.client.fetchLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createLabel(title: string, color: string): Promise<number> {
|
||||||
|
return this.client.createLabel(title, color);
|
||||||
|
}
|
||||||
|
|
||||||
deleteLabel(id: number): Promise<void> {
|
deleteLabel(id: number): Promise<void> {
|
||||||
return this.client.deleteLabel(id);
|
return this.client.deleteLabel(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addLabelToMap(labelId: number, mapId: number): Promise<void> {
|
||||||
|
return this.client.addLabelToMap(labelId, mapId);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteLabelFromMap(labelId: number, mapId: number): Promise<void> {
|
||||||
|
return this.client.deleteLabelFromMap(labelId, mapId);
|
||||||
|
}
|
||||||
|
|
||||||
fetchAccountInfo(): Promise<AccountInfo> {
|
fetchAccountInfo(): Promise<AccountInfo> {
|
||||||
return this.client.fetchAccountInfo();
|
return this.client.fetchAccountInfo();
|
||||||
}
|
}
|
||||||
|
@ -94,8 +94,11 @@ interface Client {
|
|||||||
updateStarred(id: number, starred: boolean): Promise<void>;
|
updateStarred(id: number, starred: boolean): Promise<void>;
|
||||||
updateMapToPublic(id: number, isPublic: boolean): Promise<void>;
|
updateMapToPublic(id: number, isPublic: boolean): Promise<void>;
|
||||||
|
|
||||||
|
createLabel(title: string, color: string): Promise<number>;
|
||||||
fetchLabels(): Promise<Label[]>;
|
fetchLabels(): Promise<Label[]>;
|
||||||
deleteLabel(id: number): Promise<void>;
|
deleteLabel(id: number): Promise<void>;
|
||||||
|
addLabelToMap(labelId: number, mapId: number): Promise<void>;
|
||||||
|
deleteLabelFromMap(labelId: number, mapId: number): Promise<void>;
|
||||||
fetchAccountInfo(): Promise<AccountInfo>;
|
fetchAccountInfo(): Promise<AccountInfo>;
|
||||||
|
|
||||||
registerNewUser(user: NewUser): Promise<void>;
|
registerNewUser(user: NewUser): Promise<void>;
|
||||||
|
@ -327,9 +327,46 @@ class MockClient implements Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createLabel(title: string, color: string): Promise<number> {
|
||||||
|
const newId = Math.max.apply(Number, this.labels.map(l => l.id)) + 1;
|
||||||
|
this.labels.push({
|
||||||
|
id: newId,
|
||||||
|
title,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
|
||||||
deleteLabel(id: number): Promise<void> {
|
deleteLabel(id: number): Promise<void> {
|
||||||
this.labels = this.labels.filter((l) => l.id != id);
|
this.labels = this.labels.filter((l) => l.id != id);
|
||||||
console.log('Label delete:' + this.labels);
|
this.maps = this.maps.map(m => {
|
||||||
|
return {
|
||||||
|
...m,
|
||||||
|
labels: m.labels.filter((l) => l.id != id)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
addLabelToMap(labelId: number, mapId: number): Promise<void> {
|
||||||
|
const labelToAdd = this.labels.find((l) => l.id === labelId);
|
||||||
|
if (!labelToAdd) {
|
||||||
|
return Promise.reject({ msg: `unable to find label with id ${labelId}`});
|
||||||
|
}
|
||||||
|
const map = this.maps.find((m) => m.id === mapId);
|
||||||
|
if (!map) {
|
||||||
|
return Promise.reject({ msg: `unable to find map with id ${mapId}` });
|
||||||
|
}
|
||||||
|
map.labels.push(labelToAdd);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteLabelFromMap(labelId: number, mapId: number): Promise<void> {
|
||||||
|
const map = this.maps.find((m) => m.id === mapId);
|
||||||
|
if (!map) {
|
||||||
|
return Promise.reject({ msg: `unable to find map with id ${mapId}` });
|
||||||
|
}
|
||||||
|
map.labels = map.labels.filter((l) => l.id !== labelId);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,10 +510,59 @@ export default class RestClient implements Client {
|
|||||||
return new Promise(handler);
|
return new Promise(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createLabel(title: string, color: string): Promise<number> {
|
||||||
|
const handler = (success: (labelId: number) => void, reject: (error: ErrorInfo) => void) => {
|
||||||
|
axios
|
||||||
|
.post(`${this.baseUrl}/c/restful/labels`, JSON.stringify({ title, color, iconName: 'smile' }), {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
success(response.headers.resourceid);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorInfo = this.parseResponseOnError(error.response);
|
||||||
|
reject(errorInfo);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return new Promise(handler);
|
||||||
|
}
|
||||||
|
|
||||||
deleteLabel(id: number): Promise<void> {
|
deleteLabel(id: number): Promise<void> {
|
||||||
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
|
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
|
||||||
axios
|
axios
|
||||||
.delete(`${this.baseUrl}/c/restful/label/${id}`)
|
.delete(`${this.baseUrl}/c/restful/labels/${id}`)
|
||||||
|
.then(() => {
|
||||||
|
success();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorInfo = this.parseResponseOnError(error.response);
|
||||||
|
reject(errorInfo);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return new Promise(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
addLabelToMap(labelId: number, mapId: number): Promise<void> {
|
||||||
|
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
|
||||||
|
axios
|
||||||
|
.post(`${this.baseUrl}/c/restful/maps/${mapId}/labels`, JSON.stringify(labelId), {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
success();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorInfo = this.parseResponseOnError(error.response);
|
||||||
|
reject(errorInfo);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return new Promise(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteLabelFromMap(labelId: number, mapId: number): Promise<void> {
|
||||||
|
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
|
||||||
|
axios
|
||||||
|
.delete(`${this.baseUrl}/c/restful/maps/${mapId}/labels/${labelId}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
success();
|
success();
|
||||||
})
|
})
|
||||||
|
@ -12,6 +12,7 @@ import InfoDialog from './info-dialog';
|
|||||||
import DeleteMultiselectDialog from './delete-multiselect-dialog';
|
import DeleteMultiselectDialog from './delete-multiselect-dialog';
|
||||||
import ExportDialog from './export-dialog';
|
import ExportDialog from './export-dialog';
|
||||||
import ShareDialog from './share-dialog';
|
import ShareDialog from './share-dialog';
|
||||||
|
import LabelDialog from './label-dialog';
|
||||||
|
|
||||||
export type BasicMapInfo = {
|
export type BasicMapInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -61,6 +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]} />}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
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 { activeInstance } from '../../../../redux/clientSlice';
|
||||||
|
|
||||||
|
|
||||||
|
const LabelDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const classes = useStyles();
|
||||||
|
const client: Client = useSelector(activeInstance);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// TODO: pass down map data instead of using query?
|
||||||
|
const { data } = useQuery<unknown, ErrorInfo, MapInfo[]>('maps', () => {
|
||||||
|
return client.fetchAllMaps();
|
||||||
|
});
|
||||||
|
|
||||||
|
const map = data.find(m => m.id === mapId);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries('maps');
|
||||||
|
queryClient.invalidateQueries('labels');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangesInLabels = (label: Label, checked: boolean) => {
|
||||||
|
changeLabelMutation.mutate({
|
||||||
|
label,
|
||||||
|
checked
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<BaseDialog
|
||||||
|
onClose={onClose}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'label.title',
|
||||||
|
defaultMessage: 'Add a label',
|
||||||
|
})}
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: 'label.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Use labels to organize your maps',
|
||||||
|
})}
|
||||||
|
PaperProps={{ classes: { root: classes.paper } }}
|
||||||
|
>
|
||||||
|
<LabelSelector onChange={handleChangesInLabels} maps={[map]} />
|
||||||
|
</BaseDialog>
|
||||||
|
</div>);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LabelDialog;
|
@ -0,0 +1,10 @@
|
|||||||
|
import createStyles from '@mui/styles/createStyles';
|
||||||
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
paper: {
|
||||||
|
maxWidth: '420px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
@ -7,7 +7,7 @@ import List from '@mui/material/List';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import { useStyles } from './style';
|
import { useStyles } from './style';
|
||||||
import { MapsList } from './maps-list';
|
import { MapsList } from './maps-list';
|
||||||
import { createIntl, createIntlCache, FormattedMessage, IntlProvider, IntlShape, useIntl } from 'react-intl';
|
import { createIntl, createIntlCache, FormattedMessage, IntlProvider } from 'react-intl';
|
||||||
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
||||||
import { activeInstance } from '../../redux/clientSlice';
|
import { activeInstance } from '../../redux/clientSlice';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@ -76,7 +76,6 @@ const MapsPage = (): ReactElement => {
|
|||||||
}, cache)
|
}, cache)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
document.title = intl.formatMessage({
|
document.title = intl.formatMessage({
|
||||||
id: 'maps.page-title',
|
id: 'maps.page-title',
|
||||||
defaultMessage: 'My Maps | WiseMapping',
|
defaultMessage: 'My Maps | WiseMapping',
|
||||||
@ -84,7 +83,10 @@ const MapsPage = (): ReactElement => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const mutation = useMutation((id: number) => client.deleteLabel(id), {
|
const mutation = useMutation((id: number) => client.deleteLabel(id), {
|
||||||
onSuccess: () => queryClient.invalidateQueries('labels'),
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries('labels');
|
||||||
|
queryClient.invalidateQueries('maps');
|
||||||
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error(`Unexpected error ${error}`);
|
console.error(`Unexpected error ${error}`);
|
||||||
},
|
},
|
||||||
|
@ -4,15 +4,15 @@ import Button from '@mui/material/Button';
|
|||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
|
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Label } from '../../../../classes/client';
|
import { Label, MapInfo } from '../../../../classes/client';
|
||||||
import { LabelSelector } from '../label-selector';
|
import { LabelSelector } from '../label-selector';
|
||||||
|
|
||||||
type AddLabelButtonTypes = {
|
type AddLabelButtonTypes = {
|
||||||
onChange?: (label: Label) => void;
|
maps: MapInfo[];
|
||||||
|
onChange: (label: Label, checked: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AddLabelButton({ onChange }: AddLabelButtonTypes): React.ReactElement {
|
export function AddLabelButton({ onChange, maps }: AddLabelButtonTypes): React.ReactElement {
|
||||||
console.log(onChange);
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
||||||
@ -29,6 +29,7 @@ export function AddLabelButton({ onChange }: AddLabelButtonTypes): React.ReactEl
|
|||||||
const id = open ? 'add-label-popover' : undefined;
|
const id = open ? 'add-label-popover' : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
arrow={true}
|
arrow={true}
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
@ -36,7 +37,6 @@ export function AddLabelButton({ onChange }: AddLabelButtonTypes): React.ReactEl
|
|||||||
defaultMessage: 'Add label to selected',
|
defaultMessage: 'Add label to selected',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<>
|
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
size="medium"
|
size="medium"
|
||||||
@ -49,6 +49,8 @@ export function AddLabelButton({ onChange }: AddLabelButtonTypes): React.ReactEl
|
|||||||
>
|
>
|
||||||
<FormattedMessage id="action.label" defaultMessage="Add Label" />
|
<FormattedMessage id="action.label" defaultMessage="Add Label" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
id={id}
|
id={id}
|
||||||
open={open}
|
open={open}
|
||||||
@ -63,9 +65,8 @@ export function AddLabelButton({ onChange }: AddLabelButtonTypes): React.ReactEl
|
|||||||
horizontal: 'center',
|
horizontal: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LabelSelector />
|
<LabelSelector onChange={onChange} maps={maps} />
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -331,7 +331,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
9;
|
|
||||||
const starredMultation = useMutation<void, ErrorInfo, number>(
|
const starredMultation = useMutation<void, ErrorInfo, number>(
|
||||||
(id: number) => {
|
(id: number) => {
|
||||||
const map = mapsInfo.find((m) => m.id == id);
|
const map = mapsInfo.find((m) => m.id == id);
|
||||||
@ -385,6 +385,58 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeLabelMultation = useMutation<void, ErrorInfo, { mapId: number, labelId: number}, number>(
|
||||||
|
({ mapId, labelId }) => {
|
||||||
|
return client.deleteLabelFromMap(labelId, mapId);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries('maps');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveLabel = (mapId: number, labelId: number) => {
|
||||||
|
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}>
|
||||||
@ -419,7 +471,10 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selected.length > 0 && <AddLabelButton />}
|
{selected.length > 0 && <AddLabelButton
|
||||||
|
onChange={handleChangesInLabels}
|
||||||
|
maps={mapsInfo.filter(m => isSelected(m.id))}
|
||||||
|
/>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={classes.toolbarListActions}>
|
<div className={classes.toolbarListActions}>
|
||||||
@ -561,7 +616,9 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className={classes.bodyCell}>
|
<TableCell className={classes.bodyCell}>
|
||||||
<LabelsCell labels={row.labels} />
|
<LabelsCell labels={row.labels} onDelete={(lbl) => {
|
||||||
|
handleRemoveLabel(row.id, lbl.id);
|
||||||
|
}} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className={classes.bodyCell}>
|
<TableCell className={classes.bodyCell}>
|
||||||
|
@ -6,24 +6,60 @@ 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 { Label as LabelComponent } from '../label';
|
||||||
import Client, { Label, ErrorInfo } 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 } from './styled';
|
import { StyledButton, NewLabelContainer, NewLabelColor, CreateLabel } from './styled';
|
||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
export function LabelSelector(): React.ReactElement {
|
const labelColors = [
|
||||||
|
'#00b327',
|
||||||
|
'#0565ff',
|
||||||
|
'#2d2dd6',
|
||||||
|
'#6a00ba',
|
||||||
|
'#ad1599',
|
||||||
|
'#ff1e35',
|
||||||
|
'#ff6600',
|
||||||
|
'#ffff47',
|
||||||
|
];
|
||||||
|
|
||||||
|
export type LabelSelectorProps = {
|
||||||
|
maps: MapInfo[];
|
||||||
|
onChange: (label: Label, checked: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 [state, setState] = React.useState(labels.reduce((acc, label) => {
|
const checkedLabelIds = labels.map(l => l.id).filter(labelId => maps.every(m => m.labels.find(l => l.id === labelId)));
|
||||||
acc[label.id] = false //label.checked;
|
|
||||||
return acc;
|
|
||||||
}, {}),);
|
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const [createLabelColorIndex, setCreateLabelColorIndex] = React.useState(Math.floor(Math.random() * labelColors.length));
|
||||||
setState({ ...state, [event.target.id]: event.target.checked });
|
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 (
|
||||||
@ -35,8 +71,10 @@ export function LabelSelector(): React.ReactElement {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`${id}`}
|
id={`${id}`}
|
||||||
checked={state[id]}
|
checked={checkedLabelIds.includes(id)}
|
||||||
onChange={handleChange}
|
onChange={(e) => {
|
||||||
|
onChange({ id, title, color }, e.target.checked);
|
||||||
|
}}
|
||||||
name={title}
|
name={title}
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
@ -45,13 +83,42 @@ export function LabelSelector(): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Divider />
|
<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
|
<StyledButton
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
|
onClick={() => handleSubmitNew()}
|
||||||
|
disabled={!newLabelTitle.length}
|
||||||
>
|
>
|
||||||
{/* i18n */}
|
<FormattedMessage id="label.add-button" defaultMessage="Add label" />
|
||||||
Add new label
|
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
|
|
||||||
|
</CreateLabel>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,33 @@
|
|||||||
import styled from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
export const StyledButton = styled(Button)`
|
export const StyledButton = styled(Button)`
|
||||||
margin: 4px;
|
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;
|
||||||
|
`;
|
||||||
|
@ -1,26 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Chip from '@mui/material/Chip';
|
import { LabelContainer, LabelText } from './styled';
|
||||||
|
|
||||||
import { Label } from '../../../../classes/client';
|
import { Label } from '../../../../classes/client';
|
||||||
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
|
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Clear';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
labels: Label[],
|
labels: Label[],
|
||||||
|
onDelete: (label: Label) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LabelsCell({ labels }: Props): React.ReactElement<Props> {
|
export function LabelsCell({ labels, onDelete }: Props): React.ReactElement<Props> {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{labels.map(label => (
|
{labels.map(label => (
|
||||||
<Chip
|
<LabelContainer
|
||||||
key={label.id}
|
key={label.id}
|
||||||
size="small"
|
color={label.color}
|
||||||
icon={<LabelTwoTone />}
|
>
|
||||||
label={label.title}
|
<LabelTwoTone htmlColor={label.color} style={{ height: '0.6em', width: '0.6em' }} />
|
||||||
clickable
|
<LabelText>{ label.title }</LabelText>
|
||||||
color="primary"
|
<IconButton color="default" size='small' aria-label="delete tag" component="span"
|
||||||
style={{ backgroundColor: label.color, opacity: '0.75' }}
|
onClick={(e) => {
|
||||||
onDelete={() => { return 1; }}
|
e.stopPropagation();
|
||||||
/>
|
onDelete(label);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon style={{ height: '0.6em', width: '0.6em' }} />
|
||||||
|
</IconButton>
|
||||||
|
</LabelContainer>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const LabelContainer = styled.div`
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: smaller;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LabelText = styled.span`
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 2px;
|
||||||
|
`;
|
@ -7,6 +7,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|||||||
module.exports = merge(common, {
|
module.exports = merge(common, {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
|
watch: true,
|
||||||
devServer: {
|
devServer: {
|
||||||
contentBase: path.join(__dirname, 'dist'),
|
contentBase: path.join(__dirname, 'dist'),
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
@ -5661,7 +5661,7 @@ dateformat@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
||||||
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
||||||
|
|
||||||
dayjs@^1.10.4:
|
dayjs@^1.10.4, dayjs@^1.10.7:
|
||||||
version "1.10.7"
|
version "1.10.7"
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
||||||
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
|
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
|
||||||
|
Loading…
Reference in New Issue
Block a user