<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Trans, t } from '@lingui/macro';

import { findByBossId } from 'raids';
import lazyLoadComponent from 'common/lazyLoadComponent';
import retryingPromise from 'common/retryingPromise';
import makeWclUrl from 'common/makeWclUrl';
import Tooltip from 'common/Tooltip';
import getFightName from 'common/getFightName';
import REPORT_HISTORY_TYPES from 'interface/home/ReportHistory/REPORT_HISTORY_TYPES';
import { appendReportHistory } from 'interface/actions/reportHistory';
import { getResultTab } from 'interface/selectors/url/report';
import { hasPremium } from 'interface/selectors/user';
import Warning from 'interface/common/Alert/Warning';
import Ad from 'interface/common/Ad';
import ReadableList from 'interface/common/ReadableList';
import Contributor from 'interface/contributor/Button';
import WarcraftLogsIcon from 'interface/icons/WarcraftLogs';
import WipefestIcon from 'interface/icons/Wipefest';
import { i18n } from 'interface/RootLocalizationProvider';
import LoadingBar from 'interface/layout/NavigationBar/LoadingBar';
import Panel from 'interface/others/Panel';
import ChangelogTab from 'interface/others/ChangelogTab';
import ErrorBoundary from 'interface/common/ErrorBoundary';
import Checklist from 'parser/shared/modules/features/Checklist/Module';
import StatTracker from 'parser/shared/modules/StatTracker';

import './Results.scss';
import Header from './Header';
import About from './About';
import Overview from './Overview';
import Statistics from './Statistics';
import Character from './Character';
import EncounterStats from './EncounterStats';
import DegradedExperience from './DegradedExperience';
import EVENT_PARSING_STATE from '../EVENT_PARSING_STATE';
import BOSS_PHASES_STATE from '../BOSS_PHASES_STATE';
import ScrollToTop from './ScrollToTop';
import TABS from './TABS';

const TimelineTab = lazyLoadComponent(() => retryingPromise(() => import(/* webpackChunkName: 'TimelineTab' */ './Timeline/Container').then(exports => exports.default)), 0);
const EventsTab = lazyLoadComponent(() => retryingPromise(() => import(/* webpackChunkName: 'EventsTab' */ 'interface/others/EventsTab').then(exports => exports.default)));

class Results extends React.PureComponent {
  static propTypes = {
    parser: PropTypes.shape({
      boss: PropTypes.shape({
        fight: PropTypes.shape({
          resultsWarning: PropTypes.any,
        }),
      }),
      getModule: PropTypes.func.isRequired,
      selectedCombatant: PropTypes.any,
      fight: PropTypes.shape({
        boss: PropTypes.any,
        difficulty: PropTypes.any,
        start_time: PropTypes.any,
        end_time: PropTypes.any,
      }),
      generateResults: PropTypes.func.isRequired,
      disabledModules: PropTypes.any,
      playerId: PropTypes.any,
    }),
    characterProfile: PropTypes.object,
    selectedTab: PropTypes.string,
    makeBuildUrl: PropTypes.func.isRequired,
    makeTabUrl: PropTypes.func.isRequired,
    phases: PropTypes.object,
    selectedPhase: PropTypes.string.isRequired,
    selectedInstance: PropTypes.number.isRequired,
    handlePhaseSelection: PropTypes.func.isRequired,
    applyFilter: PropTypes.func.isRequired,
    timeFilter: PropTypes.object,
    build: PropTypes.string,
    report: PropTypes.shape({
      code: PropTypes.string.isRequired,
    }).isRequired,
    fight: PropTypes.shape({
      id: PropTypes.number.isRequired,
      start_time: PropTypes.number.isRequired,
      end_time: PropTypes.number.isRequired,
      offset_time: PropTypes.number.isRequired,
      boss: PropTypes.number.isRequired,
      phase: PropTypes.string,
    }).isRequired,
    player: PropTypes.shape({
      name: PropTypes.string.isRequired,
    }).isRequired,
    isLoadingParser: PropTypes.bool,
    isLoadingEvents: PropTypes.bool,
    isLoadingPhases: PropTypes.bool,
    isFilteringEvents: PropTypes.bool,
    bossPhaseEventsLoadingState: PropTypes.oneOf(Object.values(BOSS_PHASES_STATE)),
    isLoadingCharacterProfile: PropTypes.bool,
    parsingState: PropTypes.oneOf(Object.values(EVENT_PARSING_STATE)),
    progress: PropTypes.number,
    premium: PropTypes.bool,
    appendReportHistory: PropTypes.func.isRequired,
  };
  static childContextTypes = {
    updateResults: PropTypes.func.isRequired,
    parser: PropTypes.object,
  };
  getChildContext() {
    return {
      updateResults: this.forceUpdate.bind(this),
      parser: this.props.parser,
    };
  }
  static contextTypes = {
    config: PropTypes.object.isRequired,
  };

  constructor(props) {
    super(props);
    this.state = {
      adjustForDowntime: false,
    };
  }

  componentDidMount() {
    this.scrollToTop();
    this.appendHistory(this.props.report, this.props.fight, this.props.player);
  }
  componentDidUpdate(prevProps, prevState, prevContext) {
    if (this.props.selectedTab !== prevProps.selectedTab) {
      // TODO: To improve user experience we could try to avoid scrolling when the header is still within vision.
      this.scrollToTop();
    }
  }
  scrollToTop() {
    window.scrollTo(0, 0);
  }

  appendHistory(report, fight, player) {
    // TODO: Add spec and show it in the list
    this.props.appendReportHistory({
      code: report.code,
      title: report.title,
      start: Math.floor(report.start / 1000),
      end: Math.floor(report.end / 1000),
      fightId: fight.id,
      fightName: getFightName(report, fight),
      playerId: player.id,
      playerName: player.name,
      playerClass: player.type,
      type: REPORT_HISTORY_TYPES.REPORT,
    });
  }

  get warning() {
    const parser = this.props.parser;
    const boss = parser.boss;
    if (boss && boss.fight.resultsWarning) {
      return boss.fight.resultsWarning;
    }
    return null;
  }
  get isLoading() {
    return this.props.isLoadingParser
      || this.props.isLoadingEvents
      || this.props.bossPhaseEventsLoadingState === BOSS_PHASES_STATE.LOADING
      || this.props.isLoadingCharacterProfile
      || this.props.isLoadingPhases
      || this.props.isFilteringEvents
      || this.props.parsingState !== EVENT_PARSING_STATE.DONE;
  }

  renderContent(selectedTab, results) {
    const { parser, premium } = this.props;

    switch (selectedTab) {
      case TABS.OVERVIEW: {
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        const checklist = parser.getModule(Checklist, true);
        return (
          <Overview
            checklist={checklist && checklist.render()}
            issues={results.issues}
          />
        );
      }
      case TABS.STATISTICS:
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        return (
          <Statistics parser={parser}>{results.statistics}</Statistics>
        );
      case TABS.TIMELINE:
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        return (
          <TimelineTab parser={parser} />
        );
      case TABS.EVENTS:
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        return (

            <EventsTab parser={parser} />

        );
      case TABS.CHARACTER: {
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        const statTracker = parser.getModule(StatTracker);

        return (

            <Character
              statTracker={statTracker}
              combatant={parser.selectedCombatant}
            />

            {premium === false && (

                <Ad />

            )}

            <EncounterStats
              currentBoss={parser.fight.boss}
              difficulty={parser.fight.difficulty}
              spec={parser.selectedCombatant._combatantInfo.specID}
              duration={parser.fight.end_time - parser.fight.start_time}
            />

        );
      }
      case TABS.ABOUT: {
        const config = this.context.config;
        return (

            <About config={config} />

            <ChangelogTab />

        );
      }
      default: {
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }

        const tab = results.tabs.find(tab => tab.url === selectedTab);

        return (

            <ErrorBoundary>
              {tab ? tab.render() : '404 tab not found'}
            </ErrorBoundary>

        );
      }
    }
  }
  renderLoadingIndicator() {
    const { progress, isLoadingParser, isLoadingEvents, bossPhaseEventsLoadingState, isLoadingCharacterProfile, isLoadingPhases, isFilteringEvents, parsingState } = this.props;

    return (

        <Panel
          title="Loading..."
          className="loading-indicators"
        >
          <LoadingBar progress={progress} style={{ marginBottom: 30 }} />

              Spec analyzer from WoWAnalyzer

            <div className={`col-md-4 ${isLoadingParser ? 'loading' : 'ok'}`}>
              {isLoadingParser ? 'Loading...' : 'OK'}

              Player events from Warcraft Logs

            <div className={`col-md-4 ${isLoadingEvents ? 'loading' : 'ok'}`}>
              {isLoadingEvents ? 'Loading...' : 'OK'}

              Boss events from Warcraft Logs

            <div className={`col-md-4 ${bossPhaseEventsLoadingState === BOSS_PHASES_STATE.LOADING ? 'loading' : (bossPhaseEventsLoadingState === BOSS_PHASES_STATE.SKIPPED ? 'skipped' : 'ok')}`}>
              {bossPhaseEventsLoadingState === BOSS_PHASES_STATE.SKIPPED && 'Skipped'}
              {bossPhaseEventsLoadingState === BOSS_PHASES_STATE.LOADING && 'Loading...'}
              {bossPhaseEventsLoadingState === BOSS_PHASES_STATE.DONE && 'OK'}

              Character info from Blizzard

            <div className={`col-md-4 ${isLoadingCharacterProfile ? 'loading' : 'ok'}`}>
              {isLoadingCharacterProfile ? 'Loading...' : 'OK'}

              Analyzing phases

            <div className={`col-md-4 ${isLoadingPhases ? 'loading' : 'ok'}`}>
              {isLoadingPhases ? 'Loading...' : 'OK'}

              Filtering events

            <div className={`col-md-4 ${isFilteringEvents ? 'loading' : 'ok'}`}>
              {isFilteringEvents ? 'Loading...' : 'OK'}

              Analyzing events

            <div className={`col-md-4 ${parsingState === EVENT_PARSING_STATE.WAITING ? 'waiting' : (parsingState === EVENT_PARSING_STATE.PARSING ? 'loading' : 'ok')}`}>
              {parsingState === EVENT_PARSING_STATE.WAITING && 'Waiting'}
              {parsingState === EVENT_PARSING_STATE.PARSING && 'Loading...'}
              {parsingState === EVENT_PARSING_STATE.DONE && 'OK'}

        </Panel>

    );
  }
  render() {
    const { parser, report, fight, player, build, characterProfile, makeBuildUrl, makeTabUrl, selectedTab, premium, handlePhaseSelection, selectedPhase, selectedInstance, phases, applyFilter, timeFilter } = this.props;
    const config = this.context.config;

    const boss = findByBossId(fight.boss);

    const results = !this.isLoading && parser.generateResults({
      i18n, // TODO: Remove and use singleton
      adjustForDowntime: this.state.adjustForDowntime,
    });

    const contributorinfo = <ReadableList>{(config.contributors.length !== 0) ? config.contributors.map(contributor => <Contributor key={contributor.nickname} {...contributor} />) : 'CURRENTLY UNMAINTAINED'}</ReadableList>;

    return (
      <div className={`results boss-${fight.boss}`}>
        <Header
          config={config}
          name={player.name}
          characterProfile={characterProfile}
          boss={boss}
          fight={fight}
          tabs={results ? results.tabs : []}
          makeTabUrl={makeTabUrl}
          makeBuildUrl={makeBuildUrl}
          selectedTab={selectedTab}
          selectedPhase={selectedPhase}
          selectedInstance={selectedInstance}
          phases={phases}
          handlePhaseSelection={handlePhaseSelection}
          applyFilter={applyFilter}
          build={build}
          isLoading={this.isLoading}
        />
        {parser && parser.disabledModules && <DegradedExperience disabledModules={parser.disabledModules} />}
        {boss && boss.fight.resultsWarning && (

            <Warning style={{ marginBottom: 30 }}>
              {boss.fight.resultsWarning}
            </Warning>

        )}
        {timeFilter && (

            <Warning style={{ marginBottom: 30 }}>
              These results are filtered to the selected time period. Time filtered results are under development and may not be entirely accurate.
 Please report any issues you may find on our GitHub or Discord.
            </Warning>

        )}
        {build && (

            <Warning style={{ marginBottom: 30 }}>
              These results are analyzed under build different from the standard build. While this will make some modules more accurate, some may also not provide the information you expect them to.
 Please report any issues you may find on our GitHub or Discord.
            </Warning>

        )}
        {this.renderContent(selectedTab, results)}

              Provided by

                <Trans>{config.spec.specName} {config.spec.className} analysis has been provided by {contributorinfo}. They love hearing what you think, so please let them know! <Link to={makeTabUrl('about')}>More information about this spec's analyzer.</Link></Trans>

              View on

              <Tooltip content={i18n._(t`Opens in a new tab. View the original report.`)}>

                  <WarcraftLogsIcon style={{ height: '1.2em', marginTop: '-0.1em' }} /> Warcraft Logs

              </Tooltip>

              <Tooltip content={i18n._(t`Opens in a new tab. View insights and timelines for raid encounters.`)}>
                <a
                  href={`https://www.wipefest.net/report/${report.code}/fight/${fight.id}`}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="btn"
                  style={{ fontSize: 20, padding: '6px 0' }}
                >
                  <WipefestIcon style={{ height: '1.2em', marginTop: '-0.1em' }} /> Wipefest

              </Tooltip>

              <Tooltip content={<Trans>Scroll back to the top.</Trans>}>
                <ScrollToTop />
              </Tooltip>

        {premium === false && (

            <Ad />

        )}

    );
  }
}

const mapStateToProps = state => ({
  selectedTab: getResultTab(state),
  premium: hasPremium(state),
});

export default connect(
  mapStateToProps,
  {
    appendReportHistory,
  }
)(Results);
<commit_msg>more fine grained check for header warning<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Trans, t } from '@lingui/macro';

import { findByBossId } from 'raids';
import lazyLoadComponent from 'common/lazyLoadComponent';
import retryingPromise from 'common/retryingPromise';
import makeWclUrl from 'common/makeWclUrl';
import Tooltip from 'common/Tooltip';
import getFightName from 'common/getFightName';
import REPORT_HISTORY_TYPES from 'interface/home/ReportHistory/REPORT_HISTORY_TYPES';
import { appendReportHistory } from 'interface/actions/reportHistory';
import { getResultTab } from 'interface/selectors/url/report';
import { hasPremium } from 'interface/selectors/user';
import Warning from 'interface/common/Alert/Warning';
import Ad from 'interface/common/Ad';
import ReadableList from 'interface/common/ReadableList';
import Contributor from 'interface/contributor/Button';
import WarcraftLogsIcon from 'interface/icons/WarcraftLogs';
import WipefestIcon from 'interface/icons/Wipefest';
import { i18n } from 'interface/RootLocalizationProvider';
import LoadingBar from 'interface/layout/NavigationBar/LoadingBar';
import Panel from 'interface/others/Panel';
import ChangelogTab from 'interface/others/ChangelogTab';
import ErrorBoundary from 'interface/common/ErrorBoundary';
import Checklist from 'parser/shared/modules/features/Checklist/Module';
import StatTracker from 'parser/shared/modules/StatTracker';
import DEFAULT_BUILD from 'parser/DEFAULT_BUILD';

import './Results.scss';
import Header from './Header';
import About from './About';
import Overview from './Overview';
import Statistics from './Statistics';
import Character from './Character';
import EncounterStats from './EncounterStats';
import DegradedExperience from './DegradedExperience';
import EVENT_PARSING_STATE from '../EVENT_PARSING_STATE';
import BOSS_PHASES_STATE from '../BOSS_PHASES_STATE';
import ScrollToTop from './ScrollToTop';
import TABS from './TABS';

const TimelineTab = lazyLoadComponent(() => retryingPromise(() => import(/* webpackChunkName: 'TimelineTab' */ './Timeline/Container').then(exports => exports.default)), 0);
const EventsTab = lazyLoadComponent(() => retryingPromise(() => import(/* webpackChunkName: 'EventsTab' */ 'interface/others/EventsTab').then(exports => exports.default)));

class Results extends React.PureComponent {
  static propTypes = {
    parser: PropTypes.shape({
      boss: PropTypes.shape({
        fight: PropTypes.shape({
          resultsWarning: PropTypes.any,
        }),
      }),
      getModule: PropTypes.func.isRequired,
      selectedCombatant: PropTypes.any,
      fight: PropTypes.shape({
        boss: PropTypes.any,
        difficulty: PropTypes.any,
        start_time: PropTypes.any,
        end_time: PropTypes.any,
      }),
      generateResults: PropTypes.func.isRequired,
      disabledModules: PropTypes.any,
      playerId: PropTypes.any,
    }),
    characterProfile: PropTypes.object,
    selectedTab: PropTypes.string,
    makeBuildUrl: PropTypes.func.isRequired,
    makeTabUrl: PropTypes.func.isRequired,
    phases: PropTypes.object,
    selectedPhase: PropTypes.string.isRequired,
    selectedInstance: PropTypes.number.isRequired,
    handlePhaseSelection: PropTypes.func.isRequired,
    applyFilter: PropTypes.func.isRequired,
    timeFilter: PropTypes.object,
    build: PropTypes.string,
    report: PropTypes.shape({
      code: PropTypes.string.isRequired,
    }).isRequired,
    fight: PropTypes.shape({
      id: PropTypes.number.isRequired,
      start_time: PropTypes.number.isRequired,
      end_time: PropTypes.number.isRequired,
      offset_time: PropTypes.number.isRequired,
      boss: PropTypes.number.isRequired,
      phase: PropTypes.string,
    }).isRequired,
    player: PropTypes.shape({
      name: PropTypes.string.isRequired,
    }).isRequired,
    isLoadingParser: PropTypes.bool,
    isLoadingEvents: PropTypes.bool,
    isLoadingPhases: PropTypes.bool,
    isFilteringEvents: PropTypes.bool,
    bossPhaseEventsLoadingState: PropTypes.oneOf(Object.values(BOSS_PHASES_STATE)),
    isLoadingCharacterProfile: PropTypes.bool,
    parsingState: PropTypes.oneOf(Object.values(EVENT_PARSING_STATE)),
    progress: PropTypes.number,
    premium: PropTypes.bool,
    appendReportHistory: PropTypes.func.isRequired,
  };
  static childContextTypes = {
    updateResults: PropTypes.func.isRequired,
    parser: PropTypes.object,
  };
  getChildContext() {
    return {
      updateResults: this.forceUpdate.bind(this),
      parser: this.props.parser,
    };
  }
  static contextTypes = {
    config: PropTypes.object.isRequired,
  };

  constructor(props) {
    super(props);
    this.state = {
      adjustForDowntime: false,
    };
  }

  componentDidMount() {
    this.scrollToTop();
    this.appendHistory(this.props.report, this.props.fight, this.props.player);
  }
  componentDidUpdate(prevProps, prevState, prevContext) {
    if (this.props.selectedTab !== prevProps.selectedTab) {
      // TODO: To improve user experience we could try to avoid scrolling when the header is still within vision.
      this.scrollToTop();
    }
  }
  scrollToTop() {
    window.scrollTo(0, 0);
  }

  appendHistory(report, fight, player) {
    // TODO: Add spec and show it in the list
    this.props.appendReportHistory({
      code: report.code,
      title: report.title,
      start: Math.floor(report.start / 1000),
      end: Math.floor(report.end / 1000),
      fightId: fight.id,
      fightName: getFightName(report, fight),
      playerId: player.id,
      playerName: player.name,
      playerClass: player.type,
      type: REPORT_HISTORY_TYPES.REPORT,
    });
  }

  get warning() {
    const parser = this.props.parser;
    const boss = parser.boss;
    if (boss && boss.fight.resultsWarning) {
      return boss.fight.resultsWarning;
    }
    return null;
  }
  get isLoading() {
    return this.props.isLoadingParser
      || this.props.isLoadingEvents
      || this.props.bossPhaseEventsLoadingState === BOSS_PHASES_STATE.LOADING
      || this.props.isLoadingCharacterProfile
      || this.props.isLoadingPhases
      || this.props.isFilteringEvents
      || this.props.parsingState !== EVENT_PARSING_STATE.DONE;
  }

  renderContent(selectedTab, results) {
    const { parser, premium } = this.props;

    switch (selectedTab) {
      case TABS.OVERVIEW: {
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        const checklist = parser.getModule(Checklist, true);
        return (
          <Overview
            checklist={checklist && checklist.render()}
            issues={results.issues}
          />
        );
      }
      case TABS.STATISTICS:
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        return (
          <Statistics parser={parser}>{results.statistics}</Statistics>
        );
      case TABS.TIMELINE:
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        return (
          <TimelineTab parser={parser} />
        );
      case TABS.EVENTS:
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        return (

            <EventsTab parser={parser} />

        );
      case TABS.CHARACTER: {
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }
        const statTracker = parser.getModule(StatTracker);

        return (

            <Character
              statTracker={statTracker}
              combatant={parser.selectedCombatant}
            />

            {premium === false && (

                <Ad />

            )}

            <EncounterStats
              currentBoss={parser.fight.boss}
              difficulty={parser.fight.difficulty}
              spec={parser.selectedCombatant._combatantInfo.specID}
              duration={parser.fight.end_time - parser.fight.start_time}
            />

        );
      }
      case TABS.ABOUT: {
        const config = this.context.config;
        return (

            <About config={config} />

            <ChangelogTab />

        );
      }
      default: {
        if (this.isLoading) {
          return this.renderLoadingIndicator();
        }

        const tab = results.tabs.find(tab => tab.url === selectedTab);

        return (

            <ErrorBoundary>
              {tab ? tab.render() : '404 tab not found'}
            </ErrorBoundary>

        );
      }
    }
  }
  renderLoadingIndicator() {
    const { progress, isLoadingParser, isLoadingEvents, bossPhaseEventsLoadingState, isLoadingCharacterProfile, isLoadingPhases, isFilteringEvents, parsingState } = this.props;

    return (

        <Panel
          title="Loading..."
          className="loading-indicators"
        >
          <LoadingBar progress={progress} style={{ marginBottom: 30 }} />

              Spec analyzer from WoWAnalyzer

            <div className={`col-md-4 ${isLoadingParser ? 'loading' : 'ok'}`}>
              {isLoadingParser ? 'Loading...' : 'OK'}

              Player events from Warcraft Logs

            <div className={`col-md-4 ${isLoadingEvents ? 'loading' : 'ok'}`}>
              {isLoadingEvents ? 'Loading...' : 'OK'}

              Boss events from Warcraft Logs

            <div className={`col-md-4 ${bossPhaseEventsLoadingState === BOSS_PHASES_STATE.LOADING ? 'loading' : (bossPhaseEventsLoadingState === BOSS_PHASES_STATE.SKIPPED ? 'skipped' : 'ok')}`}>
              {bossPhaseEventsLoadingState === BOSS_PHASES_STATE.SKIPPED && 'Skipped'}
              {bossPhaseEventsLoadingState === BOSS_PHASES_STATE.LOADING && 'Loading...'}
              {bossPhaseEventsLoadingState === BOSS_PHASES_STATE.DONE && 'OK'}

              Character info from Blizzard

            <div className={`col-md-4 ${isLoadingCharacterProfile ? 'loading' : 'ok'}`}>
              {isLoadingCharacterProfile ? 'Loading...' : 'OK'}

              Analyzing phases

            <div className={`col-md-4 ${isLoadingPhases ? 'loading' : 'ok'}`}>
              {isLoadingPhases ? 'Loading...' : 'OK'}

              Filtering events

            <div className={`col-md-4 ${isFilteringEvents ? 'loading' : 'ok'}`}>
              {isFilteringEvents ? 'Loading...' : 'OK'}

              Analyzing events

            <div className={`col-md-4 ${parsingState === EVENT_PARSING_STATE.WAITING ? 'waiting' : (parsingState === EVENT_PARSING_STATE.PARSING ? 'loading' : 'ok')}`}>
              {parsingState === EVENT_PARSING_STATE.WAITING && 'Waiting'}
              {parsingState === EVENT_PARSING_STATE.PARSING && 'Loading...'}
              {parsingState === EVENT_PARSING_STATE.DONE && 'OK'}

        </Panel>

    );
  }
  render() {
    const { parser, report, fight, player, build, characterProfile, makeBuildUrl, makeTabUrl, selectedTab, premium, handlePhaseSelection, selectedPhase, selectedInstance, phases, applyFilter, timeFilter } = this.props;
    const config = this.context.config;

    const boss = findByBossId(fight.boss);

    const results = !this.isLoading && parser.generateResults({
      i18n, // TODO: Remove and use singleton
      adjustForDowntime: this.state.adjustForDowntime,
    });

    const contributorinfo = <ReadableList>{(config.contributors.length !== 0) ? config.contributors.map(contributor => <Contributor key={contributor.nickname} {...contributor} />) : 'CURRENTLY UNMAINTAINED'}</ReadableList>;

    return (
      <div className={`results boss-${fight.boss}`}>
        <Header
          config={config}
          name={player.name}
          characterProfile={characterProfile}
          boss={boss}
          fight={fight}
          tabs={results ? results.tabs : []}
          makeTabUrl={makeTabUrl}
          makeBuildUrl={makeBuildUrl}
          selectedTab={selectedTab}
          selectedPhase={selectedPhase}
          selectedInstance={selectedInstance}
          phases={phases}
          handlePhaseSelection={handlePhaseSelection}
          applyFilter={applyFilter}
          build={build}
          isLoading={this.isLoading}
        />
        {parser && parser.disabledModules && <DegradedExperience disabledModules={parser.disabledModules} />}
        {boss && boss.fight.resultsWarning && (

            <Warning style={{ marginBottom: 30 }}>
              {boss.fight.resultsWarning}
            </Warning>

        )}
        {timeFilter && (

            <Warning style={{ marginBottom: 30 }}>
              These results are filtered to the selected time period. Time filtered results are under development and may not be entirely accurate.
 Please report any issues you may find on our GitHub or Discord.
            </Warning>

        )}
        {build && build !== DEFAULT_BUILD.url && config.builds && Object.values(config.builds).find(b => b.url=== build) && (

            <Warning style={{ marginBottom: 30 }}>
              These results are analyzed under build different from the standard build. While this will make some modules more accurate, some may also not provide the information you expect them to.
 Please report any issues you may find on our GitHub or Discord.
            </Warning>

        )}
        {this.renderContent(selectedTab, results)}

              Provided by

                <Trans>{config.spec.specName} {config.spec.className} analysis has been provided by {contributorinfo}. They love hearing what you think, so please let them know! <Link to={makeTabUrl('about')}>More information about this spec's analyzer.</Link></Trans>

              View on

              <Tooltip content={i18n._(t`Opens in a new tab. View the original report.`)}>

                  <WarcraftLogsIcon style={{ height: '1.2em', marginTop: '-0.1em' }} /> Warcraft Logs

              </Tooltip>

              <Tooltip content={i18n._(t`Opens in a new tab. View insights and timelines for raid encounters.`)}>
                <a
                  href={`https://www.wipefest.net/report/${report.code}/fight/${fight.id}`}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="btn"
                  style={{ fontSize: 20, padding: '6px 0' }}
                >
                  <WipefestIcon style={{ height: '1.2em', marginTop: '-0.1em' }} /> Wipefest

              </Tooltip>

              <Tooltip content={<Trans>Scroll back to the top.</Trans>}>
                <ScrollToTop />
              </Tooltip>

        {premium === false && (

            <Ad />

        )}

    );
  }
}

const mapStateToProps = state => ({
  selectedTab: getResultTab(state),
  premium: hasPremium(state),
});

export default connect(
  mapStateToProps,
  {
    appendReportHistory,
  }
)(Results);
<|endoftext|>


<commit_before>
var tr = require('..').transform;

exports['Transform produces constant string'] = function (test) {
    var result = tr("adam", { self: "hi" });

    test.ok(result);
    test.strictEqual(result, "hi");
};

exports['Transform produces self string'] = function (test) {
    var result = tr("adam", { self: "{$}" });

    test.ok(result);
    test.strictEqual(result, "adam");
};

<commit_msg>Additional test, using self string in string<commit_after>
var tr = require('..').transform;

exports['Transform produces constant string'] = function (test) {
    var result = tr("adam", { self: "hi" });

    test.ok(result);
    test.strictEqual(result, "hi");
};

exports['Transform produces self string'] = function (test) {
    var result = tr("adam", { self: "{$}" });

    test.ok(result);
    test.strictEqual(result, "adam");
};

exports['Transform produces string using self string'] = function (test) {
    var result = tr("adam", { self: "hi {$}" });

    test.ok(result);
    test.strictEqual(result, "hi adam");
};

<|endoftext|>


<commit_before>'use strict';

module.exports = function (aplazame) {

  var _ = aplazame._,
      $q = require('q-promise'),
      Events = require('events-wrapper'),
      api = require('../core/api'),
      isMobile = window.matchMedia('( max-width: 767px )'),
      each = Array.prototype.forEach;

  function parsePrice (price) {
    price = price.match(/[\d,.]+/);
    price = price && price[0] || '';
    price = price.replace(/([,.])0$/, '$100');
    var priceParts = ( '' + price ).replace(/[^0-9.,]/g, '').split(/[,.]/),
        amount = Number(priceParts.shift()),
        piece = priceParts.shift(), i, n;

    if( !piece ) {
      return amount*100;
    }

    while( piece ) {
      for( i = 0, n = piece.length ; i < n ; i++ ) {
        amount*=10;
      }
      amount += Number(piece);
      piece = priceParts.shift();
    }
    return amount;
  }

  function getQty (qtySelector) {
    if( !_.isString(qtySelector) ) {
      console.warn('warning: data-qty should be an string. pe: form#article .final-price ');
      return 1;
    }
    var qtyElement;
    try {
      qtyElement = document.querySelector(qtySelector);
    } catch(err) {
      console.warn(err.message + '\ndata-qty should be an string. pe: form#article .final-price ');
      return 1;
    }

    switch( qtyElement.nodeName.toLowerCase() ) {
      case 'input':
        return Number( qtyElement.value );
      case 'select':
        return Number( qtyElement.querySelector('option[selected]').value );
      default:
        return Number( qtyElement.textContent.trim() );
    }
  }

  var cmsPriceSelector = [
    'form#product_addtocart_form .special-price .price', // magento
    'form#product_addtocart_form .regular-price .price', // magento
    '#product-info .special-price .price', // magento
    '#product-info .regular-price .price', // magento
    'form#buy_block #our_price_display', // prestashop
    '#main [itemtype="http://schema.org/Product"] [itemtype="http://schema.org/Offer"] .price .amount' // woocommerce
  ],
  cmsQtySelector = [
    'form#product_addtocart_form input[name="qty"]', // magento
    'form#buy_block input[name="qty"]', // prestashop
    'form#product-options-form button[data-id=qty]', // custom
    '#main [itemtype="http://schema.org/Product"] form.cart input[name="quantity"]' // woocommerce
  ];

  function matchSelector (selector) {
    return document.querySelector(selector);
  }

  function amountGetter (widgetElement) {
    var priceSelector = widgetElement.getAttribute('data-price'),
        qtySelector = widgetElement.getAttribute('data-qty');

    if( priceSelector ) {
      try{
        document.querySelector(priceSelector);
      } catch(err) {
        priceSelector = null;
        console.warn(err.message);
      }
      if( qtySelector ) {
        try{
          document.querySelector(qtySelector);
        } catch(err) {
          qtySelector = null;
          console.warn(err.message);
        }
      }
    } else {
      priceSelector = cmsPriceSelector.find(matchSelector);

      if( priceSelector ) {
        qtySelector = cmsQtySelector.find(matchSelector);

        _.log('automatically found price selector', priceSelector, qtySelector);
      }
    }

    var getter = priceSelector ? function () {
      var qty = qtySelector ? getQty( qtySelector ) : 1,
          priceElement = document.querySelector( priceSelector ),
          amount = priceElement.value;

      if( typeof amount === 'undefined' ) {
        // console.log('priceElement.children', priceElement.children);
        if( priceElement.children && priceElement.children.length ) {
          amount = '';
          [].forEach.call( priceElement.children, function (el) {
              if( amount.text(/[,.]/) ) {
                return;
              }
              var matched = el.textContent.match(/[\d,.]+/);

              if( matched ) {
                amount += matched[0];
              }
            });
        } else {
          amount = priceElement.textContent;
        }
      }

      return qty * parsePrice( amount );
    } : function () {
      return Number( widgetElement.getAttribute('data-amount') );
    };

    getter.priceSelector = priceSelector;
    getter.qtySelector = qtySelector;

    return getter;
  }

  function Iframe (url) {
    var el = _.getIFrame({ width: '100%', height: '40px' }),
        iframe = this;
    this.el = el;
    this.el.src = url;

    new Events (this);

    this.onload = function () {
      this.trigger('load', null, this);
    };

    _.onMessage('simulator', function (e, message) {
      if( e.source === el.contentWindow ) {
        iframe.trigger('message:' + message.event, [message], this);
      }
    });

    this.on('message:resize', function (e, message) {
      el.style.height = message.height + 'px';
    });
  }

  Iframe.prototype.message = function (eventName, data) {
    var _data = _.extend({
      aplazame: 'simulator',
      event: eventName,
      mobile: isMobile.matches
    }, data || {});
    this.el.contentWindow.postMessage(_data, '*');
  };

  function maxInstalments (prev, choice) {
    if( prev === null ) {
      return choice;
    } else {
      return choice.num_instalments > prev.num_instalments ? choice : prev;
    }
  }

  function widgetsLookup (element) {
    if( !element.querySelectorAll ) {
      return;
    }

    var simulators = element.querySelectorAll('[data-aplazame-simulator]');

    each.call(simulators, function (simulator) {

      if( simulator.$$aplazame ) {
        return;
      }

      simulator.$$aplazame = true;

      var choice, choices, options, iframe,
          getAmount = amountGetter(simulator),
          dataAmount = simulator.getAttribute('data-amount') && Number(simulator.getAttribute('data-amount')),
          currentAmount = simulator.getAttribute('data-price') ? getAmount() : ( Number( simulator.getAttribute('data-amount') ) || getAmount() );

      aplazame.simulator(currentAmount, function (_choices, _options) {

        if( _options.widget && _options.widget.disabled ) {
          return;
        }

        choices = _choices;
        options = _options;

        choice = choices.reduce(maxInstalments, null);

        var widgetOptions = options.widget, widget;

        _.clearElement(simulator);

        if( widgetOptions.type === 'button' ) {
          widget = new Iframe( api.baseUrl + 'widgets/simulator/simulator.html?' + Date.now() );

          widget.on('message:require.choices choices.update', function () {
            widget.message('choices', {
              amount: currentAmount,
              choice: choice,
              choices: choices,
              options: options
            });
          });

          widget.on('choices.updating', function (e) {
            widget.message('loading');
          });

        } else {
          _.template.put('widget-raw', require('../../.tmp/simulator/templates/widget-raw') );
          widget = { el: document.createElement('div') };
          new Events(widget);

          widget.el.innerHTML = _.template('widget-raw', {
            getAmount: _.getAmount,
            amount: currentAmount,
            choice: choice,
            choices: choices,
            options: options
          });
        }

        simulator.appendChild(widget.el);

        var previousQty = getAmount.qtySelector ? getQty(getAmount.qtySelector) : 1,
            updating = false,
            amountInterval = setInterval(function () {
              if( updating ) {
                return;
              }
              var amount = getAmount(),
                  qty = getAmount.qtySelector ? getQty(getAmount.qtySelector) : 1;

              if( amount && _.isNumber(amount) && amount !== currentAmount || qty !== previousQty ) {
                previousQty = qty;

                updating = true;
                widget.trigger('choices.updating', [amount, _choices, _options]);
                aplazame.simulator(amount, function (_choices, _options) {
                  currentAmount = amount;
                  choices = _choices;
                  options = _options;
                  choice = choices.reduce(maxInstalments, null);
                  widget.trigger('choices.update', [amount, _choices, _options]);
                  updating = false;
                }, function () {
                  updating = false;
                });
                // onPriceChange(amount, currentAmount);
              }
            }, 200);

      });

    });

  }

  _.liveDOM.subscribe(widgetsLookup);

  _.ready(function () {
    widgetsLookup(document);
  });

  // *****************************************************************************
  // *****************************************************************************
  // *****************************************************************************
  // *****************************************************************************

};
<commit_msg>little fix<commit_after>'use strict';

module.exports = function (aplazame) {

  var _ = aplazame._,
      $q = require('q-promise'),
      Events = require('events-wrapper'),
      api = require('../core/api'),
      isMobile = window.matchMedia('( max-width: 767px )'),
      each = Array.prototype.forEach;

  function parsePrice (price) {
    price = price.match(/[\d,.]+/);
    price = price && price[0] || '';
    price = price.replace(/([,.])0$/, '$100');
    var priceParts = ( '' + price ).replace(/[^0-9.,]/g, '').split(/[,.]/),
        amount = Number(priceParts.shift()),
        piece = priceParts.shift(), i, n;

    if( !piece ) {
      return amount*100;
    }

    while( piece ) {
      for( i = 0, n = piece.length ; i < n ; i++ ) {
        amount*=10;
      }
      amount += Number(piece);
      piece = priceParts.shift();
    }
    return amount;
  }

  function getQty (qtySelector) {
    if( !_.isString(qtySelector) ) {
      console.warn('warning: data-qty should be an string. pe: form#article .final-price ');
      return 1;
    }
    var qtyElement;
    try {
      qtyElement = document.querySelector(qtySelector);
    } catch(err) {
      console.warn(err.message + '\ndata-qty should be an string. pe: form#article .final-price ');
      return 1;
    }

    switch( qtyElement.nodeName.toLowerCase() ) {
      case 'input':
        return Number( qtyElement.value );
      case 'select':
        return Number( qtyElement.querySelector('option[selected]').value );
      default:
        return Number( qtyElement.textContent.trim() );
    }
  }

  var cmsPriceSelector = [
    'form#product_addtocart_form .special-price .price', // magento
    'form#product_addtocart_form .regular-price .price', // magento
    '#product-info .special-price .price', // magento
    '#product-info .regular-price .price', // magento
    'form#buy_block #our_price_display', // prestashop
    '#main [itemtype="http://schema.org/Product"] [itemtype="http://schema.org/Offer"] .price .amount' // woocommerce
  ],
  cmsQtySelector = [
    'form#product_addtocart_form input[name="qty"]', // magento
    'form#buy_block input[name="qty"]', // prestashop
    'form#product-options-form button[data-id=qty]', // custom
    '#main [itemtype="http://schema.org/Product"] form.cart input[name="quantity"]' // woocommerce
  ];

  function matchSelector (selector) {
    return document.querySelector(selector);
  }

  function amountGetter (widgetElement) {
    var priceSelector = widgetElement.getAttribute('data-price'),
        qtySelector = widgetElement.getAttribute('data-qty');

    if( priceSelector ) {
      try{
        document.querySelector(priceSelector);
      } catch(err) {
        priceSelector = null;
        console.warn(err.message);
      }
      if( qtySelector ) {
        try{
          document.querySelector(qtySelector);
        } catch(err) {
          qtySelector = null;
          console.warn(err.message);
        }
      }
    } else {
      priceSelector = cmsPriceSelector.find(matchSelector);

      if( priceSelector ) {
        qtySelector = cmsQtySelector.find(matchSelector);

        _.log('automatically found price selector', priceSelector, qtySelector);
      }
    }

    var getter = priceSelector ? function () {
      var qty = qtySelector ? getQty( qtySelector ) : 1,
          priceElement = document.querySelector( priceSelector ),
          amount = priceElement.value;

      if( typeof amount === 'undefined' ) {
        // console.log('priceElement.children', priceElement.children);
        if( priceElement.children && priceElement.children.length ) {
          amount = '';
          [].forEach.call( priceElement.children, function (el) {
              if( amount.test(/[,.]/) ) {
                return;
              }
              var matched = el.textContent.match(/[\d,.]+/);

              if( matched ) {
                amount += matched[0];
              }
            });
        } else {
          amount = priceElement.textContent;
        }
      }

      return qty * parsePrice( amount );
    } : function () {
      return Number( widgetElement.getAttribute('data-amount') );
    };

    getter.priceSelector = priceSelector;
    getter.qtySelector = qtySelector;

    return getter;
  }

  function Iframe (url) {
    var el = _.getIFrame({ width: '100%', height: '40px' }),
        iframe = this;
    this.el = el;
    this.el.src = url;

    new Events (this);

    this.onload = function () {
      this.trigger('load', null, this);
    };

    _.onMessage('simulator', function (e, message) {
      if( e.source === el.contentWindow ) {
        iframe.trigger('message:' + message.event, [message], this);
      }
    });

    this.on('message:resize', function (e, message) {
      el.style.height = message.height + 'px';
    });
  }

  Iframe.prototype.message = function (eventName, data) {
    var _data = _.extend({
      aplazame: 'simulator',
      event: eventName,
      mobile: isMobile.matches
    }, data || {});
    this.el.contentWindow.postMessage(_data, '*');
  };

  function maxInstalments (prev, choice) {
    if( prev === null ) {
      return choice;
    } else {
      return choice.num_instalments > prev.num_instalments ? choice : prev;
    }
  }

  function widgetsLookup (element) {
    if( !element.querySelectorAll ) {
      return;
    }

    var simulators = element.querySelectorAll('[data-aplazame-simulator]');

    each.call(simulators, function (simulator) {

      if( simulator.$$aplazame ) {
        return;
      }

      simulator.$$aplazame = true;

      var choice, choices, options, iframe,
          getAmount = amountGetter(simulator),
          dataAmount = simulator.getAttribute('data-amount') && Number(simulator.getAttribute('data-amount')),
          currentAmount = simulator.getAttribute('data-price') ? getAmount() : ( Number( simulator.getAttribute('data-amount') ) || getAmount() );

      aplazame.simulator(currentAmount, function (_choices, _options) {

        if( _options.widget && _options.widget.disabled ) {
          return;
        }

        choices = _choices;
        options = _options;

        choice = choices.reduce(maxInstalments, null);

        var widgetOptions = options.widget, widget;

        _.clearElement(simulator);

        if( widgetOptions.type === 'button' ) {
          widget = new Iframe( api.baseUrl + 'widgets/simulator/simulator.html?' + Date.now() );

          widget.on('message:require.choices choices.update', function () {
            widget.message('choices', {
              amount: currentAmount,
              choice: choice,
              choices: choices,
              options: options
            });
          });

          widget.on('choices.updating', function (e) {
            widget.message('loading');
          });

        } else {
          _.template.put('widget-raw', require('../../.tmp/simulator/templates/widget-raw') );
          widget = { el: document.createElement('div') };
          new Events(widget);

          widget.el.innerHTML = _.template('widget-raw', {
            getAmount: _.getAmount,
            amount: currentAmount,
            choice: choice,
            choices: choices,
            options: options
          });
        }

        simulator.appendChild(widget.el);

        var previousQty = getAmount.qtySelector ? getQty(getAmount.qtySelector) : 1,
            updating = false,
            amountInterval = setInterval(function () {
              if( updating ) {
                return;
              }
              var amount = getAmount(),
                  qty = getAmount.qtySelector ? getQty(getAmount.qtySelector) : 1;

              if( amount && _.isNumber(amount) && amount !== currentAmount || qty !== previousQty ) {
                previousQty = qty;

                updating = true;
                widget.trigger('choices.updating', [amount, _choices, _options]);
                aplazame.simulator(amount, function (_choices, _options) {
                  currentAmount = amount;
                  choices = _choices;
                  options = _options;
                  choice = choices.reduce(maxInstalments, null);
                  widget.trigger('choices.update', [amount, _choices, _options]);
                  updating = false;
                }, function () {
                  updating = false;
                });
                // onPriceChange(amount, currentAmount);
              }
            }, 200);

      });

    });

  }

  _.liveDOM.subscribe(widgetsLookup);

  _.ready(function () {
    widgetsLookup(document);
  });

  // *****************************************************************************
  // *****************************************************************************
  // *****************************************************************************
  // *****************************************************************************

};
<|endoftext|>


<commit_before><commit_msg>Adds core unit tests for `replace()`<commit_after>module('Core tests');

test('replace with loop', function() {
	expect(1);
	before_and_after_replace({loop: true});
});

test('replace without loop', function() {
	expect(1);
	before_and_after_replace({loop: false});
});

function before_and_after_replace(options) {
	var simple = $('#simple'),
		replacement = simple.html(),
		expected = null;

	simple.owlCarousel(options);

	expected = simple.html();

	simple.trigger('replace.owl.carousel', [ replacement ]);
	simple.trigger('refresh.owl.carousel');

	equal(simple.html(), expected, 'Inner HTML before and after replace equals.');
}<|endoftext|>


<commit_before>(function (Handlebars) {

    var caramelData = 'X-Caramel-Data',
        resources = {
            js: {},
            css: {},
            code: {}
        };

    /**
     * {{#itr context}}key : {{key}} value : {{value}}{{/itr}}
     */
    Handlebars.registerHelper("itr", function (obj, options) {
        var key, buffer = '';
        for (key in obj) {
            if (obj.hasOwnProperty(key)) {
                buffer += options.fn({key: key, value: obj[key]});
            }
        }
        return buffer;
    });

    /**
     * {{#func myFunction}}{{/func}}
     */
    Handlebars.registerHelper("func", function (context, block) {
        var param,
            args = [],
            params = block.hash;
        for (param in params) {
            if (params.hasOwnProperty(param)) {
                args.push(params[param]);
            }
        }
        return block(context.apply(null, args));
    });

    /**
     * Registers  'url' handler for resolving theme files.
     * {{url "js/jquery-lates.js"}}
     */
    Handlebars.registerHelper('url', function (path) {
        if (path.indexOf('http://') === 0 || path.indexOf('https://') === 0) {
            return path;
        }
        return caramel.url(path);
    });

    /**
     * Registers  't' handler for translating texts.
     * {{t "programming"}}
     */
    Handlebars.registerHelper('t', function (text) {
        return text;
    });

    /**
     * Registers  'json' handler for serializing objects.
     * {{json data}}
     */
    Handlebars.registerHelper('json', function (obj) {
        return obj ? new Handlebars.SafeString(JSON.stringify(obj)) : null;
    });

    /**
     * Registers  'cap' handler for resolving theme files.
     * {{url "js/jquery-lates.js"}}
     */
    Handlebars.registerHelper('cap', function (str) {
        return str.replace(/[^\s]+/g, function (str) {
            return str.substr(0, 1).toUpperCase() + str.substr(1).toLowerCase();
        });
    });

    /**
     * {{#slice start="1" end="10" count="2" size="2"}}{{name}}{{/slice}}
     */
    Handlebars.registerHelper('slice', function (context, block) {
        var html = "",
            length = context.length,
            start = parseInt(block.hash.start, 10) || 0,
            end = parseInt(block.hash.end, 10) || length,
            count = parseInt(block.hash.count, 10) || length,
            size = parseInt(block.hash.size, 10) || length,
            i = start,
            c = 0;
        while (i < end && c++ < count) {
            html += block(context.slice(i, (i += size)));
        }
        return html;
    });

    /**
     * {{#t "text"}}{{/t}}
     */
    Handlebars.registerHelper('t', function (text) {
        return  new Handlebars.SafeString(text);
    });

    caramel.unloaded = {};

    caramel.data = function (areas, options) {
        var err = options.error,
            success = options.success,
            headers = options.headers || (options.headers = {});
        options.dataType = 'json';
        /*        options.success = function (data, status, xhr) {
         success(null, data);
         };
         options.error = function (xhr, status, error) {
         err({
         xhr: xhr,
         status: status,
         error: error
         });
         };*/
        headers[caramelData] = JSON.stringify(areas);
        $.ajax(options);
    };

    caramel.render = function (template, context, callback) {
        var partial, fns, html,
            fn = Handlebars.partials[template],
            unloaded = caramel.unloaded;
        if (fn) {
            html = fn(context);
        } else {
            unloaded[template] = true;
        }
        fns = [];
        for (partial in unloaded) {
            if (unloaded.hasOwnProperty(partial)) {
                fns.push(function (partial) {
                    return function (callback) {
                        delete caramel.unloaded[partial];
                        caramel.get('/themes/' + caramel.themer + '/partials/' + partial + '.hbs', function (data) {
                            Handlebars.partials[partial] = Handlebars.compile(data);
                            callback(null);
                        }, 'html');
                    };
                }(partial));
            }
        }
        if (fns.length === 0) {
            callback(null, html);
            return;
        }
        async.parallel(fns, function (err, results) {
            err ? callback(err) : caramel.render(template, context, callback);
        });
    };

    caramel.css = function (el, css, id) {
        var i, length;
        if (id && resources.css[id]) {
            return;
        } else {
            resources.css[id] = true;
        }
        if (css instanceof Array) {
            length = css.length;
            for (i = 0; i < length; i++) {
                el.append('<link rel="stylesheet" type="text/css" href="' + caramel.url('/themes/' + caramel.themer + '/css/' + css[i]) + '"/>');
            }
        } else {
            el.append('<link rel="stylesheet" type="text/css" href="' + caramel.url('/themes/' + caramel.themer + '/css/' + css) + '"/>');
        }
    };

    caramel.js = function (el, js, id, callback) {
        var i, counter, length;
        if (id && resources.js[id]) {
            callback();
            return;
        } else {
            resources.js[id] = true;
        }
        if (js instanceof Array) {
            length = js.length;
            counter = length;
            for (i = 0; i < length; i++) {
                $.getScript(caramel.url('/themes/' + caramel.themer + '/js/' + js[i]), function () {
                    if (--counter > 0) {
                        return;
                    }
                    callback();
                });
            }
        } else {
            $.getScript(caramel.url('/themes/' + caramel.themer + '/js/' + js), callback);
        }
    };

    caramel.code = function (el, code, id) {
        var i, length;
        if (id && resources.code[id]) {
            return;
        } else {
            resources.code[id] = true;
        }
        if (code instanceof Array) {
            length = code.length;
            for (i = 0; i < length; i++) {
                el.append(code[i]);
            }
        } else {
            el.append(code);
        }
    };

    caramel.loaded = function(type, name) {
        resources[type][name] = true;
    };

    var invoke = Handlebars.VM.invokePartial;

    Handlebars.VM.invokePartial = function (partial, name, context, helpers, partials, data) {
        var p = Handlebars.partials[name];
        if (p) {
            return invoke.apply(Handlebars.VM, Array.prototype.slice.call(arguments));
        }
        caramel.unloaded[name] = true;
        return '';
    };

}(Handlebars));<commit_msg>adding missing new helpers<commit_after>(function (Handlebars) {

    var caramelData = 'X-Caramel-Data',
        resources = {
            js: {},
            css: {},
            code: {}
        };

    /**
     * {{#itr context}}key : {{key}} value : {{value}}{{/itr}}
     */
    Handlebars.registerHelper("itr", function (obj, options) {
        var key, buffer = '';
        for (key in obj) {
            if (obj.hasOwnProperty(key)) {
                buffer += options.fn({key: key, value: obj[key]});
            }
        }
        return buffer;
    });

    /**
     * {{#func myFunction}}{{/func}}
     */
    Handlebars.registerHelper("func", function (context, block) {
        var param,
            args = [],
            params = block.hash;
        for (param in params) {
            if (params.hasOwnProperty(param)) {
                args.push(params[param]);
            }
        }
        return block(context.apply(null, args));
    });

    /**
     * Registers  'url' handler for resolving theme files.
     * {{url "js/jquery-lates.js"}}
     */
    Handlebars.registerHelper('url', function (path) {
        if (path.indexOf('http://') === 0 || path.indexOf('https://') === 0) {
            return path;
        }
        return caramel.url(path);
    });

    /**
     * Registers  't' handler for translating texts.
     * {{t "programming"}}
     */
    Handlebars.registerHelper('t', function (text) {
        return text;
    });

    /**
     * Registers  'json' handler for serializing objects.
     * {{json data}}
     */
    Handlebars.registerHelper('json', function (obj) {
        return obj ? new Handlebars.SafeString(JSON.stringify(obj)) : null;
    });

    /**
     * Registers  'cap' handler for resolving theme files.
     * {{url "js/jquery-lates.js"}}
     */
    Handlebars.registerHelper('cap', function (str) {
        return str.replace(/[^\s]+/g, function (str) {
            return str.substr(0, 1).toUpperCase() + str.substr(1).toLowerCase();
        });
    });

    /**
     * {{#slice start="1" end="10" count="2" size="2"}}{{name}}{{/slice}}
     */
    Handlebars.registerHelper('slice', function (context, block) {
        var html = "",
            length = context.length,
            start = parseInt(block.hash.start, 10) || 0,
            end = parseInt(block.hash.end, 10) || length,
            count = parseInt(block.hash.count, 10) || length,
            size = parseInt(block.hash.size, 10) || length,
            i = start,
            c = 0;
        while (i < end && c++ < count) {
            html += block(context.slice(i, (i += size)));
        }
        return html;
    });

    /**
     * {{#t "text"}}{{/t}}
     */
    Handlebars.registerHelper('t', function (text) {
        return  new Handlebars.SafeString(text);
    });

    /*
     The snoop helper allows a query to be executed on objects stored inside
     an array.
     Usage: {{{snoop 'target(key=value).property' context}}}
     The target should be an array containing objects.
     The function works by recursively parsing a path expression
     */
    Handlebars.registerHelper('snoop', function (path, objectInstance) {

        /*
         Checks if the provided string is in the form of a query
         A query should be of the form (field=value)
         @str: The string expression that must be validated
         @return: True if the string is an expression.
         */
        function checkIfQuery(str) {
            return(((str.indexOf('(')) != -1) && (str.indexOf(')') != -1)) ? true : false;
        }

        /*
         The function travels recursively processing the object properties
         @loc: A path expression
         @object:The object in which the path occurs
         */
        function rec(loc, object) {

            //Determine if the path can be broken down
            var components = loc.split('.');

            //Stop traversal if the object is empty
            if (object == null) {
                return '';
            }
            else if (components.length == 1) {
                //Check if the current string is a key to the object
                return object[loc] || '';
            }
            else {

                /*Given a string path like: A(key=value).B.C
                 component[0]=A(key=value)
                 component[1]=B
                 component[2]=C
                 */

                //Get the starting point of A
                var currentStrIndex = loc.indexOf(components[0]);
                //Get the length of A
                var currentStrLength = components[0].length;
                //Extract just A from the string path A.B.C
                var currentStr = loc.substring(currentStrIndex, currentStrLength);

                //Remove A. so that the string is B.C
                var nextStr = loc.replace(currentStr + '.', '');

                //Get the property object[A] which should ideally return an object.
                //Note: This will evaluate to null if path is not found.e.g. The currentStr
                //is a query like A(key=value)
                var currentObj = object[currentStr];

                //Determine if the currentStr (e.g. A ) is a query
                if (checkIfQuery(components[0])) {

                    //Remove (   )
                    var indexStart = components[0].indexOf('(');
                    var indexEnd = components[0].indexOf(')');

                    //Extract the query expression (key=value)
                    var expression = components[0].substring(indexStart, indexEnd);

                    //Extract key
                    var operand = components[0].substring(0, indexStart);

                    //Get the object at property A
                    currentObj = object[operand];

                    //Get rid of the brackets in the (key=value)
                    var removed = expression.replace('(', '');
                    removed = removed.replace(')', '');

                    //Obtain the key and value pair
                    var kv = removed.split('=');

                    //If the key value pair is malformed we stop the search
                    if (kv.length == 0) {
                        return '';
                    }

                    //Obtain the value
                    var key = kv[0];
                    var value = kv[1];

                    var stop = false;

                    //Go through all items in the array(Assumption)
                    for (var index = 0; ((index < currentObj.length) && (!stop)); index++) {

                        //Check the property by the key
                        var item = currentObj[index];

                        //Compare the key to the value
                        if (item[key] == value) {

                            currentObj = item;
                            stop = true;	//Short circuit the search
                        }
                    }

                }

                return rec(nextStr, currentObj);

            }
        }

        return  rec(path, objectInstance);
    });

    /**
     *  Registers mergeContext handler which merge different contexts that needs to be passed in to a single partial.
     *
     * {{#mergeContext thisContext=this nameContext=../../name townContext=../town}}
     *      {{>child-partial}}
     * {{/mergeContext}}
     *
     * In the child-partial
     * {{nameContext.username}}
     *
     */
    Handlebars.registerHelper('mergeContext', function (options) {
        var context = {},
            mergeContext = function (obj) {
                for (var k in obj)context[k] = obj[k];
            };
        mergeContext(options.hash);
        return options.fn(context);
    });

    caramel.unloaded = {};

    caramel.data = function (areas, options) {
        var err = options.error,
            success = options.success,
            headers = options.headers || (options.headers = {});
        options.dataType = 'json';
        /*        options.success = function (data, status, xhr) {
         success(null, data);
         };
         options.error = function (xhr, status, error) {
         err({
         xhr: xhr,
         status: status,
         error: error
         });
         };*/
        headers[caramelData] = JSON.stringify(areas);
        $.ajax(options);
    };

    caramel.render = function (template, context, callback) {
        var partial, fns, html,
            fn = Handlebars.partials[template],
            unloaded = caramel.unloaded;
        if (fn) {
            html = fn(context);
        } else {
            unloaded[template] = true;
        }
        fns = [];
        for (partial in unloaded) {
            if (unloaded.hasOwnProperty(partial)) {
                fns.push(function (partial) {
                    return function (callback) {
                        delete caramel.unloaded[partial];
                        caramel.get('/themes/' + caramel.themer + '/partials/' + partial + '.hbs', function (data) {
                            Handlebars.partials[partial] = Handlebars.compile(data);
                            callback(null);
                        }, 'html');
                    };
                }(partial));
            }
        }
        if (fns.length === 0) {
            callback(null, html);
            return;
        }
        async.parallel(fns, function (err, results) {
            err ? callback(err) : caramel.render(template, context, callback);
        });
    };

    caramel.css = function (el, css, id) {
        var i, length;
        if (id && resources.css[id]) {
            return;
        } else {
            resources.css[id] = true;
        }
        if (css instanceof Array) {
            length = css.length;
            for (i = 0; i < length; i++) {
                el.append('<link rel="stylesheet" type="text/css" href="' + caramel.url('/themes/' + caramel.themer + '/css/' + css[i]) + '"/>');
            }
        } else {
            el.append('<link rel="stylesheet" type="text/css" href="' + caramel.url('/themes/' + caramel.themer + '/css/' + css) + '"/>');
        }
    };

    caramel.js = function (el, js, id, callback) {
        var i, counter, length;
        if (id && resources.js[id]) {
            callback();
            return;
        } else {
            resources.js[id] = true;
        }
        if (js instanceof Array) {
            length = js.length;
            counter = length;
            for (i = 0; i < length; i++) {
                $.getScript(caramel.url('/themes/' + caramel.themer + '/js/' + js[i]), function () {
                    if (--counter > 0) {
                        return;
                    }
                    callback();
                });
            }
        } else {
            $.getScript(caramel.url('/themes/' + caramel.themer + '/js/' + js), callback);
        }
    };

    caramel.code = function (el, code, id) {
        var i, length;
        if (id && resources.code[id]) {
            return;
        } else {
            resources.code[id] = true;
        }
        if (code instanceof Array) {
            length = code.length;
            for (i = 0; i < length; i++) {
                el.append(code[i]);
            }
        } else {
            el.append(code);
        }
    };

    caramel.loaded = function(type, name) {
        resources[type][name] = true;
    };

    var invoke = Handlebars.VM.invokePartial;

    Handlebars.VM.invokePartial = function (partial, name, context, helpers, partials, data) {
        var p = Handlebars.partials[name];
        if (p) {
            return invoke.apply(Handlebars.VM, Array.prototype.slice.call(arguments));
        }
        caramel.unloaded[name] = true;
        return '';
    };

}(Handlebars));<|endoftext|>


<commit_before>/**
 * AdSense Main setup component.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';

/**
 * WordPress dependencies
 */
import { useEffect, useState } from '@wordpress/element';
import { __, _x, sprintf } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import ProgressBar from '../../../../components/progress-bar';
import ErrorText from '../../../../components/error-text';
import { SvgIcon } from '../../../../util';
import { STORE_NAME } from '../../datastore/constants';
import { STORE_NAME as siteStoreName } from '../../../../googlesitekit/datastore/site/constants';
import {
	ACCOUNT_STATUS_NONE,
	ACCOUNT_STATUS_MULTIPLE,
	ACCOUNT_STATUS_DISAPPROVED,
	ACCOUNT_STATUS_GRAYLISTED,
	ACCOUNT_STATUS_PENDING,
	ACCOUNT_STATUS_NO_CLIENT,
	ACCOUNT_STATUS_APPROVED,
	SITE_STATUS_NONE,
	SITE_STATUS_ADDED,
	determineAccountID,
	determineClientID,
	determineAccountStatus,
	determineSiteStatus,
} from '../../util/status';
import SetupAccountCreate from './SetupAccountCreate';
import SetupAccountSelect from './SetupAccountSelect';
import SetupAccountDisapproved from './SetupAccountDisapproved';
import SetupAccountPending from './SetupAccountPending';
import SetupAccountNoClient from './SetupAccountNoClient';
import SetupAccountApproved from './SetupAccountApproved';
import SetupSiteAdd from './SetupSiteAdd';
import SetupSiteAdded from './SetupSiteAdded';
import {
	AdBlockerWarning,
	ErrorNotice,
} from '../common';
const { useSelect, useDispatch } = Data;

export default function SetupMain( { finishSetup } ) {
	// Get settings.
	const siteURL = useSelect( ( select ) => select( siteStoreName ).getReferenceSiteURL() );
	const previousAccountID = useSelect( ( select ) => select( STORE_NAME ).getAccountID() );
	const previousClientID = useSelect( ( select ) => select( STORE_NAME ).getClientID() );
	const previousAccountStatus = useSelect( ( select ) => select( STORE_NAME ).getAccountStatus() );
	const previousSiteStatus = useSelect( ( select ) => select( STORE_NAME ).getSiteStatus() );
	const accountSetupComplete = useSelect( ( select ) => select( STORE_NAME ).getAccountSetupComplete() );
	const siteSetupComplete = useSelect( ( select ) => select( STORE_NAME ).getSiteSetupComplete() );

	// Check whether a change submission is in progress.
	const isDoingSubmitChanges = useSelect( ( select ) => select( STORE_NAME ).isDoingSubmitChanges() );

	// Check whether settings differ from server and are valid.
	const canSubmitChanges = useSelect( ( select ) => select( STORE_NAME ).canSubmitChanges() );

	// Determine account.
	const accounts = useSelect( ( select ) => select( STORE_NAME ).getAccounts() );
	const accountID = determineAccountID( {
		accounts,
		previousAccountID,
	} );

	// Determine client.
	const clients = useSelect( ( select ) => select( STORE_NAME ).getClients( accountID ) );
	const clientID = determineClientID( {
		clients,
		previousClientID,
	} );

	// Get additional information to determine account and site status.
	const alerts = useSelect( ( select ) => select( STORE_NAME ).getAlerts( accountID ) );
	const urlChannels = useSelect( ( select ) => select( STORE_NAME ).getURLChannels( accountID, clientID ) );
	const error = useSelect( ( select ) => select( STORE_NAME ).getError() );

	// Determine account and site status.
	const accountStatus = determineAccountStatus( {
		accounts,
		clients,
		alerts,
		error,
		previousAccountID,
		previousClientID,
	} );
	const siteStatus = determineSiteStatus( {
		urlChannels,
		siteURL,
	} );

	const {
		setAccountID,
		setClientID,
		setAccountStatus,
		setSiteStatus,
		setAccountSetupComplete,
		setSiteSetupComplete,
		setUseSnippet,
		submitChanges,
		resetAccounts,
		resetAlerts,
		resetClients,
		resetURLChannels,
		receiveError,
	} = useDispatch( STORE_NAME );

	// Allow flagging when a background submission should happen.
	const [ isAwaitingBackgroundSubmit, setIsAwaitingBackgroundSubmit ] = useState( false );

	// Update current account ID setting on-the-fly.
	useEffect( () => {
		// Don't do anything if setting has not loaded yet, if account ID
		// cannot be determined, or if nothing has changed.
		if ( undefined === previousAccountID || undefined === accountID || previousAccountID === accountID ) {
			return;
		}
		setAccountID( accountID );
		// Set flag to await background submission.
		setIsAwaitingBackgroundSubmit( true );
	}, [ previousAccountID, accountID ] );

	// Update current client ID setting on-the-fly.
	useEffect( () => {
		// Don't do anything if setting has not loaded yet, if client ID cannot
		// be determined, or if nothing has changed.
		if ( undefined === previousClientID || undefined === clientID || previousClientID === clientID ) {
			return;
		}
		setClientID( clientID );
		// Set flag to await background submission.
		setIsAwaitingBackgroundSubmit( true );
	}, [ previousClientID, clientID ] );

	// Update account status setting on-the-fly.
	useEffect( () => {
		// Don't do anything if setting has not loaded yet, if account status
		// cannot be determined, or if nothing has changed.
		if ( undefined === previousAccountStatus || undefined === accountStatus || previousAccountStatus === accountStatus ) {
			return;
		}
		// Force setup completion flags to false in case it had been set
		// before, and enforce snippet placement until the account is
		// approved.
		if ( accountStatus !== ACCOUNT_STATUS_APPROVED ) {
			setAccountSetupComplete( false );
			setSiteSetupComplete( false );
			setUseSnippet( true );
		}
		setAccountStatus( accountStatus );
		// Set flag to await background submission.
		setIsAwaitingBackgroundSubmit( true );
	}, [ previousAccountStatus, accountStatus ] );

	// Update site status setting on-the-fly.
	useEffect( () => {
		// Don't do anything if setting has not loaded yet, if site status
		// cannot be determined, or if nothing has changed.
		if ( undefined === previousSiteStatus || undefined === siteStatus || previousSiteStatus === siteStatus ) {
			return;
		}
		// Force site setup completion flag to false in case it had been set before.
		if ( siteStatus !== SITE_STATUS_ADDED ) {
			setSiteSetupComplete( false );
		}
		setSiteStatus( siteStatus );
		// Set flag to await background submission.
		setIsAwaitingBackgroundSubmit( true );
	}, [ previousSiteStatus, siteStatus ] );

	// Submit changes for determined parameters in the background when they are valid.
	const [ isSubmittingInBackground, setIsSubmittingInBackground ] = useState( false );
	// If a background submission should happen and changes are valid to be
	// submitted, do that here. This is wrapped in a separate useEffect hook
	// and relies on isAwaitingBackgroundSubmit since the above useEffect hook
	// must not depend on canSubmitChanges, since that is also updated when
	// other than the above four settings are updated.
	useEffect( () => {
		if ( ! isAwaitingBackgroundSubmit || isSubmittingInBackground || ! canSubmitChanges ) {
			return;
		}

		// Set flag to false since we are gonna run the background submission
		// right now.
		setIsAwaitingBackgroundSubmit( false );

		// Set internal state for submitting in background to avoid sudden
		// rendering of a progress bar.
		( async () => {
			setIsSubmittingInBackground( true );
			await submitChanges();
			setIsSubmittingInBackground( false );
		} )();
	}, [ isAwaitingBackgroundSubmit, isSubmittingInBackground, canSubmitChanges ] );

	const isAdBlockerActive = useSelect( ( select ) => select( STORE_NAME ).isAdBlockerActive() );

	// Reset all fetched data when user re-focuses tab.
	useEffect( () => {
		let timeout;
		let idleSeconds = 0;
		// Count seconds once user focuses elsewhere.
		const countIdleTime = () => {
			timeout = global.setInterval( () => {
				idleSeconds++;
			}, 1000 );
		};
		// Reset when user re-focuses after 15 seconds or more.
		const reset = () => {
			global.clearTimeout( timeout );
			// Do not reset if user has been away for less than 15 seconds.
			if ( idleSeconds < 15 ) {
				idleSeconds = 0;
				return;
			}
			idleSeconds = 0;
			// Do not reset if account status has not been determined yet, or
			// if the account is approved.
			if ( undefined === accountStatus || ACCOUNT_STATUS_APPROVED === accountStatus ) {
				return;
			}

			// Unset any potential error.
			receiveError( undefined );
			// Reset all data to force re-fetch.
			resetAccounts();
			resetAlerts();
			resetClients();
			resetURLChannels();
		};
		global.addEventListener( 'focus', reset );
		global.addEventListener( 'blur', countIdleTime );
		return () => {
			global.removeEventListener( 'focus', reset );
			global.removeEventListener( 'blur', countIdleTime );
			global.clearTimeout( timeout );
		};
	}, [ accountStatus ] );

	// When `finishSetup` is called, flag that we are navigating to keep the progress bar going.
	const [ isNavigating, setIsNavigating ] = useState( false );
	const finishSetupAndNavigate = ( ...args ) => {
		finishSetup( ...args );
		setIsNavigating( true );
	};

	// Fetch existing tag right here, to ensure the progress bar is still being
	// shown while this is being loaded. It is technically used only by child
	// components.
	const existingTag = useSelect( ( select ) => select( STORE_NAME ).getExistingTag() );

	let viewComponent;
	if ( ( undefined === accountStatus && ! error ) || undefined === existingTag || ( isDoingSubmitChanges && ! isSubmittingInBackground ) || isNavigating ) {
		// Show loading indicator if account status not determined yet or if
		// a submission is in progress that is not happening in background.
		viewComponent = <ProgressBar />;
	} else if ( accountStatus !== ACCOUNT_STATUS_APPROVED || ! accountSetupComplete ) {
		// Show relevant account status component.
		switch ( accountStatus ) {
			case ACCOUNT_STATUS_NONE:
				viewComponent = <SetupAccountCreate />;
				break;
			case ACCOUNT_STATUS_MULTIPLE:
				viewComponent = <SetupAccountSelect />;
				break;
			case ACCOUNT_STATUS_DISAPPROVED:
				viewComponent = <SetupAccountDisapproved />;
				break;
			case ACCOUNT_STATUS_GRAYLISTED:
			case ACCOUNT_STATUS_PENDING:
				viewComponent = <SetupAccountPending />;
				break;
			case ACCOUNT_STATUS_NO_CLIENT:
				viewComponent = <SetupAccountNoClient />;
				break;
			case ACCOUNT_STATUS_APPROVED:
				viewComponent = <SetupAccountApproved />;
				break;
			default:
				if ( error ) {
					viewComponent = <ErrorNotice />;
				} else {
					viewComponent = <ErrorText message={ sprintf(
						/* translators: %s: invalid account status identifier */
						__( 'Invalid account status: %s', 'google-site-kit' ),
						accountStatus
					) } />;
				}
		}
	} else if ( undefined === siteStatus ) {
		// Show loading indicator if site status not determined yet.
		viewComponent = <ProgressBar />;
	} else if ( siteStatus !== SITE_STATUS_ADDED || ! siteSetupComplete ) {
		// Show relevant site status component.
		switch ( siteStatus ) {
			case SITE_STATUS_NONE:
				viewComponent = <SetupSiteAdd />;
				break;
			case SITE_STATUS_ADDED:
				viewComponent = <SetupSiteAdded finishSetup={ finishSetupAndNavigate } />;
				break;
			default:
				if ( error ) {
					viewComponent = <ErrorNotice />;
				} else {
					viewComponent = <ErrorText message={ sprintf(
						/* translators: %s: invalid site status identifier */
						__( 'Invalid site status: %s', 'google-site-kit' ),
						siteStatus
					) } />;
				}
		}
	} else {
		// This should never be reached because the setup is not accessible
		// under these circumstances due to related PHP+/JS logic. But at
		// least in theory it should show the last step, just in case.
		viewComponent = <SetupSiteAdded finishSetup={ finishSetupAndNavigate } />;
	}

	return (

					<SvgIcon id="adsense" width="33" height="33" />

					{ _x( 'AdSense', 'Service name', 'google-site-kit' ) }

				<AdBlockerWarning />

				{ ! isAdBlockerActive && (
					viewComponent
				) }

	);
}

SetupMain.propTypes = {
	finishSetup: PropTypes.func,
};

SetupMain.defaultProps = {
	finishSetup: () => {},
};
<commit_msg>Replace recieveError() action with clearError().<commit_after>/**
 * AdSense Main setup component.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';

/**
 * WordPress dependencies
 */
import { useEffect, useState } from '@wordpress/element';
import { __, _x, sprintf } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import ProgressBar from '../../../../components/progress-bar';
import ErrorText from '../../../../components/error-text';
import { SvgIcon } from '../../../../util';
import { STORE_NAME } from '../../datastore/constants';
import { STORE_NAME as siteStoreName } from '../../../../googlesitekit/datastore/site/constants';
import {
	ACCOUNT_STATUS_NONE,
	ACCOUNT_STATUS_MULTIPLE,
	ACCOUNT_STATUS_DISAPPROVED,
	ACCOUNT_STATUS_GRAYLISTED,
	ACCOUNT_STATUS_PENDING,
	ACCOUNT_STATUS_NO_CLIENT,
	ACCOUNT_STATUS_APPROVED,
	SITE_STATUS_NONE,
	SITE_STATUS_ADDED,
	determineAccountID,
	determineClientID,
	determineAccountStatus,
	determineSiteStatus,
} from '../../util/status';
import SetupAccountCreate from './SetupAccountCreate';
import SetupAccountSelect from './SetupAccountSelect';
import SetupAccountDisapproved from './SetupAccountDisapproved';
import SetupAccountPending from './SetupAccountPending';
import SetupAccountNoClient from './SetupAccountNoClient';
import SetupAccountApproved from './SetupAccountApproved';
import SetupSiteAdd from './SetupSiteAdd';
import SetupSiteAdded from './SetupSiteAdded';
import {
	AdBlockerWarning,
	ErrorNotice,
} from '../common';
const { useSelect, useDispatch } = Data;

export default function SetupMain( { finishSetup } ) {
	// Get settings.
	const siteURL = useSelect( ( select ) => select( siteStoreName ).getReferenceSiteURL() );
	const previousAccountID = useSelect( ( select ) => select( STORE_NAME ).getAccountID() );
	const previousClientID = useSelect( ( select ) => select( STORE_NAME ).getClientID() );
	const previousAccountStatus = useSelect( ( select ) => select( STORE_NAME ).getAccountStatus() );
	const previousSiteStatus = useSelect( ( select ) => select( STORE_NAME ).getSiteStatus() );
	const accountSetupComplete = useSelect( ( select ) => select( STORE_NAME ).getAccountSetupComplete() );
	const siteSetupComplete = useSelect( ( select ) => select( STORE_NAME ).getSiteSetupComplete() );

	// Check whether a change submission is in progress.
	const isDoingSubmitChanges = useSelect( ( select ) => select( STORE_NAME ).isDoingSubmitChanges() );

	// Check whether settings differ from server and are valid.
	const canSubmitChanges = useSelect( ( select ) => select( STORE_NAME ).canSubmitChanges() );

	// Determine account.
	const accounts = useSelect( ( select ) => select( STORE_NAME ).getAccounts() );
	const accountID = determineAccountID( {
		accounts,
		previousAccountID,
	} );

	// Determine client.
	const clients = useSelect( ( select ) => select( STORE_NAME ).getClients( accountID ) );
	const clientID = determineClientID( {
		clients,
		previousClientID,
	} );

	// Get additional information to determine account and site status.
	const alerts = useSelect( ( select ) => select( STORE_NAME ).getAlerts( accountID ) );
	const urlChannels = useSelect( ( select ) => select( STORE_NAME ).getURLChannels( accountID, clientID ) );
	const error = useSelect( ( select ) => select( STORE_NAME ).getError() );

	// Determine account and site status.
	const accountStatus = determineAccountStatus( {
		accounts,
		clients,
		alerts,
		error,
		previousAccountID,
		previousClientID,
	} );
	const siteStatus = determineSiteStatus( {
		urlChannels,
		siteURL,
	} );

	const {
		clearError,
		setAccountID,
		setClientID,
		setAccountStatus,
		setSiteStatus,
		setAccountSetupComplete,
		setSiteSetupComplete,
		setUseSnippet,
		submitChanges,
		resetAccounts,
		resetAlerts,
		resetClients,
		resetURLChannels,
	} = useDispatch( STORE_NAME );

	// Allow flagging when a background submission should happen.
	const [ isAwaitingBackgroundSubmit, setIsAwaitingBackgroundSubmit ] = useState( false );

	// Update current account ID setting on-the-fly.
	useEffect( () => {
		// Don't do anything if setting has not loaded yet, if account ID
		// cannot be determined, or if nothing has changed.
		if ( undefined === previousAccountID || undefined === accountID || previousAccountID === accountID ) {
			return;
		}
		setAccountID( accountID );
		// Set flag to await background submission.
		setIsAwaitingBackgroundSubmit( true );
	}, [ previousAccountID, accountID ] );

	// Update current client ID setting on-the-fly.
	useEffect( () => {
		// Don't do anything if setting has not loaded yet, if client ID cannot
		// be determined, or if nothing has changed.
		if ( undefined === previousClientID || undefined === clientID || previousClientID === clientID ) {
			return;
		}
		setClientID( clientID );
		// Set flag to await background submission.
		setIsAwaitingBackgroundSubmit( true );
	}, [ previousClientID, clientID ] );

	// Update account status setting on-the-fly.
	useEffect( () => {
		// Don't do anything if setting has not loaded yet, if account status
		// cannot be determined, or if nothing has changed.
		if ( undefined === previousAccountStatus || undefined === accountStatus || previousAccountStatus === accountStatus ) {
			return;
		}
		// Force setup completion flags to false in case it had been set
		// before, and enforce snippet placement until the account is
		// approved.
		if ( accountStatus !== ACCOUNT_STATUS_APPROVED ) {
			setAccountSetupComplete( false );
			setSiteSetupComplete( false );
			setUseSnippet( true );
		}
		setAccountStatus( accountStatus );
		// Set flag to await background submission.
		setIsAwaitingBackgroundSubmit( true );
	}, [ previousAccountStatus, accountStatus ] );

	// Update site status setting on-the-fly.
	useEffect( () => {
		// Don't do anything if setting has not loaded yet, if site status
		// cannot be determined, or if nothing has changed.
		if ( undefined === previousSiteStatus || undefined === siteStatus || previousSiteStatus === siteStatus ) {
			return;
		}
		// Force site setup completion flag to false in case it had been set before.
		if ( siteStatus !== SITE_STATUS_ADDED ) {
			setSiteSetupComplete( false );
		}
		setSiteStatus( siteStatus );
		// Set flag to await background submission.
		setIsAwaitingBackgroundSubmit( true );
	}, [ previousSiteStatus, siteStatus ] );

	// Submit changes for determined parameters in the background when they are valid.
	const [ isSubmittingInBackground, setIsSubmittingInBackground ] = useState( false );
	// If a background submission should happen and changes are valid to be
	// submitted, do that here. This is wrapped in a separate useEffect hook
	// and relies on isAwaitingBackgroundSubmit since the above useEffect hook
	// must not depend on canSubmitChanges, since that is also updated when
	// other than the above four settings are updated.
	useEffect( () => {
		if ( ! isAwaitingBackgroundSubmit || isSubmittingInBackground || ! canSubmitChanges ) {
			return;
		}

		// Set flag to false since we are gonna run the background submission
		// right now.
		setIsAwaitingBackgroundSubmit( false );

		// Set internal state for submitting in background to avoid sudden
		// rendering of a progress bar.
		( async () => {
			setIsSubmittingInBackground( true );
			await submitChanges();
			setIsSubmittingInBackground( false );
		} )();
	}, [ isAwaitingBackgroundSubmit, isSubmittingInBackground, canSubmitChanges ] );

	const isAdBlockerActive = useSelect( ( select ) => select( STORE_NAME ).isAdBlockerActive() );

	// Reset all fetched data when user re-focuses tab.
	useEffect( () => {
		let timeout;
		let idleSeconds = 0;
		// Count seconds once user focuses elsewhere.
		const countIdleTime = () => {
			timeout = global.setInterval( () => {
				idleSeconds++;
			}, 1000 );
		};
		// Reset when user re-focuses after 15 seconds or more.
		const reset = () => {
			global.clearTimeout( timeout );
			// Do not reset if user has been away for less than 15 seconds.
			if ( idleSeconds < 15 ) {
				idleSeconds = 0;
				return;
			}
			idleSeconds = 0;
			// Do not reset if account status has not been determined yet, or
			// if the account is approved.
			if ( undefined === accountStatus || ACCOUNT_STATUS_APPROVED === accountStatus ) {
				return;
			}

			// Unset any potential error.
			clearError();
			// Reset all data to force re-fetch.
			resetAccounts();
			resetAlerts();
			resetClients();
			resetURLChannels();
		};
		global.addEventListener( 'focus', reset );
		global.addEventListener( 'blur', countIdleTime );
		return () => {
			global.removeEventListener( 'focus', reset );
			global.removeEventListener( 'blur', countIdleTime );
			global.clearTimeout( timeout );
		};
	}, [ accountStatus ] );

	// When `finishSetup` is called, flag that we are navigating to keep the progress bar going.
	const [ isNavigating, setIsNavigating ] = useState( false );
	const finishSetupAndNavigate = ( ...args ) => {
		finishSetup( ...args );
		setIsNavigating( true );
	};

	// Fetch existing tag right here, to ensure the progress bar is still being
	// shown while this is being loaded. It is technically used only by child
	// components.
	const existingTag = useSelect( ( select ) => select( STORE_NAME ).getExistingTag() );

	let viewComponent;
	if ( ( undefined === accountStatus && ! error ) || undefined === existingTag || ( isDoingSubmitChanges && ! isSubmittingInBackground ) || isNavigating ) {
		// Show loading indicator if account status not determined yet or if
		// a submission is in progress that is not happening in background.
		viewComponent = <ProgressBar />;
	} else if ( accountStatus !== ACCOUNT_STATUS_APPROVED || ! accountSetupComplete ) {
		// Show relevant account status component.
		switch ( accountStatus ) {
			case ACCOUNT_STATUS_NONE:
				viewComponent = <SetupAccountCreate />;
				break;
			case ACCOUNT_STATUS_MULTIPLE:
				viewComponent = <SetupAccountSelect />;
				break;
			case ACCOUNT_STATUS_DISAPPROVED:
				viewComponent = <SetupAccountDisapproved />;
				break;
			case ACCOUNT_STATUS_GRAYLISTED:
			case ACCOUNT_STATUS_PENDING:
				viewComponent = <SetupAccountPending />;
				break;
			case ACCOUNT_STATUS_NO_CLIENT:
				viewComponent = <SetupAccountNoClient />;
				break;
			case ACCOUNT_STATUS_APPROVED:
				viewComponent = <SetupAccountApproved />;
				break;
			default:
				if ( error ) {
					viewComponent = <ErrorNotice />;
				} else {
					viewComponent = <ErrorText message={ sprintf(
						/* translators: %s: invalid account status identifier */
						__( 'Invalid account status: %s', 'google-site-kit' ),
						accountStatus
					) } />;
				}
		}
	} else if ( undefined === siteStatus ) {
		// Show loading indicator if site status not determined yet.
		viewComponent = <ProgressBar />;
	} else if ( siteStatus !== SITE_STATUS_ADDED || ! siteSetupComplete ) {
		// Show relevant site status component.
		switch ( siteStatus ) {
			case SITE_STATUS_NONE:
				viewComponent = <SetupSiteAdd />;
				break;
			case SITE_STATUS_ADDED:
				viewComponent = <SetupSiteAdded finishSetup={ finishSetupAndNavigate } />;
				break;
			default:
				if ( error ) {
					viewComponent = <ErrorNotice />;
				} else {
					viewComponent = <ErrorText message={ sprintf(
						/* translators: %s: invalid site status identifier */
						__( 'Invalid site status: %s', 'google-site-kit' ),
						siteStatus
					) } />;
				}
		}
	} else {
		// This should never be reached because the setup is not accessible
		// under these circumstances due to related PHP+/JS logic. But at
		// least in theory it should show the last step, just in case.
		viewComponent = <SetupSiteAdded finishSetup={ finishSetupAndNavigate } />;
	}

	return (

					<SvgIcon id="adsense" width="33" height="33" />

					{ _x( 'AdSense', 'Service name', 'google-site-kit' ) }

				<AdBlockerWarning />

				{ ! isAdBlockerActive && (
					viewComponent
				) }

	);
}

SetupMain.propTypes = {
	finishSetup: PropTypes.func,
};

SetupMain.defaultProps = {
	finishSetup: () => {},
};
<|endoftext|>