Advertisement
Guest User

Untitled

a guest
May 27th, 2019
129
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 21.90 KB | None | 0 0
  1. import React, { HTMLProps } from 'react';
  2. import { ClipboardEvent, Component, ComponentClass, FunctionComponent } from 'react';
  3. import { connect } from 'react-redux';
  4. import { styled } from 'styletron-react';
  5.  
  6. import { withTheme } from '@benzinga/themetron';
  7. import DatePicker from 'antd/lib/date-picker';
  8. import { RangePickerValue } from 'antd/lib/date-picker/interface';
  9. import Radio from 'antd/lib/radio';
  10. import { RadioChangeEvent } from 'antd/lib/radio/interface';
  11. import Select from 'antd/lib/select';
  12. import Slider from 'antd/lib/slider';
  13. import invariant from 'invariant';
  14. import moment from 'moment-timezone';
  15. import { addIndex, contains, filter, find, isEmpty, isNil, join, map, pipe, prop, propEq, reduce } from 'ramda';
  16.  
  17. import { moversUpdateParameters } from '../../../../actions/moversActions';
  18. import { sagaScreenerCheckAutoRefresh, sagaScreenerRequestMovers } from '../../../../actions/sagaTriggers';
  19. import {
  20. ChevronDownIcon,
  21. ChevronRightIcon,
  22. DebugIcon,
  23. TrendingDownIcon,
  24. TrendingUpIcon
  25. } from '../../../../assets/icons/';
  26. import { MarketSession } from '../../../../entities/marketEntity';
  27. import { RootState } from '../../../../entities/rootEntity';
  28. import { WidgetId } from '../../../../entities/widgetEntity';
  29. import { MoversWidgetParameters, MoversWidgetTransient } from '../../../../entities/widgetEntity/screenerWidgetEntity';
  30. import { selectDebugMode } from '../../../../selectors/appSelectors';
  31. import { selectWidgetById } from '../../../../selectors/widgetSelectors';
  32. import { Column, ColumnStyles, Div, Inline, InlineStyles, RowStyles } from '../../../../styles/StyledHOCs';
  33. import { MECSSectorCode, NBSP } from '../../../../utils/global';
  34. import {
  35. createMoversURL,
  36. intraSessionIntervals,
  37. marketCapMarks,
  38. moversSectorOptions,
  39. moversTimeFormat,
  40. outOfSessionIntervals,
  41. priceMarks,
  42. } from '../../../../utils/moversUtils';
  43. import NoResults from '../../../search/NoResults';
  44. import Spinner from '../../../ui/Spinner';
  45. import MoversGrid from './MoversGrid';
  46.  
  47. export interface OwnProps {
  48. widgetId: WidgetId;
  49. withQuoteSubscription(props: string[]): Component<any>; // TODO: type more accurately
  50. }
  51.  
  52. export interface ReduxState {
  53. debugMode: boolean;
  54. parameters: MoversWidgetParameters;
  55. transient: MoversWidgetTransient;
  56. }
  57.  
  58. export interface DispatchableActions {
  59. moversUpdateParameters: typeof moversUpdateParameters;
  60. sagaScreenerCheckAutoRefresh: typeof sagaScreenerCheckAutoRefresh;
  61. sagaScreenerRequestMovers: typeof sagaScreenerRequestMovers,
  62. }
  63.  
  64. type Props = Readonly<OwnProps & ReduxState & DispatchableActions>;
  65.  
  66. const reduceIndexed: <TResult, T>
  67. (func: (acc: TResult, property: T, index: number) => TResult, acc: TResult, values: T[]) =>
  68. TResult = addIndex(reduce);
  69.  
  70. const { Button: RadioButton, Group: RadioGroup } = Radio;
  71. const { Option } = Select;
  72. const { RangePicker } = DatePicker;
  73.  
  74. const relativeTimeFormat = {
  75. lastDay: '[Yesterday]',
  76. lastWeek: 'dddd, MMMM Do YYYY',
  77. nextDay: '[Tomorrow]',
  78. nextWeek: 'dddd, MMMM Do YYYY',
  79. sameDay: '[Today]',
  80. sameElse: 'dddd, MMMM Do YYYY',
  81. };
  82.  
  83. const Parameter = styled('div', {
  84. ...RowStyles,
  85. alignItems: 'center',
  86. flexWrap: 'wrap',
  87. padding: '0.5em 1.5em',
  88. });
  89.  
  90. const ParameterLabel = styled('div', {
  91. ...RowStyles,
  92. alignItems: 'center',
  93. display: 'inline-flex',
  94. margin: '0.5em 0',
  95. minWidth: '100px',
  96. whiteSpace: 'nowrap',
  97. });
  98.  
  99. const DatePickerContainer = styled('div', {
  100. ...InlineStyles,
  101. paddingTop: '0.25em',
  102. });
  103.  
  104. const SliderContainer = styled('div', {
  105. ...InlineStyles,
  106. flexGrow: 1,
  107. height: '30px',
  108. marginTop: '-20px',
  109. maxWidth: '300px',
  110. width: '100%',
  111. });
  112.  
  113. const MoversContainer = styled('div', {
  114. ...ColumnStyles,
  115. flex: 1,
  116. overflowX: 'hidden',
  117. overflowY: 'auto',
  118. });
  119.  
  120. const WidgetBody = styled('div', {
  121. ...ColumnStyles,
  122. flex: 1,
  123. overflow: 'auto',
  124. });
  125.  
  126. const StyledSelect = styled(Select, {
  127. minWidth: '225px',
  128. });
  129.  
  130. interface ParametersHeaderProps {
  131. $expanded: boolean;
  132. onClick(): void;
  133. }
  134.  
  135. const ParametersHeader = withTheme<ParametersHeaderProps, HTMLProps<HTMLDivElement>>((props, theme) => ({
  136. ...InlineStyles,
  137. backgroundColor: props.$expanded ? theme.colors.brandMuted : 'transparent',
  138. color: props.$expanded ? theme.colors.brandForeground : theme.colors.foregroundInactive,
  139. cursor: 'pointer',
  140. fill: theme.colors.foregroundInactive,
  141. padding: '0.5em',
  142. }))(Div);
  143.  
  144. interface MoversParametersProps {
  145. $expanded: boolean;
  146. }
  147.  
  148. const MoversParameters = withTheme<MoversParametersProps, HTMLProps<HTMLDivElement>>((props, theme) => ({
  149. borderBottom: props.$expanded ? `2px solid ${theme.colors.brandMuted}` : 'none',
  150. borderLeft: `2px solid ${theme.colors.brandMuted}`,
  151. borderRight: props.$expanded ? `2px solid ${theme.colors.brandMuted}` : 'none',
  152. borderTop: props.$expanded ? `2px solid ${theme.colors.brandMuted}` : 'none',
  153. flex: '0 0 auto',
  154. }))(Column);
  155.  
  156. const Emphasis = withTheme((_, theme) => ({
  157. color: theme.colors.foregroundActive,
  158. whiteSpace: 'nowrap',
  159. }))(Inline);
  160.  
  161. const DatesContainerLabel = withTheme((_, theme) => ({
  162. color: theme.colors.foregroundInactive,
  163. fontSize: '0.825em',
  164. marginTop: '0.25em',
  165. }))(Div);
  166.  
  167. const GreenText = withTheme((_, theme) => ({
  168. color: theme.colors.statistic.positive,
  169. fill: theme.colors.statistic.positive,
  170. }))(Inline);
  171.  
  172. const RedText = withTheme((_, theme) => ({
  173. color: theme.colors.statistic.negative,
  174. fill: theme.colors.statistic.negative,
  175. }))(Inline);
  176.  
  177. const StyledTrendingUpIcon = withTheme((_, theme) => ({
  178. fill: theme.colors.statistic.positive,
  179. }))(TrendingUpIcon);
  180.  
  181. const StyledTrendingDownIcon = withTheme((_, theme) => ({
  182. fill: theme.colors.statistic.negative,
  183. }))(TrendingDownIcon);
  184.  
  185. const chevronIconEnhancer = withTheme((_, theme) => ({
  186. fill: theme.colors.brandForeground,
  187. marginRight: '0.25em',
  188. }));
  189.  
  190. const StyledChevronDownIcon = chevronIconEnhancer(ChevronDownIcon);
  191. const StyledChevronRightIcon = chevronIconEnhancer(ChevronRightIcon);
  192.  
  193. const StyledSpinner = styled(Spinner, {
  194. display: 'inline-flex',
  195. fontSize: '90%',
  196. marginLeft: '0.5em',
  197. });
  198.  
  199. const marketCapTuple: ['minimumMarketCap', 'maximumMarketCap'] = ['minimumMarketCap', 'maximumMarketCap'];
  200. const priceTuple: ['minimumPrice', 'maximumPrice'] = ['minimumPrice', 'maximumPrice'];
  201.  
  202. class Movers extends Component<Props> {
  203. componentDidMount() {
  204. const { sagaScreenerCheckAutoRefresh } = this.props;
  205.  
  206. this.loadMoversData();
  207. sagaScreenerCheckAutoRefresh();
  208. }
  209.  
  210. componentWillUnmount() {
  211. const { sagaScreenerCheckAutoRefresh } = this.props;
  212.  
  213. sagaScreenerCheckAutoRefresh();
  214. }
  215.  
  216. loadMoversData = () => {
  217. const {
  218. sagaScreenerRequestMovers,
  219. widgetId,
  220. } = this.props;
  221.  
  222. sagaScreenerRequestMovers(widgetId);
  223. };
  224.  
  225. updateParameters = (parameter: Partial<MoversWidgetParameters>) => () => {
  226. const {
  227. moversUpdateParameters,
  228. parameters: {
  229. interval,
  230. session,
  231. },
  232. sagaScreenerCheckAutoRefresh,
  233. widgetId,
  234. } = this.props;
  235.  
  236. let parameters: Partial<MoversWidgetParameters> = {
  237. ...parameter,
  238. };
  239.  
  240. const updatedSession = Boolean(parameters.session);
  241. if (updatedSession && session === 'REGULAR') {
  242. const isIntervalOutOfSession = !find(propEq('id', interval), outOfSessionIntervals);
  243. if (isIntervalOutOfSession) {
  244. parameters = {
  245. ...parameters,
  246. interval: 'session',
  247. };
  248. }
  249. }
  250.  
  251. moversUpdateParameters(widgetId, parameters);
  252.  
  253. // If updating autorefresh property, check if the autorefreshing saga should start/stop
  254. if (!isNil(parameters.autoRefresh)) {
  255. sagaScreenerCheckAutoRefresh();
  256. }
  257.  
  258. // Dont load movers data if turning autoRefresh off or expanding/minimising filtersExpanded
  259. const dontLoadMoversData = parameters.autoRefresh === false || !isNil(parameters.filtersExpanded);
  260. if (dontLoadMoversData) {
  261. return;
  262. }
  263.  
  264. this.loadMoversData();
  265. };
  266.  
  267. onPaste = (nextDate: 'fromDate' | 'toDate') => (event: ClipboardEvent<HTMLElement>) => {
  268. event.preventDefault();
  269. const clipData = event.clipboardData.getData('text');
  270. const momentDate = moment(clipData).format('MM/DD/YYYY');
  271. if (momentDate === 'Invalid date') {
  272. // eslint-disable-next-line
  273. return;
  274. }
  275. };
  276.  
  277. onDayPickerInputChange = (nextDate: 'fromDate' | 'toDate') => (date, { disabled }) => {
  278. if (disabled || !date) {
  279. return;
  280. }
  281.  
  282. const dateToMoment = moment(date);
  283.  
  284. this.updateParameters({
  285. interval: 'custom',
  286. [nextDate]: dateToMoment.format(moversTimeFormat),
  287. })();
  288. };
  289.  
  290. renderCollapsedFilterMenu() {
  291. const {
  292. parameters: {
  293. gainersLosersDir,
  294. interval,
  295. session,
  296. fromDate,
  297. toDate,
  298. },
  299. transient: {
  300. loading,
  301. },
  302. } = this.props;
  303.  
  304. const emphasize = (input: string | JSX.Element | null) => (
  305. <Emphasis>{input}</Emphasis>
  306. );
  307.  
  308. const gainersBlurb = <GreenText><StyledTrendingUpIcon /> Gainers</GreenText>;
  309. const losersBlurb = <RedText><StyledTrendingDownIcon /> Losers</RedText>;
  310.  
  311. const screenerType = {
  312. both: <span>{gainersBlurb}{' & '}{losersBlurb}</span>,
  313. gainers: gainersBlurb,
  314. losers: losersBlurb,
  315. }[gainersLosersDir];
  316.  
  317. const sessionType = {
  318. AFTER_HOURS: <span>of the {emphasize(MarketSession.afterHours)} session</span>,
  319. PRE_MARKET: <span>of the {emphasize(MarketSession.preMarket)}</span>,
  320. REGULAR: <span>of the {emphasize(MarketSession.regular)} session</span>,
  321. }[session];
  322.  
  323. // TODO it would be great to statically type the keys of this object against MoversInterval once typescript 2.7 drops
  324. const dateRange = {
  325. '-15m': <span>over the last {emphasize('15 minutes')}</span>,
  326. '-180d': <span>over the last {emphasize('6 months')}</span>,
  327. '-1w': <span>over the last {emphasize('week')}</span>,
  328. '-30d': <span>over the last {emphasize('month')}</span>,
  329. '-30m': <span>over the last {emphasize('30 minutes')}</span>,
  330. '-60m': <span>over the last {emphasize('hour')}</span>,
  331. '-90d': <span>over last {emphasize('3 months')}</span>,
  332. YTD: <span>over the {emphasize('Year to Date')}</span>,
  333. custom: (
  334. <span>
  335. from {emphasize(moment(fromDate || '').format('MM/DD/YYYY'))}
  336. {NBSP}to {emphasize(moment(toDate || '').format('MM/DD/YYYY'))}
  337. </span>
  338. ),
  339. session: <span>over this {emphasize('session')}</span>,
  340. }[interval];
  341.  
  342. return (
  343. <span>
  344. : {screenerType}
  345. {' | '}{emphasize(this.getSectorsLabel())}
  346. {' | '}{dateRange}{NBSP}{sessionType}
  347. {loading && <StyledSpinner />}
  348. </span>
  349. );
  350. }
  351.  
  352. getSectorsLabel() {
  353. const { sectors } = this.props.parameters;
  354. let sectorLabel: string | null = null;
  355. const matchedSectors = filter(
  356. option => contains(option.id, sectors),
  357. moversSectorOptions,
  358. );
  359. if (!isEmpty(matchedSectors)) {
  360. sectorLabel = pipe(
  361. map(prop('name')),
  362. join(', '),
  363. )(matchedSectors);
  364. }
  365. return sectorLabel;
  366. }
  367.  
  368. // takes a tuple of keys, which are then updated by the rc-slider's onAfterChange callback
  369. handleRangeChange =
  370. <T extends [keyof MoversWidgetParameters, keyof MoversWidgetParameters], K extends [number, number]>
  371. (properties: T) => (values: K) => {
  372. // tslint:disable-next-line:max-line-length
  373. const parameters: Partial<MoversWidgetParameters> = reduceIndexed<Partial<MoversWidgetParameters>, keyof MoversWidgetParameters>(
  374. (accumulator: Partial<MoversWidgetParameters>, property: keyof MoversWidgetParameters, index: number) => {
  375. accumulator[property] = values[index];
  376. return accumulator;
  377. },
  378. {},
  379. properties,
  380. );
  381. this.updateParameters(parameters)();
  382. };
  383.  
  384. handleGainersLosersChange = (event: RadioChangeEvent) => {
  385. const { target: { value } } = event;
  386. invariant(
  387. value === 'gainers' ||
  388. value === 'losers' ||
  389. value === 'both',
  390. 'Invalid value for gainers/losers radio group',
  391. );
  392. const gainersLosersDir = value as 'gainers' | 'losers' | 'both';
  393.  
  394. this.updateParameters({ gainersLosersDir })();
  395. };
  396.  
  397. handleSessionChange = (event: RadioChangeEvent) => {
  398. const { target: { value } } = event;
  399. invariant(
  400. value === 'PRE_MARKET' ||
  401. value === 'REGULAR' ||
  402. value === 'AFTER_HOURS',
  403. 'Invalid value for session type radio group',
  404. );
  405. const session = value as 'PRE_MARKET' | 'REGULAR' | 'AFTER_HOURS';
  406.  
  407. this.updateParameters({ session })();
  408. };
  409.  
  410. handleIntervalChange = (event: RadioChangeEvent) => {
  411. this.updateParameters({ interval: event.target.value })();
  412. };
  413.  
  414. handleAutoRefreshChange = (event: RadioChangeEvent) => {
  415. const autoRefresh = Boolean(event.target.value);
  416. this.updateParameters({ autoRefresh })();
  417. };
  418.  
  419. handleRangePickerChange = (date: RangePickerValue) => {
  420. const [fromDate, toDate] = date;
  421.  
  422. if (fromDate && toDate) {
  423. this.updateParameters({
  424. fromDate: fromDate.format(moversTimeFormat),
  425. toDate: toDate.format(moversTimeFormat),
  426. })();
  427. }
  428. };
  429.  
  430. handleSectorFiltersChange = (sectors: MECSSectorCode[]) => {
  431. this.updateParameters({ sectors })();
  432. };
  433.  
  434. renderFullMenu() {
  435. const {
  436. debugMode,
  437. parameters: {
  438. autoRefresh,
  439. fromDate,
  440. gainersLosersDir,
  441. interval,
  442. maximumMarketCap,
  443. maximumPrice,
  444. minimumMarketCap,
  445. minimumPrice,
  446. sectors,
  447. session,
  448. toDate,
  449. },
  450. } = this.props;
  451.  
  452. const fromMoment = moment.tz(fromDate, 'America/New_York');
  453. const toMoment = moment.tz(toDate, 'America/New_York');
  454.  
  455. let debugZone;
  456.  
  457. const gainersLosersFilter = (
  458. <Parameter>
  459. <ParameterLabel className="TUTORIAL_ScreenerFilters_Movers">Movers</ParameterLabel>
  460. <RadioGroup
  461. defaultValue={gainersLosersDir}
  462. onChange={this.handleGainersLosersChange}
  463. size="small"
  464. >
  465. <RadioButton value="both">Gainers &amp; Losers</RadioButton>
  466. <RadioButton value="gainers">Gainers</RadioButton>
  467. <RadioButton value="losers">Losers</RadioButton>
  468. </RadioGroup>
  469.  
  470. </Parameter>
  471. );
  472.  
  473. const sessionFilters = (
  474. <Parameter>
  475. <ParameterLabel className="TUTORIAL_ScreenerFilters_Session">Session</ParameterLabel>
  476. <RadioGroup
  477. defaultValue={session}
  478. onChange={this.handleSessionChange}
  479. size="small"
  480. >
  481. <RadioButton value="PRE_MARKET">{MarketSession.preMarket}</RadioButton>
  482. <RadioButton value="REGULAR">{MarketSession.regular}</RadioButton>
  483. <RadioButton value="AFTER_HOURS">{MarketSession.afterHours}</RadioButton>
  484. </RadioGroup>
  485.  
  486. </Parameter>
  487. );
  488.  
  489. const renderCustomDateInputs = () => {
  490. const shouldShowCustomDateInputs = interval === 'custom';
  491.  
  492. const presentDateTime = (theMoment: moment.Moment, relativeTime?: boolean) => (
  493. <span>
  494. {relativeTime ? theMoment.calendar(moment(), relativeTimeFormat) : theMoment.format('MMMM Do YYYY')}
  495. {NBSP}at{NBSP}
  496. <Emphasis>{theMoment.format('h:mma')}</Emphasis>
  497. </span>
  498. );
  499.  
  500. if (!shouldShowCustomDateInputs) {
  501. return (
  502. <DatesContainerLabel>
  503. comparing {presentDateTime(fromMoment, true)} to {presentDateTime(toMoment, true)}
  504. </DatesContainerLabel>
  505. );
  506. }
  507.  
  508. const disabledDates = current => current > moment().endOf('day');
  509.  
  510. return (
  511. <DatePickerContainer>
  512. <RangePicker
  513. disabledDate={disabledDates}
  514. format={'MM/DD/YYYY'}
  515. onChange={this.handleRangePickerChange}
  516. size="small"
  517. value={[moment(fromMoment, 'MM/DD/YYYY'), moment(toMoment, 'MM/DD/YYYY')]}
  518. />
  519.  
  520. <DatesContainerLabel>
  521. {presentDateTime(fromMoment)} to {presentDateTime(toMoment)}
  522. </DatesContainerLabel>
  523. </DatePickerContainer>
  524. );
  525. };
  526.  
  527. const period = {
  528. AFTER_HOURS: outOfSessionIntervals,
  529. PRE_MARKET: outOfSessionIntervals,
  530. REGULAR: intraSessionIntervals,
  531. }[session];
  532.  
  533. const intervalFilter = (
  534. <Parameter>
  535. <ParameterLabel className="TUTORIAL_ScreenerFilters_Period">Period</ParameterLabel>
  536. <Column>
  537. <RadioGroup
  538. onChange={this.handleIntervalChange}
  539. size="small"
  540. value={interval}
  541. >
  542. {map(
  543. ({ id, label }) => <RadioButton key={id} value={id}>{label}</RadioButton>,
  544. period,
  545. )}
  546. </RadioGroup>
  547. {fromDate && renderCustomDateInputs()}
  548. </Column>
  549. </Parameter>
  550. );
  551.  
  552. const sectorsFilter = (
  553. <Parameter>
  554. <ParameterLabel className="TUTORIAL_ScreenerFilters_Sectors">Sectors</ParameterLabel>
  555. <StyledSelect
  556. allowClear
  557. filterOption
  558. mode="multiple"
  559. notFoundContent="Sector Not Found"
  560. onChange={this.handleSectorFiltersChange}
  561. optionFilterProp="children"
  562. placeholder="All Sectors"
  563. value={sectors}
  564. >
  565. {map(
  566. ({ name, id }) => <Option key={id}>{name}</Option>,
  567. moversSectorOptions,
  568. )}
  569. </StyledSelect>
  570. </Parameter>
  571. );
  572.  
  573. const marketCapFilter = (
  574. <Parameter>
  575. <ParameterLabel className="TUTORIAL_ScreenerFilters_MarketCap">Market Cap ($)</ParameterLabel>
  576. <SliderContainer>
  577. <Slider
  578. defaultValue={[minimumMarketCap, maximumMarketCap]}
  579. dots
  580. marks={marketCapMarks}
  581. max={5}
  582. min={0}
  583. onAfterChange={this.handleRangeChange(marketCapTuple)}
  584. range
  585. step={1}
  586. tipFormatter={null}
  587. />
  588. </SliderContainer>
  589. </Parameter>
  590. );
  591.  
  592. const priceFilter = (
  593. <Parameter>
  594. <ParameterLabel className="TUTORIAL_ScreenerFilters_Price">Price ($)</ParameterLabel>
  595. <SliderContainer>
  596. <Slider
  597. defaultValue={[minimumPrice, maximumPrice]}
  598. dots
  599. marks={priceMarks}
  600. max={6}
  601. min={0}
  602. onAfterChange={this.handleRangeChange(priceTuple)}
  603. range
  604. step={1}
  605. tipFormatter={null}
  606. />
  607. </SliderContainer>
  608. </Parameter>
  609. );
  610.  
  611. const autoRefreshFilter = (
  612. <Parameter>
  613. <ParameterLabel className="TUTORIAL_ScreenerFilters_Refresh">Refresh (1 min)</ParameterLabel>
  614. <RadioGroup
  615. defaultValue={autoRefresh}
  616. onChange={this.handleAutoRefreshChange}
  617. size="small"
  618. >
  619. <RadioButton value>Auto Refresh</RadioButton>
  620. <RadioButton value={false}>Freeze</RadioButton>
  621. </RadioGroup>
  622. </Parameter>
  623. );
  624.  
  625. if (debugMode) {
  626. const url = createMoversURL(this.props.parameters);
  627. debugZone = (
  628. <Parameter>
  629. <ParameterLabel>Debug <DebugIcon /></ParameterLabel>
  630. <a href={url} target="_blank" rel="noopener noreferrer">{url}</a>
  631. </Parameter>
  632. );
  633. }
  634.  
  635. return (
  636. <Column>
  637. {gainersLosersFilter}
  638. {sessionFilters}
  639. {intervalFilter}
  640. {sectorsFilter}
  641. {marketCapFilter}
  642. {priceFilter}
  643. {autoRefreshFilter}
  644. {debugZone}
  645. </Column>
  646. );
  647. }
  648.  
  649. renderContent = () => {
  650. const {
  651. transient: {
  652. error,
  653. instruments,
  654. loading,
  655. },
  656. withQuoteSubscription,
  657. } = this.props;
  658.  
  659. if (error) {
  660. return <NoResults hideSearchTips message={error} />;
  661. }
  662.  
  663. // on first load, instruments will be null in the absence of an error.
  664. if (loading && (isEmpty(instruments) || isNil(instruments))) {
  665. return <Spinner />;
  666. }
  667.  
  668. return (
  669. <MoversGrid
  670. rowData={instruments}
  671. withQuoteSubscription={withQuoteSubscription}
  672. />
  673. );
  674. };
  675.  
  676. render() {
  677. const {
  678. parameters: {
  679. filtersExpanded,
  680. },
  681. } = this.props;
  682.  
  683. const header = (
  684. <ParametersHeader
  685. className="TUTORIAL_Screener-FilterMenu"
  686. onClick={this.updateParameters({ filtersExpanded: !filtersExpanded })}
  687. $expanded={filtersExpanded}
  688. >
  689. {filtersExpanded ? <StyledChevronDownIcon /> : <StyledChevronRightIcon />}
  690. Applied Filters{!filtersExpanded && this.renderCollapsedFilterMenu()}
  691. </ParametersHeader>
  692. );
  693.  
  694. return (
  695. <MoversContainer>
  696. <MoversParameters $expanded={filtersExpanded}>
  697. {header}
  698. {filtersExpanded && this.renderFullMenu()}
  699. </MoversParameters>
  700. <WidgetBody>
  701. {this.renderContent()}
  702. </WidgetBody>
  703. </MoversContainer>
  704. );
  705. }
  706. }
  707.  
  708. const mapStateToProps = (state: RootState, ownProps: Props): ReduxState => {
  709. const { data: { parameters, transient } } = selectWidgetById(state, ownProps);
  710.  
  711. return {
  712. debugMode: selectDebugMode(state),
  713. parameters,
  714. transient,
  715. };
  716. };
  717.  
  718. const mapDispatchToProps = {
  719. moversUpdateParameters,
  720. sagaScreenerCheckAutoRefresh,
  721. sagaScreenerRequestMovers,
  722. };
  723.  
  724. export type ExternalProps = Pick<OwnProps & ReduxState & DispatchableActions, 'widgetId' | 'withQuoteSubscription'>;
  725. const MoversConnect: ComponentClass<ExternalProps> & {
  726. WrappedComponent: ComponentClass<OwnProps & ReduxState & DispatchableActions> |
  727. FunctionComponent<OwnProps & ReduxState & DispatchableActions>;
  728. } = connect<ReduxState, DispatchableActions, OwnProps, RootState>(mapStateToProps, mapDispatchToProps)(Movers);
  729.  
  730. export default MoversConnect;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement