Merge branch 'feature/labels' into develop

This commit is contained in:
Paulo Gustavo Veiga 2022-02-09 16:36:51 -08:00
commit 67fb3bbc35
16 changed files with 436 additions and 69 deletions

View File

@ -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();
} }

View File

@ -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>;

View File

@ -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();
} }

View File

@ -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();
}) })

View File

@ -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>
); );
}; };

View File

@ -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;

View File

@ -0,0 +1,10 @@
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
export const useStyles = makeStyles(() =>
createStyles({
paper: {
maxWidth: '420px',
},
})
);

View File

@ -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}`);
}, },

View File

@ -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,14 +29,14 @@ export function AddLabelButton({ onChange }: AddLabelButtonTypes): React.ReactEl
const id = open ? 'add-label-popover' : undefined; const id = open ? 'add-label-popover' : undefined;
return ( return (
<Tooltip <>
arrow={true} <Tooltip
title={intl.formatMessage({ arrow={true}
id: 'map.tooltip-add', title={intl.formatMessage({
defaultMessage: 'Add label to selected', id: 'map.tooltip-add',
})} defaultMessage: 'Add label to selected',
> })}
<> >
<Button <Button
color="primary" color="primary"
size="medium" size="medium"
@ -49,23 +49,24 @@ export function AddLabelButton({ onChange }: AddLabelButtonTypes): React.ReactEl
> >
<FormattedMessage id="action.label" defaultMessage="Add Label" /> <FormattedMessage id="action.label" defaultMessage="Add Label" />
</Button> </Button>
<Popover </Tooltip>
id={id}
open={open} <Popover
anchorEl={anchorEl} id={id}
onClose={handleClose} open={open}
anchorOrigin={{ anchorEl={anchorEl}
vertical: 'bottom', onClose={handleClose}
horizontal: 'center', anchorOrigin={{
}} vertical: 'bottom',
transformOrigin={{ horizontal: 'center',
vertical: 'top', }}
horizontal: 'center', transformOrigin={{
}} vertical: 'top',
> horizontal: 'center',
<LabelSelector /> }}
</Popover> >
</> <LabelSelector onChange={onChange} maps={maps} />
</Tooltip> </Popover>
</>
); );
} }

View File

@ -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}>

View File

@ -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 />
<StyledButton <CreateLabel>
color="primary" <Typography variant="h4" component="h4" fontSize={14} >
startIcon={<AddIcon />} <FormattedMessage id="label.create-new" defaultMessage={
> intl.formatMessage({
{/* i18n */} id: 'label.add-placeholder',
Add new label defaultMessage: 'Label title',
</StyledButton> })
} />
</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> </FormGroup>
</Container> </Container>
); );

View File

@ -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;
`;

View File

@ -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>
))} ))}
</> </>
); );

View File

@ -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;
`;

View File

@ -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,

View File

@ -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==