feat: add support for search filters

This commit is contained in:
Sean Yesmunt 2019-02-18 12:24:56 -05:00
parent 541f6fc34a
commit ff0478ade3
20 changed files with 316 additions and 177 deletions

View file

@ -34,6 +34,7 @@
"postinstall": "electron-builder install-app-deps && node build/downloadDaemon.js"
},
"dependencies": {
"@lbry/components": "^2.2.0",
"@types/three": "^0.93.1",
"bluebird": "^3.5.1",
"breakdance": "^3.0.1",
@ -52,7 +53,7 @@
"hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#42c185e922a7c6091b0e1580bacbfd8e02f45a91",
"lbry-redux": "lbryio/lbry-redux#3ab065b11a52d3e2e6a50a25459f9ff0aac03b13",
"lbryinc": "lbryio/lbryinc#60d80401891743f991c040bafa8e51da7e939777",
"localforage": "^1.7.1",
"mammoth": "^1.4.6",
@ -62,8 +63,8 @@
"node-fetch": "^2.3.0",
"qrcode.react": "^0.8.0",
"rc-progress": "^2.0.6",
"react": "^16.6.0",
"react-dom": "^16.6.0",
"react": "^16.8.2",
"react-dom": "^16.8.2",
"react-feather": "^1.0.8",
"react-modal": "^3.1.7",
"react-paginate": "^5.2.1",
@ -89,7 +90,6 @@
"y18n": "^4.0.0"
},
"devDependencies": {
"@lbry/components": "^2.2.0",
"babel-eslint": "^8.2.2",
"babel-plugin-module-resolver": "^3.1.1",
"babel-polyfill": "^6.26.0",

View file

@ -29,14 +29,16 @@ type Props = {
disabled?: boolean,
},
inputButton: ?React.Node,
blockWrap: boolean,
};
export class FormField extends React.PureComponent<Props> {
static defaultProps = {
labelOnLeft: false,
blockWrap: true,
};
constructor(props) {
constructor(props: Props) {
super(props);
this.input = React.createRef();
}
@ -66,31 +68,39 @@ export class FormField extends React.PureComponent<Props> {
autoFocus,
inputButton,
labelOnLeft,
blockWrap,
...inputProps
} = this.props;
const errorMessage = typeof error === 'object' ? error.message : error;
const Wrapper = blockWrap
? ({ children: innerChildren }) => <fieldset-section>{innerChildren}</fieldset-section>
: ({ children: innerChildren }) => <React.Fragment>{innerChildren}</React.Fragment>;
let input;
if (type) {
if (type === 'radio') {
input = (
<fieldset-section>
<Wrapper>
<radio-element>
<input id={name} type="radio" {...inputProps} />
<label htmlFor={name}>{label}</label>
<radio-toggle onClick={inputProps.onChange} />
</radio-element>
</fieldset-section>
</Wrapper>
);
} else if (type === 'checkbox') {
// web components treat props weird
// we need to fully remove it for proper component:attribute css styling
const elementProps = inputProps.disabled ? { disabled: true } : {};
input = (
<fieldset-section>
<checkbox-element>
<Wrapper>
<checkbox-element {...elementProps}>
<input id={name} type="checkbox" {...inputProps} />
<label htmlFor={name}>{label}</label>
<checkbox-toggle onClick={inputProps.onChange} />
</checkbox-element>
</fieldset-section>
</Wrapper>
);
} else if (type === 'setting') {
// 'setting' should only be used for settings. Forms should use "checkbox"

View file

@ -1,9 +1,14 @@
import { connect } from 'react-redux';
import { makeSelectSearchUris, selectIsSearching, selectSearchDownloadUris } from 'lbry-redux';
import {
makeSelectSearchUris,
selectIsSearching,
selectSearchDownloadUris,
makeSelectQueryWithOptions,
} from 'lbry-redux';
import FileListSearch from './view';
const select = (state, props) => ({
uris: makeSelectSearchUris(props.query)(state),
uris: makeSelectSearchUris(makeSelectQueryWithOptions()(state))(state),
downloadUris: selectSearchDownloadUris(props.query)(state),
isSearching: selectIsSearching(state),
});

View file

@ -11,26 +11,11 @@ type Props = {
query: string,
isSearching: boolean,
uris: ?Array<string>,
downloadUris: ?Array<string>,
};
class FileListSearch extends React.PureComponent<Props> {
render() {
const { uris, query, downloadUris, isSearching } = this.props;
const fileResults = [];
const channelResults = [];
if (uris && uris.length) {
uris.forEach(uri => {
const isChannel = parseURI(uri).claimName[0] === '@';
if (isChannel) {
channelResults.push(uri);
} else {
fileResults.push(uri);
}
});
}
const { uris, query, isSearching } = this.props;
return (
query && (
<React.Fragment>
@ -38,26 +23,15 @@ class FileListSearch extends React.PureComponent<Props> {
<section className="search__results-section">
<div className="search__results-title">{__('Search Results')}</div>
<HiddenNsfwClaims uris={uris} />
{!isSearching && fileResults.length ? (
fileResults.map(uri => <FileTile key={uri} uri={uri} />)
{!isSearching && uris && uris.length ? (
uris.map(
uri =>
parseURI(uri).claimName[0] === '@' ? (
<ChannelTile key={uri} uri={uri} />
) : (
<NoResults />
)}
</section>
<section className="search__results-section">
<div className="search__results-title">{__('Channels')}</div>
{!isSearching && channelResults.length ? (
channelResults.map(uri => <ChannelTile key={uri} uri={uri} />)
) : (
<NoResults />
)}
</section>
<section className="search__results-section">
<div className="search__results-title">{__('Your downloads')}</div>
{downloadUris && downloadUris.length ? (
downloadUris.map(uri => <FileTile hideNoResult key={uri} uri={uri} />)
<FileTile key={uri} uri={uri} />
)
)
) : (
<NoResults />
)}

View file

@ -156,10 +156,12 @@ class FileTile extends React.PureComponent<Props> {
<Fragment>
<div className="media__title">
{(title || name) && (
<TruncatedText text={title || name} lines={size === 'small' ? 2 : 3} />
<TruncatedText text={title || name} lines={size !== 'small' ? 1 : 2} />
)}
</div>
{size === 'small' && this.renderFileProperties()}
{size !== 'small' ? (
<div className="media__subtext">
{__('Published to')} <UriIndicator uri={uri} link />{' '}
@ -169,10 +171,10 @@ class FileTile extends React.PureComponent<Props> {
<Fragment>
<div className="media__subtext">
<UriIndicator uri={uri} link />
</div>
<div className="media__subtext">
<div>
<DateTime timeAgo block={height} />
</div>
</div>
</Fragment>
)}
</Fragment>
@ -184,7 +186,7 @@ class FileTile extends React.PureComponent<Props> {
</div>
)}
{this.renderFileProperties()}
{size !== 'small' && this.renderFileProperties()}
{!name && (
<Yrbl

View file

@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { selectSearchOptions, doUpdateSearchOptions } from 'lbry-redux';
import SearchOptions from './view';
const select = state => ({
options: selectSearchOptions(state),
});
const perform = dispatch => ({
setSearchOption: (option, value) => dispatch(doUpdateSearchOptions({ [option]: value })),
});
export default connect(
select,
perform
)(SearchOptions);

View file

@ -0,0 +1,123 @@
// @flow
import * as ICONS from 'constants/icons';
import React, { useState } from 'react';
import { SEARCH_OPTIONS } from 'lbry-redux';
import { Form, FormField } from 'component/common/form';
import posed from 'react-pose';
import Button from 'component/button';
const ExpandableOptions = posed.div({
hide: { height: 0, opacity: 0 },
show: { height: 280, opacity: 1 },
});
type Props = {
setSearchOption: (string, boolean | string | number) => void,
options: {},
};
const SearchOptions = (props: Props) => {
const { options, setSearchOption } = props;
const [expanded, setExpanded] = useState(false);
const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT];
return (
<div className="card card--section search__options-wrapper">
<div className="card--space-between">
<Button
label={__('SEARCH OPTIONS')}
icon={ICONS.OPTIONS}
onClick={() => setExpanded(!expanded)}
/>
{/*
Will be added back when api is ready
<div className="media__action-group">
<span>{__('Find what you were looking for?')}</span>
<Button description={__('Yes')} icon={ICONS.YES} />
<Button description={__('No')} icon={ICONS.NO} />
</div> */}
</div>
<ExpandableOptions pose={expanded ? 'show' : 'hide'}>
{expanded && (
<Form className="card__content search__options">
<fieldset>
<legend className="search__legend--1">{__('Search For')}</legend>
{[
{
option: SEARCH_OPTIONS.INCLUDE_FILES,
label: __('Files'),
},
{
option: SEARCH_OPTIONS.INCLUDE_CHANNELS,
label: __('Channels'),
},
{
option: SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS,
label: __('Everything'),
},
].map(({ option, label }) => (
<FormField
key={option}
type="radio"
blockWrap={false}
label={label}
checked={options[SEARCH_OPTIONS.CLAIM_TYPE] === option}
onChange={() => setSearchOption(SEARCH_OPTIONS.CLAIM_TYPE, option)}
/>
))}
</fieldset>
<fieldset>
<legend className="search__legend--2">{__('File Types')}</legend>
{[
{
option: SEARCH_OPTIONS.MEDIA_VIDEO,
label: __('Videos'),
},
{
option: SEARCH_OPTIONS.MEDIA_AUDIO,
label: __('Sounds'),
},
{
option: SEARCH_OPTIONS.MEDIA_IMAGE,
label: __('Images'),
},
{
option: SEARCH_OPTIONS.MEDIA_TEXT,
label: __('Text'),
},
{
option: SEARCH_OPTIONS.MEDIA_APPLICATION,
label: __('Other Files'),
},
].map(({ option, label }) => (
<FormField
key={option}
type="checkbox"
blockWrap={false}
disabled={options[SEARCH_OPTIONS.CLAIM_TYPE] === SEARCH_OPTIONS.INCLUDE_CHANNELS}
label={label}
checked={options[option]}
onChange={() => setSearchOption(option, !options[option])}
/>
))}
</fieldset>
<fieldset>
<legend className="search__legend--3">{__('Other Options')}</legend>
<FormField
type="number"
value={resultCount}
onChange={e => setSearchOption(SEARCH_OPTIONS.RESULT_COUNT, e.target.value)}
blockWrap={false}
label={__('Returned Results')}
/>
</fieldset>
</Form>
)}
</ExpandableOptions>
</div>
);
};
export default SearchOptions;

View file

@ -10,8 +10,6 @@ import {
doToast,
} from 'lbry-redux';
import analytics from 'analytics';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import * as settings from 'constants/settings';
import { doNavigate } from 'redux/actions/navigation';
import Wunderbar from './view';
@ -27,13 +25,12 @@ const select = state => {
...searchState,
wunderbarValue,
suggestions: selectSearchSuggestions(state),
resultCount: makeSelectClientSetting(settings.RESULT_COUNT)(state),
};
};
const perform = dispatch => ({
onSearch: (query, size) => {
dispatch(doSearch(query, size));
onSearch: query => {
dispatch(doSearch(query));
dispatch(doNavigate(`/search`, { query }));
analytics.apiLogSearch();
},

View file

@ -12,13 +12,12 @@ const ESC_KEY_CODE = 27;
type Props = {
updateSearchQuery: string => void,
onSearch: (string, ?number) => void,
onSearch: string => void,
onSubmit: (string, {}) => void,
wunderbarValue: ?string,
suggestions: Array<string>,
doFocus: () => void,
doBlur: () => void,
resultCount: number,
focused: boolean,
doShowSnackBar: ({}) => void,
};
@ -82,7 +81,7 @@ class WunderBar extends React.PureComponent<Props> {
}
handleSubmit(value: string, suggestion?: { value: string, type: string }) {
const { onSubmit, onSearch, resultCount } = this.props;
const { onSubmit, onSearch } = this.props;
const query = value.trim();
const getParams = () => {
const parts = query.split('?');
@ -98,7 +97,7 @@ class WunderBar extends React.PureComponent<Props> {
// User selected a suggestion
if (suggestion) {
if (suggestion.type === 'search') {
onSearch(query, resultCount);
onSearch(query);
} else if (isURIValid(query)) {
const params = getParams();
const uri = normalizeURI(query);
@ -125,7 +124,7 @@ class WunderBar extends React.PureComponent<Props> {
});
}
} catch (e) {
onSearch(query, resultCount);
onSearch(query);
}
}

View file

@ -41,3 +41,6 @@ export const WALLET = 'CreditCard';
export const SETTINGS = 'Settings';
export const INVITE = 'Users';
export const FILE = 'File';
export const OPTIONS = 'Sliders';
export const YES = 'ThumbsUp';
export const NO = 'ThumbsDown';

View file

@ -1,22 +1,16 @@
import { connect } from 'react-redux';
import * as settings from 'constants/settings';
import { selectIsSearching, makeSelectCurrentParam, doUpdateSearchQuery } from 'lbry-redux';
import { doSetClientSetting } from 'redux/actions/settings';
import { doNavigate } from 'redux/actions/navigation';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import SearchPage from './view';
const select = state => ({
isSearching: selectIsSearching(state),
query: makeSelectCurrentParam('query')(state),
showUnavailable: makeSelectClientSetting(settings.SHOW_UNAVAILABLE)(state),
resultCount: makeSelectClientSetting(settings.RESULT_COUNT)(state),
});
const perform = dispatch => ({
navigate: path => dispatch(doNavigate(path)),
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
});
export default connect(

View file

@ -1,5 +1,4 @@
// @flow
import * as SETTINGS from 'constants/settings';
import * as ICONS from 'constants/icons';
import * as React from 'react';
import { isURIValid, normalizeURI, parseURI } from 'lbry-redux';
@ -9,32 +8,15 @@ import FileListSearch from 'component/fileListSearch';
import Page from 'component/page';
import ToolTip from 'component/common/tooltip';
import Icon from 'component/common/icon';
import SearchOptions from 'component/searchOptions';
type Props = {
query: ?string,
resultCount: number,
setClientSetting: (string, number | boolean) => void,
};
class SearchPage extends React.PureComponent<Props> {
constructor() {
super();
(this: any).onShowUnavailableChange = this.onShowUnavailableChange.bind(this);
(this: any).onSearchResultCountChange = this.onSearchResultCountChange.bind(this);
}
onSearchResultCountChange(event: SyntheticInputEvent<*>) {
const count = Number(event.target.value);
this.props.setClientSetting(SETTINGS.RESULT_COUNT, count);
}
onShowUnavailableChange(event: SyntheticInputEvent<*>) {
this.props.setClientSetting(SETTINGS.SHOW_UNAVAILABLE, event.target.checked);
}
render() {
const { query, resultCount } = this.props;
const { query } = this.props;
const isValid = isURIValid(query);
let uri;
@ -69,14 +51,9 @@ class SearchPage extends React.PureComponent<Props> {
</header>
)}
{/*
Commented out until I figure out what to do with it in my next PR
<div>
<FormField type="text" value={resultCount} label={__("Returned results")} /
</div>
*/}
<div className="search__results-wrapper">
<SearchOptions />
<FileListSearch query={query} />
<div className="help">{__('These search results are provided by LBRY, Inc.')}</div>
</div>

View file

@ -97,7 +97,11 @@
}
.button--uri-indicator {
color: rgba($lbry-white, 0.9);
width: 100%;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s;
&:hover {

View file

@ -2,6 +2,17 @@
// lbry/components overrides and minor styles
input[type='number'] {
padding: var(--spacing-s);
width: 8em;
}
checkbox-element {
&[disabled='true'] {
opacity: 0.3;
}
}
checkbox-element,
radio-element,
fieldset:last-child,
@ -9,6 +20,21 @@ fieldset-section:last-child {
margin-bottom: 0;
}
checkbox-element,
radio-element {
input[type='checkbox']:checked + label {
color: $lbry-black;
[data-mode='dark'] & {
color: $lbry-white;
&:hover {
color: $lbry-teal-4;
}
}
}
}
fieldset-group.fieldset-group--smushed {
justify-content: flex-start;
@ -41,6 +67,10 @@ form {
background-color: $lbry-teal-5;
border-color: $lbry-teal-5;
}
legend {
background-color: $lbry-cyan-5;
}
}
}

View file

@ -14,6 +14,7 @@
.media-tile {
display: flex;
font-size: 1.5rem;
position: relative;
&:not(:last-of-type) {
margin-bottom: var(--spacing-vertical-large);
@ -26,6 +27,7 @@
.media__info {
margin-left: var(--spacing-vertical-medium);
width: calc(80% - 20rem);
min-width: 40rem;
}
}
@ -38,7 +40,7 @@
.media__info {
margin-left: var(--spacing-vertical-large);
width: calc(80% - 30rem);
flex: 1;
}
.media__subtext {
@ -54,21 +56,26 @@
}
.media__thumb {
width: 10em;
width: 11em;
}
.media__info {
// padding-left: var(--spacing-vertical-medium);
width: calc(100% - 10em);
min-width: auto;
position: relative;
}
.media__title {
margin-bottom: var(--spacing-vertical-small);
}
.media__subtext:last-child {
margin-bottom: 0;
}
.media__properties {
bottom: -1.5rem;
left: calc(-100% - 1.5rem);
bottom: 0.5rem;
left: calc(-100% - -2rem);
position: absolute;
padding: 0 var(--spacing-vertical-small);
border-radius: 5px;
@ -143,7 +150,7 @@
.media__action-group {
> *:not(:last-child) {
margin-right: var(--spacing-vertical-large);
margin-right: var(--spacing-vertical-medium);
}
}
@ -193,6 +200,11 @@
.media__subtitle {
font-size: 0.8em;
color: rgba($lbry-black, 0.8);
[data-mode='dark'] & {
color: rgba($lbry-white, 0.8);
}
}
.media__subtitle--large {
@ -412,7 +424,7 @@
color: $lbry-white;
}
.media__subtext {
.media__subtitle {
color: mix($lbry-cyan-5, $lbry-white, 20%);
}
}

View file

@ -12,6 +12,10 @@
color: rgba($lbry-white, 0.6);
}
.media__subtitle {
color: rgba($lbry-white, 0.9);
}
html[data-mode='dark'] & {
background-color: transparent;
border-bottom: 1px solid rgba($lbry-white, 0.1);
@ -46,3 +50,43 @@
@extend .media-group__header-title;
margin-bottom: var(--spacing-vertical-large);
}
.search__options-wrapper {
font-size: 1.25em;
}
.search__options {
margin-top: var(--spacing-vertical-large);
legend {
&.search__legend--1 {
background-color: $lbry-teal-1;
}
&.search__legend--2 {
background-color: $lbry-cyan-1;
}
&.search__legend--3 {
background-color: $lbry-pink-1;
}
[data-mode='dark'] & {
&.search__legend--1 {
background-color: $lbry-teal-5;
}
&.search__legend--2 {
background-color: $lbry-cyan-5;
}
&.search__legend--3 {
background-color: $lbry-pink-5;
}
}
}
fieldset:not(:first-child) {
margin-top: var(--spacing-vertical-large);
}
}

View file

@ -86,7 +86,9 @@ code {
}
.truncated-text {
@include truncate;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
}
.busy-indicator__loader {

View file

@ -1,30 +1,3 @@
@mixin between {
display: flex;
justify-content: space-between;
}
@mixin ellipsis {
// to take over for truncate on LBRY Web
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin font-mono {
font-family: Inconsolata, 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', Consolas,
'Lucida Console', 'Courier New', Courier, monospace;
}
@mixin font-sans {
font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}
@mixin font-serif {
font-family: 'Apple Garamond', Baskerville, Georgia, 'Times New Roman', 'Droid Serif', Times,
'Source Serif Pro', serif;
}
@mixin placeholder {
animation: pulse 2s infinite ease-in-out;
background-color: $lbry-gray-2;
@ -33,26 +6,3 @@
background-color: rgba($lbry-white, 0.1);
}
}
@mixin thumbnail {
&::before,
&::after {
content: '';
}
&::before {
float: left;
padding-top: var(--video-aspect-ratio);
}
&::after {
clear: both;
display: block;
}
}
@mixin truncate {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
}

View file

@ -102,12 +102,13 @@ const fileInfoFilter = createFilter('fileInfo', [
const appFilter = createFilter('app', ['hasClickedComment']);
// We only need to persist the receiveAddress for the wallet
const walletFilter = createFilter('wallet', ['receiveAddress']);
const searchFilter = createFilter('search', ['options']);
const persistOptions = {
whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo', 'app'],
whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo', 'app', 'search'],
// Order is important. Needs to be compressed last or other transforms can't
// read the data
transforms: [walletFilter, contentFilter, fileInfoFilter, appFilter, compressor],
transforms: [walletFilter, contentFilter, fileInfoFilter, appFilter, searchFilter, compressor],
debounce: 10000,
storage: localForage,
};

View file

@ -5660,9 +5660,9 @@ lazy-val@^1.0.3:
tar-stream "^1.6.2"
zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#42c185e922a7c6091b0e1580bacbfd8e02f45a91:
lbry-redux@lbryio/lbry-redux#2b725cb31729234ba73117e2a74688b8bba26e7c:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/42c185e922a7c6091b0e1580bacbfd8e02f45a91"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/2b725cb31729234ba73117e2a74688b8bba26e7c"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"
@ -7868,14 +7868,15 @@ rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-dom@^16.6.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.7.0.tgz#a17b2a7ca89ee7390bc1ed5eb81783c7461748b8"
react-dom@^16.8.2:
version "16.8.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.2.tgz#7c8a69545dd554d45d66442230ba04a6a0a3c3d3"
integrity sha512-cPGfgFfwi+VCZjk73buu14pYkYBR1b/SRMSYqkLDdhSEHnSwcuYTPu6/Bh6ZphJFIk80XLvbSe2azfcRzNF+Xg==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.12.0"
scheduler "^0.13.2"
react-feather@^1.0.8:
version "1.1.1"
@ -7952,14 +7953,15 @@ react@^0.14.5:
envify "^3.0.0"
fbjs "^0.6.1"
react@^16.6.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381"
react@^16.8.2:
version "16.8.2"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.2.tgz#83064596feaa98d9c2857c4deae1848b542c9c0c"
integrity sha512-aB2ctx9uQ9vo09HVknqv3DGRpI7OIGJhCx3Bt0QqoRluEjHSaObJl+nG12GDdYH6sTgE7YiPJ6ZUyMx9kICdXw==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.12.0"
scheduler "^0.13.2"
read-config-file@3.1.0, read-config-file@^3.0.0:
version "3.1.0"
@ -8532,13 +8534,6 @@ sass-loader@^6.0.7:
neo-async "^2.5.0"
pify "^3.0.0"
sass@^1.17.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.17.0.tgz#e370b9302af121c9eadad5639619127772094ae6"
integrity sha512-aFi9RQqrCYkHB2DaLKBBbdUhos1N5o3l1ke9N5JqWzgSPmYwZsdmA+ViPVatUy/RPA21uejgYVUXM7GCh8lcdw==
dependencies:
chokidar "^2.0.0"
sax@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
@ -8551,9 +8546,10 @@ sax@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240"
scheduler@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0.tgz#8ab17699939c0aedc5a196a657743c496538647b"
scheduler@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.2.tgz#969eaee2764a51d2e97b20a60963b2546beff8fa"
integrity sha512-qK5P8tHS7vdEMCW5IPyt8v9MJOHqTrOUgPXib7tqm9vh834ibBX5BNhwkplX/0iOzHW5sXyluehYfS9yrkz9+w==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"