<commit_before>import React from "react"
import { renderToString } from "react-dom/server"
import { StaticRouter } from "react-router"
import Helmet from "react-helmet"
import { ApolloProvider, getDataFromTree } from "react-apollo"
import { IntlProvider } from "react-intl"

import Measure from "./Measure"
import renderPage from "./renderPage"
import { createReduxStore } from "../common/State"
import { createApolloClient } from "../common/Apollo"

/**
 * Using Apollo logic to recursively resolve all queries needed for
 * initial rendering. The convention is to use the full JSX tree,
 * traverse it and look for all occurrences of a static `fetchData() => Promise`
 * method which is being executed and waited for.
 *
 * See also:
 * https://www.npmjs.com/package/react-redux-universal-hot-example#server-side-data-fetching
 */
function renderToStringWithData(component, measure) {
  measure.start("loading-data")
  return getDataFromTree(component).then(() => {
    measure.stop("loading-data")

    measure.start("render-react")
    var result = renderToString(component)
    measure.stop("render-react")

    return result
  })
}

// SSR is disabled so we will just return an empty html page and will
// rely on the client to populate the initial react application state.
function renderLight({ request, response, nonce, initialState, language, region, measure }) {
  /* eslint-disable no-magic-numbers */
  try {
    measure.start("render-page")
    const html = renderPage({
      // Provide the redux store state, this will be bound to the window.APP_STATE
      // so that we can rehydrate the state on the client.
      initialState,

      // Nonce which allows us to safely declare inline scripts.
      nonce,

      // Send detected language and region for HTML tag info
      language,
      region
    })
    measure.stop("render-page")

    response.status(200).send(html)

    // Print measure results
    measure.print()
  } catch (error) {
    response.status(500).send(`Error during rendering: ${error}!: ${error.stack}`)
  }
}

function renderFull({ request, response, nonce, Root, apolloClient, reduxStore, language, region, measure }) {
  const routingContext = {}

  console.log("Server: Rendering app with data...")

  var currentLocale = `${language}-${region}`
  var messages = {}

  var WrappedRoot = (
    <StaticRouter location={request.url} context={routingContext}>
      <ApolloProvider client={apolloClient} store={reduxStore}>
        <IntlProvider locale={currentLocale} messages={messages}>
          <Root/>
        </IntlProvider>
      </ApolloProvider>
    </StaticRouter>
  )

  // Create the application react element.
  renderToStringWithData(
    WrappedRoot,
    measure
  ).then((renderedApp) => {
    const reduxState = reduxStore.getState()

    // Render the app to a string.
    measure.start("render-page")
    const html = renderPage({
      // Provide the full rendered App react element.
      renderedApp,

      // Provide the redux store state, this will be bound to the window.APP_STATE
      // so that we can rehydrate the state on the client.
      initialState: reduxState,

      // Nonce which allows us to safely declare inline scripts.
      nonce,

      // Running this gets all the helmet properties (e.g. headers/scripts/title etc)
      // that need to be included within our html. It's based on the rendered app.
      // @see https://github.com/nfl/react-helmet
      helmet: Helmet.rewind(),

      // Send detected language and region for HTML tag info
      language,
      region
    })
    measure.stop("render-page")

    console.log("Server: Routing Context:", routingContext)
    console.log("Server: Sending Page...")

    /* eslint-disable no-magic-numbers */

    // Check if the render result contains a redirect, if so we need to set
    // the specific status and redirect header and end the response.
    if (routingContext.url) {
      response.status(302).setHeader("Location", routingContext.url)
      response.end()
      return
    }

    // If the renderedResult contains a "missed" match then we set a 404 code.
    // Our App component will handle the rendering of an Error404 view.
    // Otherwise everything is all good and we send a 200 OK status.
    response.status(routingContext.missed ? 404 : 200).send(html)

    // Print measure results
    // measure.print()
  }).catch((error) => {
    console.error("Server: Error during producing response:", error)
  })
}

/**
 * An express middleware that is capable of doing React server side rendering.
 */
export default function createUniversalMiddleware({ Root, State, ssrData, batchRequests = false, trustNetwork = true })
{
  if (Root == null) {
    throw new Error("Server: Universal Middleware: Missing Root!")
  }

  if (State == null) {
    throw new Error("Server: Universal Middleware: Missing State!")
  }

  return function middleware(request, response)
  {
    if (typeof response.locals.nonce !== "string") {
      throw new TypeError(`Server: A "nonce" value has not been attached to the response`)
    }
    const nonce = response.locals.nonce

    if (!request.locale) {
      throw new Error("Cannot correctly deal with locale configuration!")
    }

    const { language, region } = request.locale

    console.log(
      "Incoming URL:",
      request.originalUrl,
      process.env.DISABLE_SSR ? "[SSR: disabled]" : "[SSR: enabled]",
      `[Locale: ${language}-${region}]`
    )
    let measure = new Measure()

    // After matching locales with configuration we send the accepted locale
    // back to the client using a simple session cookie
    response.cookie("locale", `${language}-${region}`)

    // Pass object with all Server Side Rendering (SSR) related data to the client
    const initialState = {
      ssr: {
        ...ssrData,
        locale: `${language}-${region}`,
        language,
        region
      }
    }

    if (process.env.DISABLE_SSR)
    {
      renderLight({
        request,
        response,
        nonce,
        initialState,
        language,
        region,
        measure
      })
    }
    else
    {
      measure.start("create-apollo")
      const apolloClient = createApolloClient({
        headers: request.headers,
        initialState,
        batchRequests,
        trustNetwork
      })
      measure.stop("create-apollo")

      measure.start("create-redux")
      const reduxStore = createReduxStore({
        reducers: State.getReducers(),
        enhancers: State.getEnhancers(),
        middlewares: State.getMiddlewares(),
        apolloClient,
        initialState
      })
      measure.stop("create-redux")

      renderFull({
        request,
        response,
        nonce,
        Root,
        apolloClient,
        reduxStore,
        language,
        region,
        measure
      })
    }
  }
}
<commit_msg>Worked on i18n support<commit_after>import React from "react"
import { renderToString } from "react-dom/server"
import { StaticRouter } from "react-router"
import Helmet from "react-helmet"
import { ApolloProvider, getDataFromTree } from "react-apollo"
import { IntlProvider } from "react-intl"

import Measure from "./Measure"
import renderPage from "./renderPage"
import { createReduxStore } from "../common/State"
import { createApolloClient } from "../common/Apollo"

/**
 * Using Apollo logic to recursively resolve all queries needed for
 * initial rendering. The convention is to use the full JSX tree,
 * traverse it and look for all occurrences of a static `fetchData() => Promise`
 * method which is being executed and waited for.
 *
 * See also:
 * https://www.npmjs.com/package/react-redux-universal-hot-example#server-side-data-fetching
 */
function renderToStringWithData(component, measure) {
  measure.start("loading-data")
  return getDataFromTree(component).then(() => {
    measure.stop("loading-data")

    measure.start("render-react")
    var result = renderToString(component)
    measure.stop("render-react")

    return result
  })
}

// SSR is disabled so we will just return an empty html page and will
// rely on the client to populate the initial react application state.
function renderLight({ request, response, nonce, initialState, language, region, measure }) {
  /* eslint-disable no-magic-numbers */
  try {
    measure.start("render-page")
    const html = renderPage({
      // Provide the redux store state, this will be bound to the window.APP_STATE
      // so that we can rehydrate the state on the client.
      initialState,

      // Nonce which allows us to safely declare inline scripts.
      nonce,

      // Send detected language and region for HTML tag info
      language,
      region
    })
    measure.stop("render-page")

    response.status(200).send(html)

    // Print measure results
    measure.print()
  } catch (error) {
    response.status(500).send(`Error during rendering: ${error}!: ${error.stack}`)
  }
}

function renderFull({ request, response, nonce, Root, apolloClient, reduxStore, language, region, measure }) {
  const routingContext = {}

  console.log("Server: Rendering app with data...")

  var currentLocale = `${language}-${region}`

  // FIXME: so kann das ja njicht gehen... hier braucht man das echte Objekt.
  var messages = import("../app/messages/" + currentLocale + ".json")

  var WrappedRoot = (
    <StaticRouter location={request.url} context={routingContext}>
      <ApolloProvider client={apolloClient} store={reduxStore}>
        <IntlProvider locale={currentLocale} messages={messages}>
          <Root/>
        </IntlProvider>
      </ApolloProvider>
    </StaticRouter>
  )

  // Create the application react element.
  renderToStringWithData(
    WrappedRoot,
    measure
  ).then((renderedApp) => {
    const reduxState = reduxStore.getState()

    // Render the app to a string.
    measure.start("render-page")
    const html = renderPage({
      // Provide the full rendered App react element.
      renderedApp,

      // Provide the redux store state, this will be bound to the window.APP_STATE
      // so that we can rehydrate the state on the client.
      initialState: reduxState,

      // Nonce which allows us to safely declare inline scripts.
      nonce,

      // Running this gets all the helmet properties (e.g. headers/scripts/title etc)
      // that need to be included within our html. It's based on the rendered app.
      // @see https://github.com/nfl/react-helmet
      helmet: Helmet.rewind(),

      // Send detected language and region for HTML tag info
      language,
      region
    })
    measure.stop("render-page")

    console.log("Server: Routing Context:", routingContext)
    console.log("Server: Sending Page...")

    /* eslint-disable no-magic-numbers */

    // Check if the render result contains a redirect, if so we need to set
    // the specific status and redirect header and end the response.
    if (routingContext.url) {
      response.status(302).setHeader("Location", routingContext.url)
      response.end()
      return
    }

    // If the renderedResult contains a "missed" match then we set a 404 code.
    // Our App component will handle the rendering of an Error404 view.
    // Otherwise everything is all good and we send a 200 OK status.
    response.status(routingContext.missed ? 404 : 200).send(html)

    // Print measure results
    // measure.print()
  }).catch((error) => {
    console.error("Server: Error during producing response:", error)
  })
}

/**
 * An express middleware that is capable of doing React server side rendering.
 */
export default function createUniversalMiddleware({ Root, State, ssrData, batchRequests = false, trustNetwork = true })
{
  if (Root == null) {
    throw new Error("Server: Universal Middleware: Missing Root!")
  }

  if (State == null) {
    throw new Error("Server: Universal Middleware: Missing State!")
  }

  return function middleware(request, response)
  {
    if (typeof response.locals.nonce !== "string") {
      throw new TypeError(`Server: A "nonce" value has not been attached to the response`)
    }
    const nonce = response.locals.nonce

    if (!request.locale) {
      throw new Error("Cannot correctly deal with locale configuration!")
    }

    const { language, region } = request.locale

    console.log(
      "Incoming URL:",
      request.originalUrl,
      process.env.DISABLE_SSR ? "[SSR: disabled]" : "[SSR: enabled]",
      `[Locale: ${language}-${region}]`
    )
    let measure = new Measure()

    // After matching locales with configuration we send the accepted locale
    // back to the client using a simple session cookie
    response.cookie("locale", `${language}-${region}`)

    // Pass object with all Server Side Rendering (SSR) related data to the client
    const initialState = {
      ssr: {
        ...ssrData,
        locale: `${language}-${region}`,
        language,
        region
      }
    }

    if (process.env.DISABLE_SSR)
    {
      renderLight({
        request,
        response,
        nonce,
        initialState,
        language,
        region,
        measure
      })
    }
    else
    {
      measure.start("create-apollo")
      const apolloClient = createApolloClient({
        headers: request.headers,
        initialState,
        batchRequests,
        trustNetwork
      })
      measure.stop("create-apollo")

      measure.start("create-redux")
      const reduxStore = createReduxStore({
        reducers: State.getReducers(),
        enhancers: State.getEnhancers(),
        middlewares: State.getMiddlewares(),
        apolloClient,
        initialState
      })
      measure.stop("create-redux")

      renderFull({
        request,
        response,
        nonce,
        Root,
        apolloClient,
        reduxStore,
        language,
        region,
        measure
      })
    }
  }
}
<|endoftext|>


<commit_before>import React, { PureComponent } from 'react';
import { isMobile, getLast } from 'binary-utils';
import { BinaryChart } from 'binary-charts';
import {
    internalTradeModelToChartTradeModel,
    serverContractModelToChartContractModel,
} from '../adapters/TradeObjectAdapter';
import { chartApi } from '../../_data/LiveData';
import { mergeTicks } from '../../_reducers/TickReducer';
import { mergeCandles } from '../../_reducers/OHLCReducer';
import { chartToDataType, getDataWithErrorHandling } from '../../_chart-utils/Utils';

const zoomInTo = (ratio) => (ev, chart) => {
    const { dataMax, dataMin } = chart.xAxis[0].getExtremes();
    const lastData = getLast(chart.series[0].options.data);
    if (!lastData) return;
    const seriesMax = lastData[0];
    if (!seriesMax) return;
    const range = (seriesMax - dataMin) * ratio;
    chart.xAxis[0].setExtremes(seriesMax - range, dataMax);
};

const zoomInMax = (ev, chart) => {
    const { dataMax } = chart.xAxis[0].getExtremes();
    const { minRange } = chart.xAxis[0].options;
    chart.xAxis[0].setExtremes(dataMax - minRange, dataMax);
};

// ad-hoc way to stop chart from keep shifting
const move2StepsBack = (ev, chart) => {
    const data = chart.series[0].options.data;
    const lastTwoData = data[data.length - 3];

    const { min } = chart.xAxis[0].getExtremes();
    chart.xAxis[0].setExtremes(min, lastTwoData[0]);
};

const defaultState = {
    dataType: 'ticks',
    chartType: 'area',
    ticks: [],
    candles: [],
};

export default class TradeViewChart extends PureComponent {

    static contextTypes = {
        theme: () => undefined,
    };

    props: {
        contractForChart: object,
        index: number,
        events: any[],
        feedLicense: string,
        pipSize: number,
        tradeForChart: object,
        tradingTime: object,
    };

    static defaultProps = {
        type: 'full',
        feedLicense: '',
        events: [
            {
                type: 'zoom-in-to',
                handler: zoomInTo(1 / 16),
            },
            {
                type: 'zoom-in-max',
                handler: zoomInMax,
            },
            {
                type: 'move-back',
                handler: move2StepsBack,
            },
        ],
        tradingTime: {},
    };

    constructor(props) {
        super(props);
        this.state = defaultState;

        this.api = chartApi(props.index + 1);
    }

    componentWillMount() {
        this.api.events.on('tick', data => {
            const { index, tradeForChart, contractForChart } = this.props;

            // ignored delayed tick from previous subscription
            if (tradeForChart && data.tick.symbol !== tradeForChart.get('symbol')) return;

            const old = this.state.ticks;
            if (old.length === 0) {
                return;
            }

            const newTick = {
                epoch: +data.tick.epoch,
                quote: +data.tick.quote,
            };
            this.setState({ ticks: old.concat([newTick]) });
            this.ticksId = data.tick.id;

            // stop the chart from moving 5 ticks after contract ends
            const endTime = contractForChart && (contractForChart.get('date_expiry') || contractForChart.get('sell_spot_time'));
            if (endTime) {
                const last6Data = old[old.length - 7];
                const last5Data = old[old.length - 6];
                if (last5Data.epoch > +endTime && last6Data.epoch <= +endTime) {
                    const chartDiv = document.getElementById(`trade-chart${index}`);
                    if (chartDiv) {
                        chartDiv.dispatchEvent(new Event('move-back'));
                    }
                }
            }
        });

        this.api.events.on('ohlc', data => {
            const { tradeForChart } = this.props;

            // ignore delayed tick from previous subscription
            if (tradeForChart && data.ohlc.symbol !== tradeForChart.get('symbol')) return;

            const old = this.state.candles;

            // list of candles might be received later than candles stream due to size
            // do not process single candle that arrived before list of candles
            if (old.length < 3) {
                return;
            }

            const ohlc = data.ohlc;
            const newOHLC = {
                epoch: +(ohlc.open_time || ohlc.epoch),
                open: +ohlc.open,
                high: +ohlc.high,
                low: +ohlc.low,
                close: +ohlc.close,
            };
            const last1 = old[old.length - 1];
            const last2 = old[old.length - 2];
            const last3 = old[old.length - 3];
            const interval = last2.epoch - last3.epoch;
            const diff = newOHLC.epoch - last1.epoch;

            if (diff < interval) {
                const newOHLCArr = old.slice(0, -1);
                newOHLCArr.push(newOHLC);
                this.setState({ candles: newOHLCArr });
            } else {
                this.setState({ candles: old.concat([newOHLC]) });
            }
            this.ohlcId = data.ohlc.id;
        });
    }

    componentDidMount() {
        const { tradeForChart, index, feedLicense } = this.props;

        if (!tradeForChart) throw new Error(`Trade no ${index} does not have trade param`);

        const symbol = tradeForChart.get('symbol');

        if (feedLicense === 'chartonly') {
            return;
        }

        this.subscribeToTicks(symbol).then(() => {
            const chartDiv = document.getElementById(`trade-chart${index}`);
            if (chartDiv) {
                chartDiv.dispatchEvent(new Event('zoom-in-to'));
            }
        });
        this.subscribeToOHLC(symbol);
    }

    componentWillReceiveProps(nextProps) {
        const thisSymbol = this.props.tradeForChart.get('symbol');
        const nextSymbol = nextProps.tradeForChart.get('symbol');
        if (
            (this.props.tradeForChart && nextProps.tradeForChart) &&
            (thisSymbol !== nextSymbol)
        ) {
            this.setState(defaultState);
            this.unsubscribe();

            const { feedLicense } = nextProps;
            if (feedLicense === 'chartonly') {
                return;
            }

            this.subscribeToTicks(nextSymbol).then(() => {
                const chartDiv = document.getElementById(`trade-chart${nextProps.index}`);
                if (chartDiv) {
                    chartDiv.dispatchEvent(new Event('zoom-in-to'));
                }
            });
            this.subscribeToOHLC(nextSymbol);
        }
    }

    componentDidUpdate(prevProps) {
        if (!prevProps.contractForChart && this.props.contractForChart) {
            const index = this.props.index;
            const chartDiv = document.getElementById(`trade-chart${index}`);
            if (chartDiv) {
                chartDiv.dispatchEvent(new Event('zoom-in-max'));
            }
        }
    }

    componentWillUnmount() {
        this.unsubscribe();
        this.api.events.ignoreAll('tick');
        this.api.events.ignoreAll('ohlc');
    }

    unsubscribe = () => {
        if (this.ticksId) this.api.unsubscribeByID(this.ticksId);
        if (this.ohlcId) this.api.unsubscribeByID(this.ohlcId);
    }

    updateData = (data, type) => {
        if (type === 'ticks') {
            const { times, prices } = data.history;
            const newTicks = times.map((t, idx) => {
                const quote = prices[idx];
                return { epoch: +t, quote: +quote };
            });

            const ticks = mergeTicks(this.state.ticks, newTicks);
            this.setState({ ticks });
            return ticks;
        }

        const candles = mergeCandles(this.state.candles, data.candles);
        this.setState({ candles });
        return candles;
    }

    fetchData = (start: number, end: number, type: ChartType, interval: number): Promise => {
        const { tradeForChart, feedLicense } = this.props;

        if (feedLicense === 'chartonly') return Promise.resolve();

        const symbol = tradeForChart.get('symbol');
        const count = type === 'ticks' ? 1000 : 500;

        // no need to handle error as not calling with subscribe
        return this.api
            .getTickHistory(symbol, { count, end, style: type, granularity: interval, adjust_start_time: 1 })
            .then(r => this.updateData(r, type));
    }

    subscribeToTicks = (symbol, count = 2000): Promise => {
        const callDependOnErr = (err) =>
            this.api.getTickHistory(symbol, {
                count: isMobile() ? 500 : count,
                end: 'latest',
                adjust_start_time: 1,
                subscribe: err ? undefined : 1,
            });

        return getDataWithErrorHandling(callDependOnErr).then(r => this.updateData(r, 'ticks'));
    }

    subscribeToOHLC = (symbol, count = 500, interval = 60): Promise => {
        const callDependOnErr = (err) =>
            this.api.getTickHistory(symbol, {
                    count: isMobile() ? 200 : count,
                    end: 'latest',
                    subscribe: err ? undefined : 1,
                    style: 'candles',
                    adjust_start_time: 1,
                    granularity: interval,
                });

        return getDataWithErrorHandling(callDependOnErr).then(r => this.updateData(r, 'candles'));
    }

    changeChartType = (type: ChartType) => {
        const { feedLicense } = this.props;
        const { chartType } = this.state;

        if (feedLicense === 'chartonly' || chartType === type) {
            return;
        }

        const newDataType = chartToDataType[type];
        if (newDataType === this.state.dataType) {
            this.setState({ chartType: type });
            return;
        }

        this.setState({ chartType: type, dataType: newDataType });
    }

    render() {
        const { contractForChart, index, events,
            feedLicense, pipSize, tradeForChart } = this.props;
        const { theme } = this.context;
        const { chartType, dataType } = this.state;

        const noData = feedLicense === 'chartonly';

        return (
            <BinaryChart
                id={`trade-chart${index}`}
                className="trade-chart"
                contract={contractForChart && serverContractModelToChartContractModel(contractForChart.toJS())}
                events={events}
                noData={noData}
                pipSize={pipSize}
                shiftMode={contractForChart ? 'dynamic' : 'fixed'}
                symbol={tradeForChart && tradeForChart.get('symbol')}
                assetName={tradeForChart && tradeForChart.get('symbolName')}
                ticks={this.state[dataType]}
                theme={theme}
                showTooltips={!isMobile()}
                hideZoomControls={isMobile()}
                type={chartType}
                trade={tradeForChart && internalTradeModelToChartTradeModel(tradeForChart.toJS())}
                // tradingTimes={tradingTime.times}
                getData={noData ? undefined : this.fetchData}
                onTypeChange={noData ? undefined : this.changeChartType}   // do not allow change type when there's contract
            />
        );
    }
}
<commit_msg>Add data cap for tradeviewchart<commit_after>import React, { PureComponent } from 'react';
import { isMobile, getLast } from 'binary-utils';
import { BinaryChart } from 'binary-charts';
import {
    internalTradeModelToChartTradeModel,
    serverContractModelToChartContractModel,
} from '../adapters/TradeObjectAdapter';
import { chartApi } from '../../_data/LiveData';
import { mergeTicks } from '../../_reducers/TickReducer';
import { mergeCandles } from '../../_reducers/OHLCReducer';
import { chartToDataType, getDataWithErrorHandling } from '../../_chart-utils/Utils';

const zoomInTo = (ratio) => (ev, chart) => {
    const { dataMax, dataMin } = chart.xAxis[0].getExtremes();
    const lastData = getLast(chart.series[0].options.data);
    if (!lastData) return;
    const seriesMax = lastData[0];
    if (!seriesMax) return;
    const range = (seriesMax - dataMin) * ratio;
    chart.xAxis[0].setExtremes(seriesMax - range, dataMax);
};

const zoomInMax = (ev, chart) => {
    const { dataMax } = chart.xAxis[0].getExtremes();
    const { minRange } = chart.xAxis[0].options;
    chart.xAxis[0].setExtremes(dataMax - minRange, dataMax);
};

// ad-hoc way to stop chart from keep shifting
const move2StepsBack = (ev, chart) => {
    const data = chart.series[0].options.data;
    const lastTwoData = data[data.length - 3];

    const { min } = chart.xAxis[0].getExtremes();
    chart.xAxis[0].setExtremes(min, lastTwoData[0]);
};

const defaultState = {
    dataType: 'ticks',
    chartType: 'area',
    ticks: [],
    candles: [],
};

export default class TradeViewChart extends PureComponent {

    static contextTypes = {
        theme: () => undefined,
    };

    props: {
        contractForChart: object,
        index: number,
        events: any[],
        feedLicense: string,
        pipSize: number,
        tradeForChart: object,
        tradingTime: object,
    };

    static defaultProps = {
        type: 'full',
        feedLicense: '',
        events: [
            {
                type: 'zoom-in-to',
                handler: zoomInTo(1 / 16),
            },
            {
                type: 'zoom-in-max',
                handler: zoomInMax,
            },
            {
                type: 'move-back',
                handler: move2StepsBack,
            },
        ],
        tradingTime: {},
    };

    constructor(props) {
        super(props);
        this.state = defaultState;

        this.api = chartApi(props.index + 1);
    }

    componentWillMount() {
        this.api.events.on('tick', data => {
            const { index, tradeForChart, contractForChart } = this.props;

            // ignored delayed tick from previous subscription
            if (tradeForChart && data.tick.symbol !== tradeForChart.get('symbol')) return;

            const old = this.state.ticks;
            if (old.length === 0) {
                return;
            }

            const newTick = {
                epoch: +data.tick.epoch,
                quote: +data.tick.quote,
            };
            const dataCap = isMobile() ? 700 : 2500;

            this.setState({ ticks: old.concat([newTick]).slice(-dataCap) });
            this.ticksId = data.tick.id;

            // stop the chart from moving 5 ticks after contract ends
            const endTime = contractForChart && (contractForChart.get('date_expiry') || contractForChart.get('sell_spot_time'));
            if (endTime) {
                const last6Data = old[old.length - 7];
                const last5Data = old[old.length - 6];
                if (last5Data.epoch > +endTime && last6Data.epoch <= +endTime) {
                    const chartDiv = document.getElementById(`trade-chart${index}`);
                    if (chartDiv) {
                        chartDiv.dispatchEvent(new Event('move-back'));
                    }
                }
            }
        });

        this.api.events.on('ohlc', data => {
            const { tradeForChart } = this.props;

            // ignore delayed tick from previous subscription
            if (tradeForChart && data.ohlc.symbol !== tradeForChart.get('symbol')) return;

            const old = this.state.candles;

            // list of candles might be received later than candles stream due to size
            // do not process single candle that arrived before list of candles
            if (old.length < 3) {
                return;
            }

            const ohlc = data.ohlc;
            const newOHLC = {
                epoch: +(ohlc.open_time || ohlc.epoch),
                open: +ohlc.open,
                high: +ohlc.high,
                low: +ohlc.low,
                close: +ohlc.close,
            };
            const last1 = old[old.length - 1];
            const last2 = old[old.length - 2];
            const last3 = old[old.length - 3];
            const interval = last2.epoch - last3.epoch;
            const diff = newOHLC.epoch - last1.epoch;

            if (diff < interval) {
                const newOHLCArr = old.slice(0, -1);
                newOHLCArr.push(newOHLC);
                this.setState({ candles: newOHLCArr });
            } else {
                const dataCap = isMobile() ? 500 : 1500;
                this.setState({ candles: old.concat([newOHLC]).slice(-dataCap) });
            }
            this.ohlcId = data.ohlc.id;
        });
    }

    componentDidMount() {
        const { tradeForChart, index, feedLicense } = this.props;

        if (!tradeForChart) throw new Error(`Trade no ${index} does not have trade param`);

        const symbol = tradeForChart.get('symbol');

        if (feedLicense === 'chartonly') {
            return;
        }

        this.subscribeToTicks(symbol).then(() => {
            const chartDiv = document.getElementById(`trade-chart${index}`);
            if (chartDiv) {
                chartDiv.dispatchEvent(new Event('zoom-in-to'));
            }
        });
        this.subscribeToOHLC(symbol);
    }

    componentWillReceiveProps(nextProps) {
        const thisSymbol = this.props.tradeForChart.get('symbol');
        const nextSymbol = nextProps.tradeForChart.get('symbol');
        if (
            (this.props.tradeForChart && nextProps.tradeForChart) &&
            (thisSymbol !== nextSymbol)
        ) {
            this.setState(defaultState);
            this.unsubscribe();

            const { feedLicense } = nextProps;
            if (feedLicense === 'chartonly') {
                return;
            }

            this.subscribeToTicks(nextSymbol).then(() => {
                const chartDiv = document.getElementById(`trade-chart${nextProps.index}`);
                if (chartDiv) {
                    chartDiv.dispatchEvent(new Event('zoom-in-to'));
                }
            });
            this.subscribeToOHLC(nextSymbol);
        }
    }

    componentDidUpdate(prevProps) {
        if (!prevProps.contractForChart && this.props.contractForChart) {
            const index = this.props.index;
            const chartDiv = document.getElementById(`trade-chart${index}`);
            if (chartDiv) {
                chartDiv.dispatchEvent(new Event('zoom-in-max'));
            }
        }
    }

    componentWillUnmount() {
        this.unsubscribe();
        this.api.events.ignoreAll('tick');
        this.api.events.ignoreAll('ohlc');
    }

    unsubscribe = () => {
        if (this.ticksId) this.api.unsubscribeByID(this.ticksId);
        if (this.ohlcId) this.api.unsubscribeByID(this.ohlcId);
    }

    updateData = (data, type) => {
        if (type === 'ticks') {
            const { times, prices } = data.history;
            const newTicks = times.map((t, idx) => {
                const quote = prices[idx];
                return { epoch: +t, quote: +quote };
            });

            const ticks = mergeTicks(this.state.ticks, newTicks);
            this.setState({ ticks });
            return ticks;
        }

        const candles = mergeCandles(this.state.candles, data.candles);
        this.setState({ candles });
        return candles;
    }

    fetchData = (start: number, end: number, type: ChartType, interval: number): Promise => {
        const { tradeForChart, feedLicense } = this.props;

        if (feedLicense === 'chartonly') return Promise.resolve();

        const symbol = tradeForChart.get('symbol');
        const count = type === 'ticks' ? 1000 : 500;

        // no need to handle error as not calling with subscribe
        return this.api
            .getTickHistory(symbol, { count, end, style: type, granularity: interval, adjust_start_time: 1 })
            .then(r => this.updateData(r, type));
    }

    subscribeToTicks = (symbol, count = 2000): Promise => {
        const callDependOnErr = (err) =>
            this.api.getTickHistory(symbol, {
                count: isMobile() ? 500 : count,
                end: 'latest',
                adjust_start_time: 1,
                subscribe: err ? undefined : 1,
            });

        return getDataWithErrorHandling(callDependOnErr).then(r => this.updateData(r, 'ticks'));
    }

    subscribeToOHLC = (symbol, count = 500, interval = 60): Promise => {
        const callDependOnErr = (err) =>
            this.api.getTickHistory(symbol, {
                    count: isMobile() ? 200 : count,
                    end: 'latest',
                    subscribe: err ? undefined : 1,
                    style: 'candles',
                    adjust_start_time: 1,
                    granularity: interval,
                });

        return getDataWithErrorHandling(callDependOnErr).then(r => this.updateData(r, 'candles'));
    }

    changeChartType = (type: ChartType) => {
        const { feedLicense } = this.props;
        const { chartType } = this.state;

        if (feedLicense === 'chartonly' || chartType === type) {
            return;
        }

        const newDataType = chartToDataType[type];
        if (newDataType === this.state.dataType) {
            this.setState({ chartType: type });
            return;
        }

        this.setState({ chartType: type, dataType: newDataType });
    }

    render() {
        const { contractForChart, index, events,
            feedLicense, pipSize, tradeForChart } = this.props;
        const { theme } = this.context;
        const { chartType, dataType } = this.state;

        const noData = feedLicense === 'chartonly';

        const mobile = isMobile();

        return (
            <BinaryChart
                id={`trade-chart${index}`}
                className="trade-chart"
                contract={contractForChart && serverContractModelToChartContractModel(contractForChart.toJS())}
                events={events}
                noData={noData}
                pipSize={pipSize}
                shiftMode={contractForChart ? 'dynamic' : 'fixed'}
                symbol={tradeForChart && tradeForChart.get('symbol')}
                assetName={tradeForChart && tradeForChart.get('symbolName')}
                ticks={this.state[dataType]}
                theme={theme}
                showTooltips={!mobile}
                hideZoomControls={mobile}
                type={chartType}
                trade={tradeForChart && internalTradeModelToChartTradeModel(tradeForChart.toJS())}
                // tradingTimes={tradingTime.times}
                getData={noData ? undefined : this.fetchData}
                onTypeChange={noData ? undefined : this.changeChartType}   // do not allow change type when there's contract
            />
        );
    }
}
<|endoftext|>


<commit_before>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import { createRecord } from '../../../database/utilities';
import { UIDatabase } from '../../../database';

/**
 * Helper method to pay off a prescription. Given a cash amount
 * a script and a patient, all receipts, credits and cash in
 * transactions will be created and the script will be finalised.
 *
 * Paying a script has three paths: underpay, exactpay and overpay.
 * All payments create a Receipt with a ReceiptLine.
 *
 * An underpayment creates a CustomerCreditLine and a ReceiptLine
 * for each source of credit (CustomerCredit) that credit was used from.
 * For example if a Patient has overpaid the last two prescriptions, then
 * there would be two sources of credit. If both sources were used to pay
 * off a script, a CustomerCreditLine is added to each CustomerCredit for
 * the amount used. A CustomerCredit can have many CustomerCreditLines If
 * only a fraction of credit is used at a time.
 *
 * An overpayment creates a CashIn Transaction to the value of the
 * amount overpaid.
 *
 * When using credit, credit must not become negative.
 * A script must be paid off in full, no partial payments.
 * Any amount overpaid must be gained in credit.
 *
 * @param {User} currentUser   The currently logged in user.
 * @param {Name} patient       A Name that is a patient
 * @param {Transaction} script A Transaction that is a prescription
 * @param {Number} cashAmount  The cash amount being paid for the script.
 * @param {Number} scriptTotal The total amount of the script.
 */

export const pay = (currentUser, patient, script, cashAmount, scriptTotal) => {
  if (!patient.isPatient) throw new Error('Patient is not a patient');
  if (!script.isPrescription) throw new Error('Script is not a script');
  if (!(script.items.length > 0)) throw new Error('Script is empty');
  if (script.isFinalised) throw new Error('Script is finalised');
  if (cashAmount < 0) throw new Error('Cash amount is negative');

  const { availableCredit } = patient;

  // Determine if this is an under, exact or over payment
  const creditAmount = scriptTotal - cashAmount;
  const usingCredit = creditAmount > 0;

  if (creditAmount > availableCredit) throw new Error('Not enough credit');

  // Create a Receipt and ReceiptLine for every payment path.
  const receipt = createRecord(UIDatabase, 'Receipt', currentUser, patient, cashAmount, '');
  createRecord(UIDatabase, 'ReceiptLine', receipt, script, cashAmount);

  // When using credit, create a CustomerCreditLine and ReceiptLine for each
  // source of credit being used.
  let creditToAllocate = creditAmount;
  if (usingCredit) {
    // Iterate over all the patients CustomerCredits, taking credit
    // from each with available credit, until all has been allocated.
    patient.customerCredits.some(customerCredit => {
      const creditFromCustomerCredit = customerCredit.outstanding;

      if (creditFromCustomerCredit >= 0) return false;

      const amountToTake = Math.min(Math.abs(creditFromCustomerCredit), creditToAllocate);

      createRecord(UIDatabase, 'CustomerCreditLine', customerCredit, amountToTake);
      createRecord(UIDatabase, 'ReceiptLine', receipt, customerCredit, -amountToTake);

      creditToAllocate -= amountToTake;

      return !creditToAllocate;
    });
  }

  if (usingCredit && creditToAllocate > 0) throw new Error('Credit not fully allocated');

  // Ensure the script is finalised once paid.
  script.finalise(UIDatabase);
};
<commit_msg>Add use of new params in pay<commit_after>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import { createRecord } from '../../../database/utilities';
import { UIDatabase } from '../../../database';

/**
 * Helper method to pay off a prescription. Given a cash amount
 * a script and a patient, all receipts, credits and cash in
 * transactions will be created and the script will be finalised.
 *
 * Paying a script has three paths: underpay, exactpay and overpay.
 * All payments create a Receipt with a ReceiptLine.
 *
 * An underpayment creates a CustomerCreditLine and a ReceiptLine
 * for each source of credit (CustomerCredit) that credit was used from.
 * For example if a Patient has overpaid the last two prescriptions, then
 * there would be two sources of credit. If both sources were used to pay
 * off a script, a CustomerCreditLine is added to each CustomerCredit for
 * the amount used. A CustomerCredit can have many CustomerCreditLines If
 * only a fraction of credit is used at a time.
 *
 * An overpayment creates a CashIn Transaction to the value of the
 * amount overpaid.
 *
 * When using credit, credit must not become negative.
 * A script must be paid off in full, no partial payments.
 * Any amount overpaid must be gained in credit.
 *
 * @param {User} currentUser   The currently logged in user.
 * @param {Name} patient       A Name that is a patient
 * @param {Transaction} script A Transaction that is a prescription
 * @param {Number} cashAmount  The cash amount being paid for the script.
 * @param {Number} scriptTotal The total amount of the script.
 */

export const pay = (
  currentUser,
  patient,
  script,
  cashAmount,
  scriptTotal,
  subtotal,
  discountAmount,
  discountRate
) => {
  if (!patient.isPatient) throw new Error('Patient is not a patient');
  if (!script.isPrescription) throw new Error('Script is not a script');
  if (!(script.items.length > 0)) throw new Error('Script is empty');
  if (script.isFinalised) throw new Error('Script is finalised');
  if (cashAmount < 0) throw new Error('Cash amount is negative');

  const { availableCredit } = patient;

  // Determine if this is an under, exact or over payment
  const creditAmount = scriptTotal - cashAmount;
  const usingCredit = creditAmount > 0;

  if (creditAmount > availableCredit) throw new Error('Not enough credit');

  // Create a Receipt and ReceiptLine for every payment path.
  const receipt = createRecord(UIDatabase, 'Receipt', currentUser, patient, cashAmount, '');
  createRecord(UIDatabase, 'ReceiptLine', receipt, script, cashAmount);

  // When using credit, create a CustomerCreditLine and ReceiptLine for each
  // source of credit being used.
  let creditToAllocate = creditAmount;
  if (usingCredit) {
    // Iterate over all the patients CustomerCredits, taking credit
    // from each with available credit, until all has been allocated.
    patient.customerCredits.some(customerCredit => {
      const creditFromCustomerCredit = customerCredit.outstanding;

      if (creditFromCustomerCredit >= 0) return false;

      const amountToTake = Math.min(Math.abs(creditFromCustomerCredit), creditToAllocate);

      createRecord(UIDatabase, 'CustomerCreditLine', customerCredit, amountToTake);
      createRecord(UIDatabase, 'ReceiptLine', receipt, customerCredit, -amountToTake);

      creditToAllocate -= amountToTake;

      return !creditToAllocate;
    });
  }

  if (usingCredit && creditToAllocate > 0) throw new Error('Credit not fully allocated');

  // Ensure the script is finalised once paid.
  script.subtotal = subtotal;
  script.total = subtotal;
  script.insuranceDiscountAmount = discountAmount;
  script.insuranceDiscountRate = discountRate;
  script.finalise(UIDatabase);
};
<|endoftext|>


<commit_before>

// vAccordion directive
angular.module('vAccordion.directives')
  .directive('vAccordion', vAccordionDirective);

function vAccordionDirective ($timeout) {
  return {
    restrict: 'E',
    transclude: true,
    controller: vAccordionController,
    scope: {
      control: '=?',
      allowMultiple: '=?multiple',
      expandCb: '&?onexpand',
      collapseCb: '&?oncollapse',
      id: '@?'
    },
    link: {
      pre: function (scope, iElement, iAttrs) {
        scope.allowMultiple = (angular.isDefined(iAttrs.multiple) && iAttrs.multiple === '');
      },
      post: function (scope, iElement, iAttrs, ctrl, transclude) {
        transclude(scope.$parent.$new(), function (clone, transclusionScope) {
          transclusionScope.$accordion = scope.internalControl;
          if (scope.id) { transclusionScope.$accordion.id = scope.id; }
          iElement.append(clone);
        });

        iAttrs.$set('role', 'tablist');

        if (scope.allowMultiple) {
          iAttrs.$set('aria-multiselectable', 'true');
        }

        if (angular.isDefined(scope.control)) {
          checkCustomControlAPIMethods();

          var mergedControl = angular.extend({}, scope.internalControl, scope.control);
          scope.control = scope.internalControl = mergedControl;
        }
        else {
          scope.control = scope.internalControl;
        }

        function checkCustomControlAPIMethods () {
          var protectedApiMethods = ['toggle', 'expand', 'collapse', 'expandAll', 'collapseAll', 'hasExpandedPane'];

          angular.forEach(protectedApiMethods, function (iteratedMethodName) {
            if (scope.control[iteratedMethodName]) {
              throw new Error('The `' + iteratedMethodName + '` method can not be overwritten');
            }
          });
        }

        $timeout(function () {
          var eventName = (angular.isDefined(ctrl.getAccordionId())) ? ctrl.getAccordionId() + ':onReady' : 'vAccordion:onReady';
          scope.$emit(eventName);
        }, 0);
      }
    }
  };
}
vAccordionDirective.$inject = ['$timeout'];

// vAccordion directive controller
function vAccordionController ($scope) {
  var ctrl = this;
  var isDisabled = false;

  $scope.panes = [];

	$scope.expandCb = (angular.isFunction($scope.expandCb)) ? $scope.expandCb : angular.noop;
	$scope.collapseCb = (angular.isFunction($scope.collapseCb)) ? $scope.collapseCb : angular.noop;

  ctrl.hasExpandedPane = function hasExpandedPane () {
    var bool = false;

    for (var i = 0, length = $scope.panes.length; i < length; i++) {
      var iteratedPane = $scope.panes[i];

      if (iteratedPane.isExpanded) {
        bool = true;
        break;
      }
    }

    return bool;
  };

  ctrl.getPaneByIndex = function getPaneByIndex (index) {
    var thePane;

    angular.forEach($scope.panes, function (iteratedPane) {
      if (iteratedPane.$parent && angular.isDefined(iteratedPane.$parent.$index) && iteratedPane.$parent.$index === index) {
        thePane = iteratedPane;
      }
    });

    return (thePane) ? thePane : $scope.panes[index];
  };

  ctrl.getPaneIndex = function getPaneIndex (pane) {
    var theIndex;

    angular.forEach($scope.panes, function (iteratedPane) {
      if (iteratedPane.$parent && angular.isDefined(iteratedPane.$parent.$index) && iteratedPane === pane) {
        theIndex = iteratedPane.$parent.$index;
      }
    });

    return (angular.isDefined(theIndex)) ? theIndex : $scope.panes.indexOf(pane);
  };

  ctrl.getPaneById = function getPaneById (id) {
    var thePane;

    angular.forEach($scope.panes, function (iteratedPane) {
      if (iteratedPane.id && iteratedPane.id === id) {
        thePane = iteratedPane;
      }
    });

    return thePane;
  };

  ctrl.getPaneId = function getPaneId (pane) {
    return pane.id;
  };

  ctrl.getAccordionId = function getAccordionId () {
    return $scope.id;
  };

  ctrl.disable = function disable () {
    isDisabled = true;
  };

  ctrl.enable = function enable () {
    isDisabled = false;
  };

  ctrl.addPane = function addPane (paneToAdd) {
    if (!$scope.allowMultiple) {
      if (ctrl.hasExpandedPane() && paneToAdd.isExpanded) {
        throw new Error('The `multiple` attribute can\'t be found');
      }
    }

    $scope.panes.push(paneToAdd);

    if (paneToAdd.isExpanded) {
      $scope.expandCb({ index: ctrl.getPaneIndex(paneToAdd), id: paneToAdd.id, pane: paneToAdd });
    }
  };

  ctrl.focusNext = function focusNext () {
    var length = $scope.panes.length;

    for (var i = 0; i < length; i++) {
      var iteratedPane = $scope.panes[i];

      if (iteratedPane.isFocused) {
        var paneToFocusIndex = i + 1;

        if (paneToFocusIndex > $scope.panes.length - 1) {
          paneToFocusIndex = 0;
        }

        var paneToFocus = $scope.panes[paneToFocusIndex];
            paneToFocus.paneElement.find('v-pane-header')[0].focus();

        break;
      }
    }
  };

  ctrl.focusPrevious = function focusPrevious () {
    var length = $scope.panes.length;

    for (var i = 0; i < length; i++) {
      var iteratedPane = $scope.panes[i];

      if (iteratedPane.isFocused) {
        var paneToFocusIndex = i - 1;

        if (paneToFocusIndex < 0) {
          paneToFocusIndex = $scope.panes.length - 1;
        }

        var paneToFocus = $scope.panes[paneToFocusIndex];
            paneToFocus.paneElement.find('v-pane-header')[0].focus();

        break;
      }
    }
  };

  ctrl.toggle = function toggle (paneToToggle) {
    if (isDisabled || !paneToToggle) { return; }

    if (!$scope.allowMultiple) {
      ctrl.collapseAll(paneToToggle);
    }

    paneToToggle.isExpanded = !paneToToggle.isExpanded;

    if (paneToToggle.isExpanded) {
      $scope.expandCb({ index: ctrl.getPaneIndex(paneToToggle), id: paneToToggle.id, pane: paneToToggle });
    } else {
      $scope.collapseCb({ index: ctrl.getPaneIndex(paneToToggle), id: paneToToggle.id, pane: paneToToggle });
    }
  };

  ctrl.expand = function expand (paneToExpand) {
    if (isDisabled || !paneToExpand) { return; }

    if (!$scope.allowMultiple) {
      ctrl.collapseAll(paneToExpand);
    }

    if (!paneToExpand.isExpanded) {
      paneToExpand.isExpanded = true;
      $scope.expandCb({ index: ctrl.getPaneIndex(paneToExpand), id: paneToExpand.id, pane: paneToExpand });
    }
  };

  ctrl.collapse = function collapse (paneToCollapse) {
    if (isDisabled || !paneToCollapse) { return; }

    if (paneToCollapse.isExpanded) {
      paneToCollapse.isExpanded = false;
      $scope.collapseCb({ index: ctrl.getPaneIndex(paneToCollapse), id: paneToCollapse.id, pane: paneToCollapse });
    }
  };

  ctrl.expandAll = function expandAll () {
    if (isDisabled) { return; }

    if ($scope.allowMultiple) {
      angular.forEach($scope.panes, function (iteratedPane) {
        ctrl.expand(iteratedPane);
      });
    } else {
      throw new Error('The `multiple` attribute can\'t be found');
    }
  };

  ctrl.collapseAll = function collapseAll (exceptionalPane) {
    if (isDisabled) { return; }

    angular.forEach($scope.panes, function (iteratedPane) {
      if (iteratedPane !== exceptionalPane) {
        ctrl.collapse(iteratedPane);
      }
    });
  };

  // API
  $scope.internalControl = {
    toggle: function toggle (indexOrId) {
      if (angular.isString(indexOrId)) {
        ctrl.toggle( ctrl.getPaneById(indexOrId) );
      } else {
        ctrl.toggle( ctrl.getPaneByIndex(indexOrId) );
      }
    },
    expand: function expand (indexOrId) {
      if (angular.isString(indexOrId)) {
        ctrl.expand( ctrl.getPaneById(indexOrId) );
      } else {
        ctrl.expand( ctrl.getPaneByIndex(indexOrId) );
      }
    },
    collapse: function collapse (indexOrId) {
      if (angular.isString(indexOrId)) {
        ctrl.collapse( ctrl.getPaneById(indexOrId) );
      } else {
        ctrl.collapse( ctrl.getPaneByIndex(indexOrId) );
      }
    },
    expandAll: ctrl.expandAll,
    collapseAll: ctrl.collapseAll,
    hasExpandedPane: ctrl.hasExpandedPane
  };
}
vAccordionController.$inject = ['$scope'];
<commit_msg> attribute with  value<commit_after>

// vAccordion directive
angular.module('vAccordion.directives')
  .directive('vAccordion', vAccordionDirective);

function vAccordionDirective ($timeout) {
  return {
    restrict: 'E',
    transclude: true,
    controller: vAccordionController,
    scope: {
      control: '=?',
      expandCb: '&?onexpand',
      collapseCb: '&?oncollapse',
      id: '@?'
    },
    link: {
      pre: function (scope, iElement, iAttrs) {
        scope.allowMultiple = (angular.isDefined(iAttrs.multiple) && (iAttrs.multiple === '' || iAttrs.multiple === 'true'));
      },
      post: function (scope, iElement, iAttrs, ctrl, transclude) {
        transclude(scope.$parent.$new(), function (clone, transclusionScope) {
          transclusionScope.$accordion = scope.internalControl;
          if (scope.id) { transclusionScope.$accordion.id = scope.id; }
          iElement.append(clone);
        });

        iAttrs.$set('role', 'tablist');

        if (scope.allowMultiple) {
          iAttrs.$set('aria-multiselectable', 'true');
        }

        if (angular.isDefined(scope.control)) {
          checkCustomControlAPIMethods();

          var mergedControl = angular.extend({}, scope.internalControl, scope.control);
          scope.control = scope.internalControl = mergedControl;
        }
        else {
          scope.control = scope.internalControl;
        }

        function checkCustomControlAPIMethods () {
          var protectedApiMethods = ['toggle', 'expand', 'collapse', 'expandAll', 'collapseAll', 'hasExpandedPane'];

          angular.forEach(protectedApiMethods, function (iteratedMethodName) {
            if (scope.control[iteratedMethodName]) {
              throw new Error('The `' + iteratedMethodName + '` method can not be overwritten');
            }
          });
        }

        $timeout(function () {
          var eventName = (angular.isDefined(ctrl.getAccordionId())) ? ctrl.getAccordionId() + ':onReady' : 'vAccordion:onReady';
          scope.$emit(eventName);
        }, 0);
      }
    }
  };
}
vAccordionDirective.$inject = ['$timeout'];

// vAccordion directive controller
function vAccordionController ($scope) {
  var ctrl = this;
  var isDisabled = false;

  $scope.panes = [];

	$scope.expandCb = (angular.isFunction($scope.expandCb)) ? $scope.expandCb : angular.noop;
	$scope.collapseCb = (angular.isFunction($scope.collapseCb)) ? $scope.collapseCb : angular.noop;

  ctrl.hasExpandedPane = function hasExpandedPane () {
    var bool = false;

    for (var i = 0, length = $scope.panes.length; i < length; i++) {
      var iteratedPane = $scope.panes[i];

      if (iteratedPane.isExpanded) {
        bool = true;
        break;
      }
    }

    return bool;
  };

  ctrl.getPaneByIndex = function getPaneByIndex (index) {
    var thePane;

    angular.forEach($scope.panes, function (iteratedPane) {
      if (iteratedPane.$parent && angular.isDefined(iteratedPane.$parent.$index) && iteratedPane.$parent.$index === index) {
        thePane = iteratedPane;
      }
    });

    return (thePane) ? thePane : $scope.panes[index];
  };

  ctrl.getPaneIndex = function getPaneIndex (pane) {
    var theIndex;

    angular.forEach($scope.panes, function (iteratedPane) {
      if (iteratedPane.$parent && angular.isDefined(iteratedPane.$parent.$index) && iteratedPane === pane) {
        theIndex = iteratedPane.$parent.$index;
      }
    });

    return (angular.isDefined(theIndex)) ? theIndex : $scope.panes.indexOf(pane);
  };

  ctrl.getPaneById = function getPaneById (id) {
    var thePane;

    angular.forEach($scope.panes, function (iteratedPane) {
      if (iteratedPane.id && iteratedPane.id === id) {
        thePane = iteratedPane;
      }
    });

    return thePane;
  };

  ctrl.getPaneId = function getPaneId (pane) {
    return pane.id;
  };

  ctrl.getAccordionId = function getAccordionId () {
    return $scope.id;
  };

  ctrl.disable = function disable () {
    isDisabled = true;
  };

  ctrl.enable = function enable () {
    isDisabled = false;
  };

  ctrl.addPane = function addPane (paneToAdd) {
    if (!$scope.allowMultiple) {
      if (ctrl.hasExpandedPane() && paneToAdd.isExpanded) {
        throw new Error('The `multiple` attribute can\'t be found');
      }
    }

    $scope.panes.push(paneToAdd);

    if (paneToAdd.isExpanded) {
      $scope.expandCb({ index: ctrl.getPaneIndex(paneToAdd), id: paneToAdd.id, pane: paneToAdd });
    }
  };

  ctrl.focusNext = function focusNext () {
    var length = $scope.panes.length;

    for (var i = 0; i < length; i++) {
      var iteratedPane = $scope.panes[i];

      if (iteratedPane.isFocused) {
        var paneToFocusIndex = i + 1;

        if (paneToFocusIndex > $scope.panes.length - 1) {
          paneToFocusIndex = 0;
        }

        var paneToFocus = $scope.panes[paneToFocusIndex];
            paneToFocus.paneElement.find('v-pane-header')[0].focus();

        break;
      }
    }
  };

  ctrl.focusPrevious = function focusPrevious () {
    var length = $scope.panes.length;

    for (var i = 0; i < length; i++) {
      var iteratedPane = $scope.panes[i];

      if (iteratedPane.isFocused) {
        var paneToFocusIndex = i - 1;

        if (paneToFocusIndex < 0) {
          paneToFocusIndex = $scope.panes.length - 1;
        }

        var paneToFocus = $scope.panes[paneToFocusIndex];
            paneToFocus.paneElement.find('v-pane-header')[0].focus();

        break;
      }
    }
  };

  ctrl.toggle = function toggle (paneToToggle) {
    if (isDisabled || !paneToToggle) { return; }

    if (!$scope.allowMultiple) {
      ctrl.collapseAll(paneToToggle);
    }

    paneToToggle.isExpanded = !paneToToggle.isExpanded;

    if (paneToToggle.isExpanded) {
      $scope.expandCb({ index: ctrl.getPaneIndex(paneToToggle), id: paneToToggle.id, pane: paneToToggle });
    } else {
      $scope.collapseCb({ index: ctrl.getPaneIndex(paneToToggle), id: paneToToggle.id, pane: paneToToggle });
    }
  };

  ctrl.expand = function expand (paneToExpand) {
    if (isDisabled || !paneToExpand) { return; }

    if (!$scope.allowMultiple) {
      ctrl.collapseAll(paneToExpand);
    }

    if (!paneToExpand.isExpanded) {
      paneToExpand.isExpanded = true;
      $scope.expandCb({ index: ctrl.getPaneIndex(paneToExpand), id: paneToExpand.id, pane: paneToExpand });
    }
  };

  ctrl.collapse = function collapse (paneToCollapse) {
    if (isDisabled || !paneToCollapse) { return; }

    if (paneToCollapse.isExpanded) {
      paneToCollapse.isExpanded = false;
      $scope.collapseCb({ index: ctrl.getPaneIndex(paneToCollapse), id: paneToCollapse.id, pane: paneToCollapse });
    }
  };

  ctrl.expandAll = function expandAll () {
    if (isDisabled) { return; }

    if ($scope.allowMultiple) {
      angular.forEach($scope.panes, function (iteratedPane) {
        ctrl.expand(iteratedPane);
      });
    } else {
      throw new Error('The `multiple` attribute can\'t be found');
    }
  };

  ctrl.collapseAll = function collapseAll (exceptionalPane) {
    if (isDisabled) { return; }

    angular.forEach($scope.panes, function (iteratedPane) {
      if (iteratedPane !== exceptionalPane) {
        ctrl.collapse(iteratedPane);
      }
    });
  };

  // API
  $scope.internalControl = {
    toggle: function toggle (indexOrId) {
      if (angular.isString(indexOrId)) {
        ctrl.toggle( ctrl.getPaneById(indexOrId) );
      } else {
        ctrl.toggle( ctrl.getPaneByIndex(indexOrId) );
      }
    },
    expand: function expand (indexOrId) {
      if (angular.isString(indexOrId)) {
        ctrl.expand( ctrl.getPaneById(indexOrId) );
      } else {
        ctrl.expand( ctrl.getPaneByIndex(indexOrId) );
      }
    },
    collapse: function collapse (indexOrId) {
      if (angular.isString(indexOrId)) {
        ctrl.collapse( ctrl.getPaneById(indexOrId) );
      } else {
        ctrl.collapse( ctrl.getPaneByIndex(indexOrId) );
      }
    },
    expandAll: ctrl.expandAll,
    collapseAll: ctrl.collapseAll,
    hasExpandedPane: ctrl.hasExpandedPane
  };
}
vAccordionController.$inject = ['$scope'];
<|endoftext|>


<commit_before>import getUpstreamChanges from 'viewmodel/helpers/getUpstreamChanges';
import notifyPatternObservers from 'viewmodel/prototype/applyChanges/notifyPatternObservers';

var unwrap = { evaluateWrapped: true },
	dependantGroups = [ 'observers', 'default' ];

export default function Viewmodel$applyChanges () {
	var self = this,
		changes,
		upstreamChanges,
		allChanges = [],
		computations,
		addComputations,
		cascade,
		hash = {};

	if ( !this.changes.length ) {
		// TODO we end up here on initial render. Perhaps we shouldn't?
		return;
	}

	addComputations = function ( keypath ) {
		var newComputations;

		if ( newComputations = self.deps.computed[ keypath ] ) {
			addNewItems( computations, newComputations );
		}
	};

	cascade = function ( keypath ) {
		var map;

		addComputations( keypath );

		if ( map = self.depsMap.computed[ keypath ] ) {
			map.forEach( cascade );
		}
	};

	// Find computations and evaluators that are invalidated by
	// these changes. If they have changed, add them to the
	// list of changes. Lather, rinse and repeat until the
	// system is settled
	do {
		changes = this.changes;
		addNewItems( allChanges, changes );

		this.changes = [];
		computations = [];

		upstreamChanges = getUpstreamChanges( changes );
		upstreamChanges.forEach( addComputations );

		changes.forEach( cascade );

		computations.forEach( updateComputation );
	} while ( this.changes.length );

	upstreamChanges = getUpstreamChanges( allChanges );

	// Pattern observers are a weird special case
	if ( this.patternObservers.length ) {
		upstreamChanges.forEach( keypath => notifyPatternObservers( this, keypath, true ) );
		allChanges.forEach( keypath => notifyPatternObservers( this, keypath ) );
	}

	dependantGroups.forEach( group => {
		upstreamChanges.forEach( keypath => notifyDependants( this, keypath, group, true ) );
		allChanges.forEach( keypath => notifyDependants( this, keypath, group ) );
	});

	// Return a hash of keypaths to updated values
	allChanges.forEach( keypath => {
		hash[ keypath ] = this.get( keypath );
	});

	return hash;
}

function updateComputation ( computation ) {
	computation.update();
}

function notifyDependants ( viewmodel, keypath, group, onlyDirect ) {
	var depsByKeypath = viewmodel.deps[ group ], value, unwrapped;

	if ( !depsByKeypath ) {
		return;
	}

	// update dependants of this keypath
	value = viewmodel.get( keypath );
	unwrapped = viewmodel.get( keypath, unwrap );

	updateAll( depsByKeypath[ keypath ], value, unwrapped );

	// If we're only notifying direct dependants, not dependants
	// of downstream keypaths, then YOU SHALL NOT PASS
	if ( onlyDirect ) {
		return;
	}

	// otherwise, cascade
	cascade( viewmodel.depsMap[ group ][ keypath ], viewmodel, group );
}

function updateAll ( dependants, value, unwrapped ) {
	if ( dependants ) {
		dependants.forEach( d => d.setValue( value, unwrapped ) );
	}
}

function cascade ( childDeps, viewmodel, group ) {
	var i;

	if ( childDeps ) {
		i = childDeps.length;
		while ( i-- ) {
			notifyDependants( viewmodel, childDeps[i], group );
		}
	}
}

function addNewItems ( arr, items ) {
	items.forEach( item => {
		if ( arr.indexOf( item ) === -1 ) {
			arr.push( item );
		}
	});
}
<commit_msg>snapshot dependants before notifying them of changes, to avoid double updates<commit_after>import getUpstreamChanges from 'viewmodel/helpers/getUpstreamChanges';
import notifyPatternObservers from 'viewmodel/prototype/applyChanges/notifyPatternObservers';

var unwrap = { evaluateWrapped: true },
	dependantGroups = [ 'observers', 'default' ];

export default function Viewmodel$applyChanges () {
	var self = this,
		changes,
		upstreamChanges,
		allChanges = [],
		computations,
		addComputations,
		cascade,
		hash = {};

	if ( !this.changes.length ) {
		// TODO we end up here on initial render. Perhaps we shouldn't?
		return;
	}

	addComputations = function ( keypath ) {
		var newComputations;

		if ( newComputations = self.deps.computed[ keypath ] ) {
			addNewItems( computations, newComputations );
		}
	};

	cascade = function ( keypath ) {
		var map;

		addComputations( keypath );

		if ( map = self.depsMap.computed[ keypath ] ) {
			map.forEach( cascade );
		}
	};

	// Find computations and evaluators that are invalidated by
	// these changes. If they have changed, add them to the
	// list of changes. Lather, rinse and repeat until the
	// system is settled
	do {
		changes = this.changes;
		addNewItems( allChanges, changes );

		this.changes = [];
		computations = [];

		upstreamChanges = getUpstreamChanges( changes );
		upstreamChanges.forEach( addComputations );

		changes.forEach( cascade );

		computations.forEach( updateComputation );
	} while ( this.changes.length );

	upstreamChanges = getUpstreamChanges( allChanges );

	// Pattern observers are a weird special case
	if ( this.patternObservers.length ) {
		upstreamChanges.forEach( keypath => notifyPatternObservers( this, keypath, true ) );
		allChanges.forEach( keypath => notifyPatternObservers( this, keypath ) );
	}

	dependantGroups.forEach( group => {
		if ( !this.deps[ group ] ) {
			return;
		}

		upstreamChanges.forEach( keypath => notifyUpstreamDependants( this, keypath, group ) );
		notifyAllDependants( this, allChanges, group );
	});

	// Return a hash of keypaths to updated values
	allChanges.forEach( keypath => {
		hash[ keypath ] = this.get( keypath );
	});

	return hash;
}

function updateComputation ( computation ) {
	computation.update();
}

function notifyUpstreamDependants ( viewmodel, keypath, groupName ) {
	var dependants, value;

	if ( dependants = findDependants( viewmodel, keypath, groupName ) ) {
		value = viewmodel.get( keypath, unwrap );
		dependants.forEach( d => d.setValue( value ) );
	}
}

function notifyAllDependants ( viewmodel, keypaths, groupName ) {
	var queue = [];

	addKeypaths( keypaths );
	queue.forEach( dispatch );

	function addKeypaths ( keypaths ) {
		keypaths.forEach( addKeypath );
		keypaths.forEach( cascade );
	}

	function addKeypath ( keypath ) {
		var deps = findDependants( viewmodel, keypath, groupName );

		if ( deps ) {
			queue.push({
				keypath: keypath,
				deps: deps
			});
		}
	}

	function cascade ( keypath ) {
		var childDeps;

		if ( childDeps = viewmodel.depsMap[ groupName ][ keypath ] ) {
			addKeypaths( childDeps );
		}
	}

	function dispatch ( set ) {
		var value = viewmodel.get( set.keypath, unwrap );
		set.deps.forEach( d => d.setValue( value ) );
	}
}

function findDependants ( viewmodel, keypath, groupName ) {
	var group = viewmodel.deps[ groupName ];
	return group ?  group[ keypath ] : null;
}

function addNewItems ( arr, items ) {
	items.forEach( item => {
		if ( arr.indexOf( item ) === -1 ) {
			arr.push( item );
		}
	});
}
<|endoftext|>


<commit_before>import path from 'path';
import defaultsDeep from 'lodash/defaultsDeep';

const entryError =
    `Le point d'entrée correspond au point d'entrée dans la SPA,
le paramètre devrait être une chaine de caractère correspondant au fichier de lancement de l'application, ou à son dossier si c'est un index.js
Par exemple (et par défaut) path.resolve(process.cwd(), './src'), càd le dossier src du dossier courant`;

/**
 * Classe pour construire une configuration de façon simple, et modulable.
 *
 * @class ConfigBuilder
 */
class ConfigBuilder {
    configEnv = {};
    entries = [];
    output = {
        // libraryTarget: 'umd'
    };
    devtool = false;
    stats = {};
    plugins = [];
    loaders = [];
    extensions = ['.js', '.jsx'];
    rules = [];
    debugConfig = false;
    sourceMaps = false;
    externals = {};
    definedVariables = {};
    projectName = null;
    aliases = {};

    _debugInfo() {
        if (this.debugConfig) {
            console.log('########################################################################');
            console.log(arguments);
            console.log('########################################################################');
            console.log();
        }
    }

    /**
     * Ajoute un point d'entrée dans l'application
     * Cf https://webpack.js.org/configuration/entry-context/#entry
     *
     * @param {Array|string} entry Le chemin indiquant le point d'entrée de l'application (relatif, ou absolu), par exemple path.resolve(process.cwd(), './src'), ou path.resolve(process.cwd(), './src/index.js'), ou './src'
     *
     * @memberOf ConfigBuilder
     */
    addEntry(entry) {
        this._debugInfo('addEntry', [...arguments]);
        if (Array.isArray(entry)) {
            this.entries.push(...entry);
        } else if (typeof (entry) === 'string') {
            this.entries.push(entry);
        } else {
            throw new Error(entryError);
        }
    }

    /**
     * Indique le dossier de sortie du build webpack.
     * Cf https://webpack.js.org/configuration/output/#output-path
     *
     * @param {string} outPath Le path pour le dossier d'output
     * @param {boolean} [isRelative=true] Indique si le path donnée est relatif par rapport au dossier du projet (par défaut), ou absolu.
     *
     * @memberOf ConfigBuilder
     */
    setOuputPath(outPath, isRelative = true) {
        this._debugInfo('setOuputPath', [...arguments]);
        this.output.path = isRelative ? path.resolve(process.cwd(), outPath) : outPath;
    }

    /**
     * Ajoute un alias pour la résolution des sources (ex: test focus, etc).
     * Cf https://webpack.js.org/configuration/resolve/#resolve-alias
     *
     * @param {any} aliasName le nom de l'alias
     * @param {any} aliasPath le path du dossier/fichier pour la résolution
     * @param {boolean} [isRelative=true] Indique si le path donnée est relatif par rapport au dossier du projet (par défaut), ou absolu.
     *
     * @memberOf ConfigBuilder
     */
    addAlias(aliasName, aliasPath, isRelative = true) {
        this._debugInfo('addAlias', [...arguments]);
        this.aliases[aliasName] = isRelative ? path.resolve(process.cwd(), aliasPath) : aliasPath;
    }

    /**
     * Indique le dossier contenant les assets servies par la SPA (css, images, ...).
     * Cf https://webpack.js.org/configuration/output/#output-publicpath
     *
     * @param {string} path Le path pour le dossier d'assets
     *
     * @memberOf ConfigBuilder
     */
    setAssetsPublicPath(path) {
        this._debugInfo('setAssetsPublicPath', [...arguments]);
        this.output.publicPath = path;
    }

    /**
     * Indique le nom des chunk en cas de require.ensure.
     * @param {string} name
     */
    setChunkFileName(name) {
        this._debugInfo('setChunkFileName', [...arguments]);
        this.output.chunkFilename = name;
    }

    /**
     * Indique le nom du fichier d'output, sans l'extension. Peut contenir [name], [id] et [contenthash]
     * Cf https://webpack.js.org/configuration/output/#output-filename
     * Cf https://github.com/webpack-contrib/extract-text-webpack-plugin
     *
     * @param {string} name le nom du fichier JS et CSS sans l'extension (par défault, le nom du package NPM :`${npm_package_name}`)
     *
     * @memberOf ConfigBuilder
     */
    setFilename(name) {
        this._debugInfo('setFilename', [...arguments]);
        this.projectName = name;
        this.output.filename = this.projectName + '.js';
    }

    /**
     * Ajoute une extension dans la liste des extensions automatiquement résolues (par défaut, .js, .jsx, .json).
     * Cf https://webpack.js.org/configuration/resolve/#resolve-extensions
     *
     * @param {string} extension une extension à résoudre automatiquement
     *
     * @memberOf ConfigBuilder
     */
    addExtension(extension) {
        this._debugInfo('addExtension', [...arguments]);
        if (Array.isArray(extension)) {
            this.extensions.push(...extension);
        } else if (typeof (extension) === 'string') {
            this.extensions.push(extension);
        } else {
            throw new Error('Une extension est soit une chaine de caractère, soit tableau de chaine de caractère');
        }
    }

    _getInsertIndex(orderedList, newOrdre) {
        return orderedList.findIndex(({ ordre }) => ordre > newOrdre);
    }

    _getElement(orderedList, eltOrdre) {
        return orderedList.find(({ ordre }) => eltOrdre === ordre);
    }

    _getIndexElement(orderedList, eltOrdre) {
        return orderedList.findIndex(({ ordre }) => eltOrdre === ordre);
    }

    _insertElt(orderedList, newElt) {
        if (this._getIndexElement(orderedList, newElt.ordre) !== -1) {
            throw new Error('Un élément d\'ordre ' + newElt.ordre + ' existe déjà');
        }
        const index = this._getInsertIndex(orderedList, newElt.ordre);
        if (index === -1) {
            // L'élément a l'ordre le plus grand
            orderedList.push(newElt)
        } else {
            // Sinon, on injecte juste avant le plugin ayant un ordre plus élevé
            orderedList.splice(index, 0, newElt);
        }
    }

    _removeElt(orderedList, ordre) {
        const index = this._getIndexElement(orderedList, ordre);
        if (index === -1) {
            console.warn("Aucun élément d'ordre " + ordre + " n'existe, donc aucun élément n'a été retiré")
            return null;
        } else {
            return orderedList.splice(index, 1)[0];
        }
    }

    /**
     * Ajoute un plugin à la configuration webpack, à la position indiquée.
     * Cf https://webpack.js.org/configuration/plugins/#plugins
     *
     * @param {int} ordre la position dans la liste des plugins (les plugins par défaut sont espacés de 10, pour permettre l'injection)
     * @param {any} plugin Le plugin webpack à insérer
     *
     * @memberOf ConfigBuilder
     */
    addPlugin(ordre, plugin) {
        this._debugInfo('addPlugin', [...arguments]);
        this._insertElt(this.plugins, { ordre, plugin })
    }

    /**
     * Retire un plugin à la configuration webpack, à la position indiquée.
     *
     * @param {int} ordre la position dans la liste des plugins
     * @returns {object} l'élément supprimé
     * @memberof ConfigBuilder
     *
     */
    removePlugin(ordre) {
        this._debugInfo('removePlugin', [...arguments]);
        return (this._removeElt(this.plugins, ordre) || {}).plugin;
    }

    /**
     * Accède à un plugin de la configuration webpack, à la position indiquée.
     *
     * @param {int} ordre la position dans la liste des plugins
     * @returns {object} l'élément
     * @memberof ConfigBuilder
     *
     */
    getPlugin(ordre) {
        this._debugInfo('getPlugin', [...arguments]);
        return (this._getElement(this.plugins, ordre) || {}).plugin;
    }

    /**
     * Ajoute un loader/Rule simple à la configuration webpack, à la position indiquée.
     * Cf https://webpack.js.org/configuration/module/#rule
     * Cf https://webpack.js.org/configuration/module/#rule-loader
     * Cf https://webpack.js.org/configuration/module/#rule-use
     *
     * @param {int} ordre la position dans la liste des loaders (les loaders par défaut sont espacés de 10, pour permettre l'injection)
     * @param {RegExp} test le test pour matcher ou non le fichier
     * @param {string} loader le nom du loader
     * @param {object} [options=null] un objet d'options, pour le loader
     * @param {boolean} [isPreloader=false] si le loader est un pre-loader
     *
     * @memberOf ConfigBuilder
     */
    addSimpleLoader(ordre, test, loader, options = null, isPreloader = false) {
        this._debugInfo('addSimpleLoader', [...arguments]);
        const newLoader = { ordre, loader: { test, loader } };
        if (options !== null) {
            newLoader.loader.options = options;
        }
        if (isPreloader) {
            newLoader.loader.enforce = 'pre';
        }
        this._insertElt(this.loaders, newLoader)
    }

    /**
     * Ajoute un loader/Rule à la configuration webpack, à la position indiquée.
     * Cf https://webpack.js.org/configuration/module/#rule
     * Cf https://webpack.js.org/configuration/module/#rule-loader
     * Cf https://webpack.js.org/configuration/module/#rule-use     *
     * @param {int} ordre la position dans la liste des loaders (les loaders par défaut sont espacés de 10, pour permettre l'injection)
     * @param {object} rule l'objet
     *
     * @memberOf ConfigBuilder
     */
    addComplexLoader(ordre, rule) {
        this._debugInfo('addComplexLoader', [...arguments]);
        const newLoader = { ordre, loader: rule };
        this._insertElt(this.loaders, newLoader)
    }

    /**
     * Retire un loader à la configuration webpack, à la position indiquée.
     *
     * @param {int} ordre la position dans la liste des loaders
     * @returns {object} l'élément supprimé
     * @memberof ConfigBuilder
     *
     */
    removeLoader(ordre) {
        this._debugInfo('removeLoader', [...arguments]);
        return (this._removeElt(this.loaders, ordre) || {}).loader;
    }

    /**
     * Accède à un loader de la configuration webpack, à la position indiquée.
     *
     * @param {int} ordre la position dans la liste des loaders
     * @returns {object} l'élément
     * @memberof ConfigBuilder
     *
     */
    getLoader(ordre) {
        this._debugInfo('getLoader', [...arguments]);
        return (this._getElement(this.loaders, ordre) || {}).loader;
    }

    /**
     * Indique si les sourcemaps doivent être utilisé ou non.
     *
     * @param {boolean} useSourceMaps si les sourcemaps doivent être utilisé ou non.
     *
     * @memberOf ConfigBuilder
     */
    useSourceMaps(useSourceMaps = false) {
        this._debugInfo('useSourceMaps', [...arguments]);
        this.sourceMaps = useSourceMaps;
    }

    /**
     * Ajoute une variable qui sera définie par le plugin DefineVariable, et injectée (càd remplacée par sa valeur) par webpack.
     * Note : Il est nécessaire d'utiliser getDefinedVariables dans le cas d'une configuration non basée sur celle par défaut.
     *
     * @param {string} key le nom de la variable
     * @param {string} value la valeur de la variable
     *
     * @memberOf ConfigBuilder
     */
    addDefinedVariable(key, value) {
        this.definedVariables[key] = value;
    }

    /**
     * Retourne un objet de config pour le plugin webpack.DefinePlugin.
     *
     * @returns {object} l'objet de config pour le plugin DefinePlugin
     *
     * @memberOf ConfigBuilder
     */
    getDefinedVariables() {
        return this.definedVariables;
    }

    /**
     * Retourne le nom du fichier css (pour ExtractTextPlugin).
     *
     * @returns {string} le nom du fichier css
     *
     * @memberOf ConfigBuilder
     */
    getCssFilename() {
        return this.projectName + '.css';
    }

    /**
     * Ajoute un external à la configuration webpack, càd un nom qui sera résolu de manière global (jQuery, Backbone, ...)
     * Cf https://webpack.js.org/configuration/externals/
     *
     * @param {string} key le nom de la variable
     * @param {string|object} value la valeur à résoudre dans le bundle au runtime (peut être par type de lib), cf doc webpack
     *
     * @memberOf ConfigBuilder
     */
    addExternal(key, value) {
        this.externals[key] = value;
    }

    _buildConfig(env) {
        const config = {
            entry: this.entries,
            output: this.output,
            resolve: {
                extensions: this.extensions,
                alias: this.aliases
            },
            plugins: this.plugins.map(({ plugin }) => plugin).map(item => typeof (item) === 'function' ? item(env) : item),
            module: {
                rules: this.loaders.map(({ loader }) => loader).map(item => typeof (item) === 'function' ? item(env) : item)
            },
            externals: this.externals,
            stats: {
                colors: true,
                version: false,
                timings: false,
                assets: false,
                chunks: false,
                modules: false,
                reasons: false,
                children: false,
                source: false,
                errors: true,
                errorDetails: true,
                warnings: true
            }
        };
        if (this.sourceMaps) {
            config.devtool = env.MINIMIFY ? 'source-map' : 'eval-source-map';

        }
        return config;
    }

    /**
     * Construit l'objet de configuration webpack.
     *
     * @param {object} [env={}] une object contenant des valeurs pour la résolution des plugins et loaders.
     * @param {object} [customConf={}] une configuration custom, pouvant être mergée avec la conf buildée.
     * @returns {object} la configuration buildée, et mergée
     *
     * @memberOf ConfigBuilder
     */
    toWebpackConfig(env = {}, customConf = {}) {
        return defaultsDeep(customConf, this._buildConfig(env));
    }

}

export default ConfigBuilder;<commit_msg>Fixing sourcemaps options<commit_after>import path from 'path';
import defaultsDeep from 'lodash/defaultsDeep';

const entryError =
    `Le point d'entrée correspond au point d'entrée dans la SPA,
le paramètre devrait être une chaine de caractère correspondant au fichier de lancement de l'application, ou à son dossier si c'est un index.js
Par exemple (et par défaut) path.resolve(process.cwd(), './src'), càd le dossier src du dossier courant`;

/**
 * Classe pour construire une configuration de façon simple, et modulable.
 *
 * @class ConfigBuilder
 */
class ConfigBuilder {
    configEnv = {};
    entries = [];
    output = {
        // libraryTarget: 'umd'
    };
    devtool = false;
    stats = {};
    plugins = [];
    loaders = [];
    extensions = ['.js', '.jsx'];
    rules = [];
    debugConfig = false;
    sourceMaps = false;
    externals = {};
    definedVariables = {};
    projectName = null;
    aliases = {};

    _debugInfo() {
        if (this.debugConfig) {
            console.log('########################################################################');
            console.log(arguments);
            console.log('########################################################################');
            console.log();
        }
    }

    /**
     * Ajoute un point d'entrée dans l'application
     * Cf https://webpack.js.org/configuration/entry-context/#entry
     *
     * @param {Array|string} entry Le chemin indiquant le point d'entrée de l'application (relatif, ou absolu), par exemple path.resolve(process.cwd(), './src'), ou path.resolve(process.cwd(), './src/index.js'), ou './src'
     *
     * @memberOf ConfigBuilder
     */
    addEntry(entry) {
        this._debugInfo('addEntry', [...arguments]);
        if (Array.isArray(entry)) {
            this.entries.push(...entry);
        } else if (typeof (entry) === 'string') {
            this.entries.push(entry);
        } else {
            throw new Error(entryError);
        }
    }

    /**
     * Indique le dossier de sortie du build webpack.
     * Cf https://webpack.js.org/configuration/output/#output-path
     *
     * @param {string} outPath Le path pour le dossier d'output
     * @param {boolean} [isRelative=true] Indique si le path donnée est relatif par rapport au dossier du projet (par défaut), ou absolu.
     *
     * @memberOf ConfigBuilder
     */
    setOuputPath(outPath, isRelative = true) {
        this._debugInfo('setOuputPath', [...arguments]);
        this.output.path = isRelative ? path.resolve(process.cwd(), outPath) : outPath;
    }

    /**
     * Ajoute un alias pour la résolution des sources (ex: test focus, etc).
     * Cf https://webpack.js.org/configuration/resolve/#resolve-alias
     *
     * @param {any} aliasName le nom de l'alias
     * @param {any} aliasPath le path du dossier/fichier pour la résolution
     * @param {boolean} [isRelative=true] Indique si le path donnée est relatif par rapport au dossier du projet (par défaut), ou absolu.
     *
     * @memberOf ConfigBuilder
     */
    addAlias(aliasName, aliasPath, isRelative = true) {
        this._debugInfo('addAlias', [...arguments]);
        this.aliases[aliasName] = isRelative ? path.resolve(process.cwd(), aliasPath) : aliasPath;
    }

    /**
     * Indique le dossier contenant les assets servies par la SPA (css, images, ...).
     * Cf https://webpack.js.org/configuration/output/#output-publicpath
     *
     * @param {string} path Le path pour le dossier d'assets
     *
     * @memberOf ConfigBuilder
     */
    setAssetsPublicPath(path) {
        this._debugInfo('setAssetsPublicPath', [...arguments]);
        this.output.publicPath = path;
    }

    /**
     * Indique le nom des chunk en cas de require.ensure.
     * @param {string} name
     */
    setChunkFileName(name) {
        this._debugInfo('setChunkFileName', [...arguments]);
        this.output.chunkFilename = name;
    }

    /**
     * Indique le nom du fichier d'output, sans l'extension. Peut contenir [name], [id] et [contenthash]
     * Cf https://webpack.js.org/configuration/output/#output-filename
     * Cf https://github.com/webpack-contrib/extract-text-webpack-plugin
     *
     * @param {string} name le nom du fichier JS et CSS sans l'extension (par défault, le nom du package NPM :`${npm_package_name}`)
     *
     * @memberOf ConfigBuilder
     */
    setFilename(name) {
        this._debugInfo('setFilename', [...arguments]);
        this.projectName = name;
        this.output.filename = this.projectName + '.js';
    }

    /**
     * Ajoute une extension dans la liste des extensions automatiquement résolues (par défaut, .js, .jsx, .json).
     * Cf https://webpack.js.org/configuration/resolve/#resolve-extensions
     *
     * @param {string} extension une extension à résoudre automatiquement
     *
     * @memberOf ConfigBuilder
     */
    addExtension(extension) {
        this._debugInfo('addExtension', [...arguments]);
        if (Array.isArray(extension)) {
            this.extensions.push(...extension);
        } else if (typeof (extension) === 'string') {
            this.extensions.push(extension);
        } else {
            throw new Error('Une extension est soit une chaine de caractère, soit tableau de chaine de caractère');
        }
    }

    _getInsertIndex(orderedList, newOrdre) {
        return orderedList.findIndex(({ ordre }) => ordre > newOrdre);
    }

    _getElement(orderedList, eltOrdre) {
        return orderedList.find(({ ordre }) => eltOrdre === ordre);
    }

    _getIndexElement(orderedList, eltOrdre) {
        return orderedList.findIndex(({ ordre }) => eltOrdre === ordre);
    }

    _insertElt(orderedList, newElt) {
        if (this._getIndexElement(orderedList, newElt.ordre) !== -1) {
            throw new Error('Un élément d\'ordre ' + newElt.ordre + ' existe déjà');
        }
        const index = this._getInsertIndex(orderedList, newElt.ordre);
        if (index === -1) {
            // L'élément a l'ordre le plus grand
            orderedList.push(newElt)
        } else {
            // Sinon, on injecte juste avant le plugin ayant un ordre plus élevé
            orderedList.splice(index, 0, newElt);
        }
    }

    _removeElt(orderedList, ordre) {
        const index = this._getIndexElement(orderedList, ordre);
        if (index === -1) {
            console.warn("Aucun élément d'ordre " + ordre + " n'existe, donc aucun élément n'a été retiré")
            return null;
        } else {
            return orderedList.splice(index, 1)[0];
        }
    }

    /**
     * Ajoute un plugin à la configuration webpack, à la position indiquée.
     * Cf https://webpack.js.org/configuration/plugins/#plugins
     *
     * @param {int} ordre la position dans la liste des plugins (les plugins par défaut sont espacés de 10, pour permettre l'injection)
     * @param {any} plugin Le plugin webpack à insérer
     *
     * @memberOf ConfigBuilder
     */
    addPlugin(ordre, plugin) {
        this._debugInfo('addPlugin', [...arguments]);
        this._insertElt(this.plugins, { ordre, plugin })
    }

    /**
     * Retire un plugin à la configuration webpack, à la position indiquée.
     *
     * @param {int} ordre la position dans la liste des plugins
     * @returns {object} l'élément supprimé
     * @memberof ConfigBuilder
     *
     */
    removePlugin(ordre) {
        this._debugInfo('removePlugin', [...arguments]);
        return (this._removeElt(this.plugins, ordre) || {}).plugin;
    }

    /**
     * Accède à un plugin de la configuration webpack, à la position indiquée.
     *
     * @param {int} ordre la position dans la liste des plugins
     * @returns {object} l'élément
     * @memberof ConfigBuilder
     *
     */
    getPlugin(ordre) {
        this._debugInfo('getPlugin', [...arguments]);
        return (this._getElement(this.plugins, ordre) || {}).plugin;
    }

    /**
     * Ajoute un loader/Rule simple à la configuration webpack, à la position indiquée.
     * Cf https://webpack.js.org/configuration/module/#rule
     * Cf https://webpack.js.org/configuration/module/#rule-loader
     * Cf https://webpack.js.org/configuration/module/#rule-use
     *
     * @param {int} ordre la position dans la liste des loaders (les loaders par défaut sont espacés de 10, pour permettre l'injection)
     * @param {RegExp} test le test pour matcher ou non le fichier
     * @param {string} loader le nom du loader
     * @param {object} [options=null] un objet d'options, pour le loader
     * @param {boolean} [isPreloader=false] si le loader est un pre-loader
     *
     * @memberOf ConfigBuilder
     */
    addSimpleLoader(ordre, test, loader, options = null, isPreloader = false) {
        this._debugInfo('addSimpleLoader', [...arguments]);
        const newLoader = { ordre, loader: { test, loader } };
        if (options !== null) {
            newLoader.loader.options = options;
        }
        if (isPreloader) {
            newLoader.loader.enforce = 'pre';
        }
        this._insertElt(this.loaders, newLoader)
    }

    /**
     * Ajoute un loader/Rule à la configuration webpack, à la position indiquée.
     * Cf https://webpack.js.org/configuration/module/#rule
     * Cf https://webpack.js.org/configuration/module/#rule-loader
     * Cf https://webpack.js.org/configuration/module/#rule-use     *
     * @param {int} ordre la position dans la liste des loaders (les loaders par défaut sont espacés de 10, pour permettre l'injection)
     * @param {object} rule l'objet
     *
     * @memberOf ConfigBuilder
     */
    addComplexLoader(ordre, rule) {
        this._debugInfo('addComplexLoader', [...arguments]);
        const newLoader = { ordre, loader: rule };
        this._insertElt(this.loaders, newLoader)
    }

    /**
     * Retire un loader à la configuration webpack, à la position indiquée.
     *
     * @param {int} ordre la position dans la liste des loaders
     * @returns {object} l'élément supprimé
     * @memberof ConfigBuilder
     *
     */
    removeLoader(ordre) {
        this._debugInfo('removeLoader', [...arguments]);
        return (this._removeElt(this.loaders, ordre) || {}).loader;
    }

    /**
     * Accède à un loader de la configuration webpack, à la position indiquée.
     *
     * @param {int} ordre la position dans la liste des loaders
     * @returns {object} l'élément
     * @memberof ConfigBuilder
     *
     */
    getLoader(ordre) {
        this._debugInfo('getLoader', [...arguments]);
        return (this._getElement(this.loaders, ordre) || {}).loader;
    }

    /**
     * Indique si les sourcemaps doivent être utilisé ou non.
     *
     * @param {boolean} useSourceMaps si les sourcemaps doivent être utilisé ou non.
     *
     * @memberOf ConfigBuilder
     */
    useSourceMaps(useSourceMaps = false) {
        this._debugInfo('useSourceMaps', [...arguments]);
        this.sourceMaps = useSourceMaps;
    }

    /**
     * Ajoute une variable qui sera définie par le plugin DefineVariable, et injectée (càd remplacée par sa valeur) par webpack.
     * Note : Il est nécessaire d'utiliser getDefinedVariables dans le cas d'une configuration non basée sur celle par défaut.
     *
     * @param {string} key le nom de la variable
     * @param {string} value la valeur de la variable
     *
     * @memberOf ConfigBuilder
     */
    addDefinedVariable(key, value) {
        this.definedVariables[key] = value;
    }

    /**
     * Retourne un objet de config pour le plugin webpack.DefinePlugin.
     *
     * @returns {object} l'objet de config pour le plugin DefinePlugin
     *
     * @memberOf ConfigBuilder
     */
    getDefinedVariables() {
        return this.definedVariables;
    }

    /**
     * Retourne le nom du fichier css (pour ExtractTextPlugin).
     *
     * @returns {string} le nom du fichier css
     *
     * @memberOf ConfigBuilder
     */
    getCssFilename() {
        return this.projectName + '.css';
    }

    /**
     * Ajoute un external à la configuration webpack, càd un nom qui sera résolu de manière global (jQuery, Backbone, ...)
     * Cf https://webpack.js.org/configuration/externals/
     *
     * @param {string} key le nom de la variable
     * @param {string|object} value la valeur à résoudre dans le bundle au runtime (peut être par type de lib), cf doc webpack
     *
     * @memberOf ConfigBuilder
     */
    addExternal(key, value) {
        this.externals[key] = value;
    }

    _buildConfig(env) {
        const config = {
            entry: this.entries,
            output: this.output,
            resolve: {
                extensions: this.extensions,
                alias: this.aliases
            },
            plugins: this.plugins.map(({ plugin }) => plugin).map(item => typeof (item) === 'function' ? item(env) : item),
            module: {
                rules: this.loaders.map(({ loader }) => loader).map(item => typeof (item) === 'function' ? item(env) : item)
            },
            externals: this.externals,
            stats: {
                colors: true,
                version: false,
                timings: false,
                assets: false,
                chunks: false,
                modules: false,
                reasons: false,
                children: false,
                source: false,
                errors: true,
                errorDetails: true,
                warnings: true
            }
        };

        config.devtool = this.sourceMaps && env.MINIMIFY ? 'source-map' : this.sourceMaps ? 'eval-source-map' : false;

        return config;
    }

    /**
     * Construit l'objet de configuration webpack.
     *
     * @param {object} [env={}] une object contenant des valeurs pour la résolution des plugins et loaders.
     * @param {object} [customConf={}] une configuration custom, pouvant être mergée avec la conf buildée.
     * @returns {object} la configuration buildée, et mergée
     *
     * @memberOf ConfigBuilder
     */
    toWebpackConfig(env = {}, customConf = {}) {
        return defaultsDeep(customConf, this._buildConfig(env));
    }

}

export default ConfigBuilder;<|endoftext|>