feat: style app connections

This commit is contained in:
Ali BARIN
2021-12-15 21:58:14 +01:00
parent fc85716d07
commit 78375934d7
17 changed files with 232 additions and 34 deletions

View File

@@ -11,7 +11,8 @@ const connectionType = new GraphQLObjectType({
key: { type: GraphQLString }, key: { type: GraphQLString },
data: { type: connectionDataType }, data: { type: connectionDataType },
verified: { type: GraphQLBoolean }, verified: { type: GraphQLBoolean },
app: { type: appType } app: { type: appType },
createdAt: { type: GraphQLString }
} }
} }
}) })

View File

@@ -13,12 +13,14 @@
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15", "@types/jest": "^26.0.15",
"@types/lodash.template": "^4.5.0", "@types/lodash.template": "^4.5.0",
"@types/luxon": "^2.0.8",
"@types/node": "^12.0.0", "@types/node": "^12.0.0",
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"clipboard-copy": "^4.0.1", "clipboard-copy": "^4.0.1",
"graphql": "^15.6.0", "graphql": "^15.6.0",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"luxon": "^2.2.0",
"notistack": "^2.0.2", "notistack": "^2.0.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",

View File

@@ -2,9 +2,11 @@ import * as React from 'react';
import { useLazyQuery, useMutation } from '@apollo/client'; import { useLazyQuery, useMutation } from '@apollo/client';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import CardActionArea from '@mui/material/CardActionArea'; import CardActionArea from '@mui/material/CardActionArea';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import { useSnackbar } from 'notistack'; import { useSnackbar } from 'notistack';
import { DateTime } from 'luxon';
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection'; import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
import { TEST_CONNECTION } from 'graphql/queries/test-connection'; import { TEST_CONNECTION } from 'graphql/queries/test-connection';
@@ -17,7 +19,14 @@ type AppConnectionRowProps = {
connection: Connection; connection: Connection;
} }
const countTranslation = (value: React.ReactNode) => (<><strong>{value}</strong><br /></>); const countTranslation = (value: React.ReactNode) => (
<>
<Typography variant="body1">
{value}
</Typography>
<br />
</>
);
function AppConnectionRow(props: AppConnectionRowProps) { function AppConnectionRow(props: AppConnectionRowProps) {
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
@@ -25,7 +34,7 @@ function AppConnectionRow(props: AppConnectionRowProps) {
const [deleteConnection] = useMutation(DELETE_CONNECTION); const [deleteConnection] = useMutation(DELETE_CONNECTION);
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { id, key, data, verified } = props.connection; const { id, key, data, verified, createdAt } = props.connection;
const contextButtonRef = React.useRef<SVGSVGElement | null>(null); const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null); const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
@@ -57,23 +66,34 @@ function AppConnectionRow(props: AppConnectionRowProps) {
} }
}, [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]); }, [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]);
const relativeCreatedAt = DateTime.fromMillis(parseInt(createdAt, 10)).toRelative();
return ( return (
<> <>
<Card sx={{ my: 2 }}> <Card sx={{ my: 2 }}>
<CardActionArea onClick={onContextMenuClick}> <CardActionArea onClick={onContextMenuClick}>
<CardContent> <CardContent>
<Box> <Stack
direction="column"
justifyContent="center"
alignItems="flex-start"
spacing={1}
>
<Typography variant="h6"> <Typography variant="h6">
{data.screenName} {data.screenName}
</Typography> </Typography>
</Box>
<Typography variant="caption">
{formatMessage('connection.addedAt', { datetime: relativeCreatedAt })}
</Typography>
</Stack>
<Box> <Box>
{testCalled && !testLoading && (verified ? 'yes' : 'no')} {testCalled && !testLoading && (verified ? 'yes' : 'no')}
</Box> </Box>
<Box sx={{ px: 2 }}> <Box sx={{ px: 2 }}>
<Typography variant="body2"> <Typography variant="caption" color="textSecondary" sx={{ display: ['none', 'inline-block'] }}>
{formatMessage('connection.flowCount', { count: countTranslation(0) })} {formatMessage('connection.flowCount', { count: countTranslation(0) })}
</Typography> </Typography>
</Box> </Box>

View File

@@ -1,18 +1,34 @@
import * as React from 'react';
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import AppConnectionRow from 'components/AppConnectionRow'; import AppConnectionRow from 'components/AppConnectionRow';
import NoResultFound from 'components/NoResultFound';
import useFormatMessage from 'hooks/useFormatMessage';
import type { Connection } from 'types/connection'; import type { Connection } from 'types/connection';
import * as URLS from 'config/urls';
type AppConnectionsProps = { type AppConnectionsProps = {
appKey: String; appKey: string;
} }
export default function AppConnections(props: AppConnectionsProps) { export default function AppConnections(props: AppConnectionsProps) {
const { appKey } = props; const { appKey } = props;
const formatMessage = useFormatMessage();
const { data } = useQuery(GET_APP_CONNECTIONS, { variables: { key: appKey } }); const { data } = useQuery(GET_APP_CONNECTIONS, { variables: { key: appKey } });
const appConnections: Connection[] = data?.getApp?.connections || []; const appConnections: Connection[] = data?.getApp?.connections || [];
const hasConnections = appConnections?.length;
if (!hasConnections) {
return (
<NoResultFound
to={URLS.APP_ADD_CONNECTION(appKey)}
text={formatMessage('app.noConnections')}
/>
);
}
return ( return (
<> <>
{appConnections.map((appConnection: Connection) => ( {appConnections.map((appConnection: Connection) => (

View File

@@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import type { AvatarProps } from '@mui/material/Avatar';
type AppIconProps = { type AppIconProps = {
name: string; name: string;
@@ -11,18 +12,19 @@ const inlineImgStyle: React.CSSProperties = {
objectFit: 'contain', objectFit: 'contain',
}; };
export default function AppIcon(props: AppIconProps) { export default function AppIcon(props: AppIconProps & AvatarProps) {
const { name, url } = props; const { name, url, sx = {}, ...restProps } = props;
const color = url ? 'white' : props.color const color = url ? 'white' : props.color
return ( return (
<Avatar <Avatar
component="span" component="span"
variant="square" variant="square"
sx={{ bgcolor: `#${color}` }} sx={{ bgcolor: `#${color}`, display: 'inline-flex', width: 50, height: 50, ...sx }}
imgProps={{ style: inlineImgStyle }} imgProps={{ style: inlineImgStyle }}
src={url} src={url}
alt={name} alt={name}
{...restProps}
/> />
); );
}; };

View File

@@ -22,6 +22,12 @@ export default function Drawer(props: SwipeableDrawerProps) {
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true }); const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const closeOnClick = (event: React.SyntheticEvent) => {
if (matchSmallScreens) {
props.onClose(event);
}
}
return ( return (
<BaseDrawer <BaseDrawer
{...props} {...props}
@@ -38,18 +44,21 @@ export default function Drawer(props: SwipeableDrawerProps) {
icon={<SwapCallsIcon htmlColor={theme.palette.primary.main} />} icon={<SwapCallsIcon htmlColor={theme.palette.primary.main} />}
primary={formatMessage('drawer.flows')} primary={formatMessage('drawer.flows')}
to={URLS.FLOWS} to={URLS.FLOWS}
onClick={closeOnClick}
/> />
<ListItemLink <ListItemLink
icon={<AppsIcon htmlColor={theme.palette.primary.main} />} icon={<AppsIcon htmlColor={theme.palette.primary.main} />}
primary={formatMessage('drawer.apps')} primary={formatMessage('drawer.apps')}
to={URLS.APPS} to={URLS.APPS}
onClick={closeOnClick}
/> />
<ListItemLink <ListItemLink
icon={<ExploreIcon htmlColor={theme.palette.primary.main} />} icon={<ExploreIcon htmlColor={theme.palette.primary.main} />}
primary={formatMessage('drawer.explore')} primary={formatMessage('drawer.explore')}
to={URLS.EXPLORE} to={URLS.EXPLORE}
onClick={closeOnClick}
/> />
</List> </List>

View File

@@ -1,4 +1,4 @@
import { useMemo, forwardRef } from 'react'; import * as React from 'react';
import { useMatch } from 'react-router-dom'; import { useMatch } from 'react-router-dom';
import ListItem from '@mui/material/ListItemButton'; import ListItem from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemIcon from '@mui/material/ListItemIcon';
@@ -9,15 +9,16 @@ type ListItemLinkProps = {
icon: React.ReactNode; icon: React.ReactNode;
primary: string; primary: string;
to: string; to: string;
onClick?: (event: React.SyntheticEvent) => void;
} }
export default function ListItemLink(props: ListItemLinkProps) { export default function ListItemLink(props: ListItemLinkProps) {
const { icon, primary, to } = props; const { icon, primary, to, onClick } = props;
const selected = useMatch({ path: to, end: false }); const selected = useMatch({ path: to, end: false });
const CustomLink = useMemo( const CustomLink = React.useMemo(
() => () =>
forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InLineLink( React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InLineLink(
linkProps, linkProps,
ref, ref,
) { ) {
@@ -28,7 +29,12 @@ export default function ListItemLink(props: ListItemLinkProps) {
return ( return (
<li> <li>
<ListItem component={CustomLink} sx={{ pl: { xs: 2, sm: 3 } }} selected={!!selected}> <ListItem
component={CustomLink}
sx={{ pl: { xs: 2, sm: 3 } }}
selected={!!selected}
onClick={onClick}
>
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon> <ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
<ListItemText primary={primary} primaryTypographyProps={{ variant: 'body1', }} /> <ListItemText primary={primary} primaryTypographyProps={{ variant: 'body1', }} />
</ListItem> </ListItem>

View File

@@ -0,0 +1,42 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import type { LinkProps } from 'react-router-dom';
import Card from '@mui/material/Card';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import CardActionArea from '@mui/material/CardActionArea';
import Typography from '@mui/material/Typography';
import { CardContent } from './style';
type NoResultFoundProps = {
text?: string;
to: string;
}
export default function NoResultFound(props: NoResultFoundProps) {
const { text, to } = props;
const ActionAreaLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
linkProps,
ref,
) {
return <Link ref={ref} to={to} {...linkProps} />;
}),
[to],
);
return (
<Card elevation={0}>
<CardActionArea component={ActionAreaLink} {...props}>
<CardContent>
<AddCircleIcon color="primary" />
<Typography variant="body1">
{text}
</Typography>
</CardContent>
</CardActionArea>
</Card>
)
};

View File

@@ -0,0 +1,11 @@
import { styled } from '@mui/material/styles';
import MuiCardContent from '@mui/material/CardContent';
export const CardContent = styled(MuiCardContent)`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
min-height: 200px;
`;

View File

@@ -11,6 +11,7 @@ export const GET_APP_CONNECTIONS = gql`
data { data {
screenName screenName
} }
createdAt
} }
} }
} }

View File

@@ -9,9 +9,12 @@
"app.connectionCount": "{count} connections", "app.connectionCount": "{count} connections",
"app.flowCount": "{count} flows", "app.flowCount": "{count} flows",
"app.addConnection": "Add connection", "app.addConnection": "Add connection",
"app.createFlow": "Create flow",
"app.settings": "Settings", "app.settings": "Settings",
"app.connections": "Connections", "app.connections": "Connections",
"app.noConnections": "You don't have any connections yet.",
"app.flows": "Flows", "app.flows": "Flows",
"app.noFlows": "You don't have any flows yet.",
"apps.title": "My Apps", "apps.title": "My Apps",
"apps.addConnection": "Add connection", "apps.addConnection": "Add connection",
"apps.addNewAppConnection": "Add a new app connection", "apps.addNewAppConnection": "Add a new app connection",
@@ -21,5 +24,7 @@
"connection.testConnection": "Test connection", "connection.testConnection": "Test connection",
"connection.reconnect": "Reconnect", "connection.reconnect": "Reconnect",
"connection.delete": "Delete", "connection.delete": "Delete",
"connection.deletedMessage": "The connection has been deleted." "connection.deletedMessage": "The connection has been deleted.",
"connection.addedAt": "Added {datetime}"
} }

View File

@@ -1,17 +1,20 @@
import * as React from 'react';
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { Link, Route, Navigate, Routes, useParams, useMatch, useNavigate } from 'react-router-dom'; import { Link, Route, Navigate, Routes, useParams, useMatch, useNavigate } from 'react-router-dom';
import type { LinkProps } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import Tabs from '@mui/material/Tabs'; import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab'; import Tab from '@mui/material/Tab';
import SettingsIcon from '@mui/icons-material/Settings'; import AddIcon from '@mui/icons-material/Add';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import { GET_APP } from 'graphql/queries/get-app'; import { GET_APP } from 'graphql/queries/get-app';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import ConditionalIconButton from 'components/ConditionalIconButton';
import AppConnections from 'components/AppConnections'; import AppConnections from 'components/AppConnections';
import AppFlows from 'components/AppFlows'; import AppFlows from 'components/AppFlows';
import AddAppConnection from 'components/AddAppConnection'; import AddAppConnection from 'components/AddAppConnection';
@@ -37,8 +40,9 @@ const ReconnectConnection = (props: any) => {
); );
} }
export default function Application() { export default function Application() {
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const connectionsPathMatch = useMatch({ path: URLS.APP_CONNECTIONS_PATTERN, end: false }); const connectionsPathMatch = useMatch({ path: URLS.APP_CONNECTIONS_PATTERN, end: false });
const flowsPathMatch = useMatch({ path: URLS.APP_FLOWS_PATTERN, end: false }); const flowsPathMatch = useMatch({ path: URLS.APP_FLOWS_PATTERN, end: false });
@@ -49,34 +53,91 @@ export default function Application() {
const goToApplicationPage = () => navigate('connections'); const goToApplicationPage = () => navigate('connections');
const app = data?.getApp || {}; const app = data?.getApp || {};
const NewConnectionLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
linkProps,
ref,
) {
return <Link ref={ref} to={URLS.APP_ADD_CONNECTION(appKey)} {...linkProps} />;
}),
[appKey],
);
const NewFlowLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
linkProps,
ref,
) {
return <Link ref={ref} to={URLS.APP_ADD_CONNECTION(appKey)} {...linkProps} />;
}),
[appKey],
);
return ( return (
<> <>
<Box sx={{ py: 3 }}> <Box sx={{ py: 3 }}>
<Container> <Container>
<Grid container sx={{ mb: 3 }}> <Grid container sx={{ mb: 3 }} alignItems="center">
<Grid item xs="auto" sx={{ mr: 1.5 }}> <Grid item xs="auto" sx={{ mr: 3 }}>
<AppIcon url={app.iconUrl} color={app.primaryColor} name={app.name} /> <AppIcon
url={app.iconUrl}
color={app.primaryColor}
name={app.name}
/>
</Grid> </Grid>
<Grid item xs> <Grid item xs>
<PageTitle>{app.name}</PageTitle> <PageTitle>{app.name}</PageTitle>
</Grid> </Grid>
<Grid item xs="auto" justifyContent="flex-end"> <Grid item xs="auto">
<IconButton sx={{ mr: 2 }} title={formatMessage('app.settings')}> <Routes>
<SettingsIcon /> <Route
</IconButton> path={`${URLS.FLOWS}/*`}
element={
<ConditionalIconButton
type="submit"
variant="contained"
color="primary"
size="large"
component={NewFlowLink}
fullWidth
icon={<AddIcon />}
>
{formatMessage('app.createFlow')}
</ConditionalIconButton>
}
/>
<Button variant="contained" component={Link} to={URLS.APP_ADD_CONNECTION(appKey)}> <Route
path={`${URLS.CONNECTIONS}/*`}
element={
<ConditionalIconButton
type="submit"
variant="contained"
color="primary"
size="large"
component={NewConnectionLink}
fullWidth
icon={<AddIcon />}
>
{formatMessage('app.addConnection')} {formatMessage('app.addConnection')}
</Button> </ConditionalIconButton>
}
/>
</Routes>
</Grid> </Grid>
</Grid> </Grid>
<Grid container> <Grid container>
<Grid item xs> <Grid item xs>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}> <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
<Tabs value={connectionsPathMatch?.pattern?.path || flowsPathMatch?.pattern?.path}> <Tabs
variant={matchSmallScreens ? 'fullWidth' : undefined}
value={connectionsPathMatch?.pattern?.path || flowsPathMatch?.pattern?.path}
>
<Tab <Tab
label={formatMessage('app.connections')} label={formatMessage('app.connections')}
to={URLS.APP_CONNECTIONS(appKey)} to={URLS.APP_CONNECTIONS(appKey)}

View File

@@ -6,7 +6,6 @@ import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import ConditionalIconButton from 'components/ConditionalIconButton'; import ConditionalIconButton from 'components/ConditionalIconButton';
import Container from 'components/Container'; import Container from 'components/Container';
import AddNewAppConnection from 'components/AddNewAppConnection'; import AddNewAppConnection from 'components/AddNewAppConnection';

View File

@@ -1,5 +1,5 @@
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Container from '@mui/material/Container'; import Container from 'components/Container';
export default function Explore() { export default function Explore() {
return ( return (

View File

@@ -180,6 +180,9 @@ const extendedTheme = createTheme({
styleOverrides: { styleOverrides: {
root: { root: {
background: 'rgba(0, 8, 20, 0.64)' background: 'rgba(0, 8, 20, 0.64)'
},
invisible: {
background: 'transparent',
} }
} }
}, },
@@ -243,6 +246,15 @@ const extendedTheme = createTheme({
} }
} }
}, },
MuiTab: {
styleOverrides: {
root: {
[referenceTheme.breakpoints.up('sm')]: {
padding: referenceTheme.spacing(1.5, 3),
}
}
},
},
MuiToolbar: { MuiToolbar: {
styleOverrides: { styleOverrides: {
root: { root: {

View File

@@ -7,6 +7,7 @@ type Connection = {
key: string; key: string;
data: ConnectionData; data: ConnectionData;
verified: boolean; verified: boolean;
createdAt: string;
}; };
export type { Connection, ConnectionData }; export type { Connection, ConnectionData };

View File

@@ -3206,6 +3206,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45"
integrity sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw== integrity sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==
"@types/luxon@^2.0.8":
version "2.0.8"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-2.0.8.tgz#a0fdd7ab0b67e08bf1d301232a7fef79b74ded69"
integrity sha512-lGmxL6hMEVqXr8w9bL52RUWXVu90o7vH8WQSutQssr2e+w0TNttXx2Zfw2V2lHHHWfW6OGqB8bXDvtKocv19qQ==
"@types/mime@^1": "@types/mime@^1":
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
@@ -10531,6 +10536,11 @@ lru-cache@^6.0.0:
dependencies: dependencies:
yallist "^4.0.0" yallist "^4.0.0"
luxon@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.2.0.tgz#f5c4a234ba4016f792488b11aaed2d5bc14c888e"
integrity sha512-LwmknessH4jVIseCsizUgveIHwlLv/RQZWC2uDSMfGJs7w8faPUi2JFxfyfMcTPrpNbChTem3Uz6IKRtn+LcIA==
lz-string@^1.4.4: lz-string@^1.4.4:
version "1.4.4" version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"